LoginSignup
2
1

More than 1 year has passed since last update.

py4cl 機能メモ

Last updated at Posted at 2023-01-31

TL;DR

(ql:quickload :py4cl)
(py4cl:python-exec "import numpy as np")
(py4cl:python-eval "np.array([1, 2, 3])") ; #(1 2 3)

機能紹介

py4clでできることを公式gitの内容を参考にしつつ、列挙していく。(ql:quickload py4cl)実行済みを前提とする。

import系

import-module

簡単なimportだけが可能で、from ... imoprt ...import datetime.datetimeは後述のimport-function/python-execを利用する必要がある。引数の文字列は展開されない。

(py4cl:import-module "numpy" :as "np")
(np:arange 0 5 1) ; #(0 1 2 3 4)
(let ((name "numpy"))
  (py4cl:import-module name)) ; ERROR

import-function

対応するimport-moduleを先に実行しておく必要がある。関数名単体で利用しようとすると、:asを使わねばならず、少し冗長。importした関数に対しては、下記の様にしてキーワード引数も利用できる。

(py4cl:import-module "random")
(py4cl:import-function "random.choice")
(py4cl:import-function "random.sample" :as "sample")
(random.choice '(1 5)) ; 1
(sample '(1 2 3 4 5) :k 3) ; (4 2 5)

eval系

python-eval

渡したpython式が実行され、その値が返される。return "python式"が実行可能である必要がある。

(py4cl:python-eval "[x for x in [0, 1, 2, 3]]") ; #(0 1 2 3)
(py4cl:python-eval "from random import choices") ; ERROR

python-exec

渡したpython式が実行され、その値は返されない。from ... import ...をするには、これを使うしかない(使ったところで、扱いずらさはあるが)。

(py4cl:python-exec "[x for x in [0, 1, 2, 3]]") ; NIL
(py4cl:python-exec "from random import choices")
(choices '(1 2) :k 5) ; ERROR

python-call

第一引数はpythonの関数名 (collable) でないといけない。前述の要領で、キーワードを渡すこともできる。

(py4cl:python-exec "from random import choices")
(py4cl:python-call "choices" '(1 2) :k 5) ; #(2 1 2 2 1)
(py4cl:python-call "range" 0 5) ; #S(PY4CL::PYTHON-OBJECT :TYPE "<class 'range'>" :HANDLE 1) 
(py4cl:python-call "lambda a, b=1: a + b" :a 1 :b 2) ; 3

python-method

第一引数はpythonのオブジェクトである必要がある。そのため、py4cl:python-evalの返り値が使えることもある。第二引数はメソッド名。

(py4cl:python-method "str is object." 'split "is") ; #("str " " object.") 
(py4cl:python-exec "import pandas as pd")
(py4cl:python-method (py4cl:python-eval "pd.Series([1, 2, 3])") 'max)

chain

書式は(py4cl:python-chain OBJ (METHOD ARGS) (METHOD ARGS))OBJARGSは評価されて、METHODは評価されないのが特徴。

(py4cl:import-module "pandas" :as "pd")
(let ((df (py4cl:python-call "pd.DataFrame" '(1 2 3 4 5))))
  (py4cl:chain df
               (rolling 3)
               (mean)
               (to_numpy))) ; #2A((NAN) (NAN) (2.0) (3.0) (4.0))

letを使わずpy4cl:chain内に直接py4cl:python-call "pd.DataFrame" ...を書き込むと、(なぜか)エラー。

その他

パス設定が必要(かも)

python以外のコマンドを利用する場合は、下記の設定が必要。

(setq py4cl:*python-command* "python3")

python辞書はclハッシュテーブル

(py4cl:python-eval "{'a': 1, 'b': 2}") ; #<HASH-TABLE :TEST EQUAL :COUNT 2 {100520F473}> 

変数をpythonと共有する

下記のマクロを利用することで、clispだけでなくpython側でも同じ変数を束縛(代入)させることができる。clispのシンボルの扱い上、python側では全ての変数は大文字としている点に注意。

(ql:quickload :uiop)

(defun py-code-concate (params values &optional (con ""))
  (if (car params)
      (py-code-concate (cdr params)
                        (cdr values)
                        (let ((form (uiop:strcat con (string-upcase (string (car params))) "=")))
                          (if (stringp (car values))
                              (uiop:strcat form "'~A';")
                              (uiop:strcat form "~A;"))))
      con))

;; https://stackoverflow.com/questions/75296433/getting-value-in-a-let-binded-list-common-lisp?noredirect=1#comment132866726_75296433
(defmacro py-let ((&rest bindings) &body body)
  (let ((symbols (gensym))
        (values (gensym))
        (pycode (gensym)))
    `(let ,bindings
       (let ((,symbols ',(mapcar #'car bindings))
             (,values (list ,@(mapcar #'car bindings))))
         (let ((,pycode (py-code-concate ,symbols ,values)))
           (py4cl:python-exec (eval `(format nil ,,pycode ,@,values)))
           ,@body)))))

(let ((bar "PIyo"))
  (py-let ((Hoge bar)
           (FuGa (+ 1 2 3)))
    (py4cl:python-eval "print(HOGE, FUGA)"))) ; Piyo 6

f文字列は(多分)ない

pythonとclispでは変数が共有されていない(できない)ため、(let ((a "HOGE")) (py4cl:python-eval "f'{a}-PIYO'"))としても期待通りの動作にはならない。そのためf文字列から変数を抜き出して、python-execで値を代入する必要がある。例えば下記。

(ql:quickload :cl-ppcre)

(defmacro fstr (str)
  (labels ((peel (str) (subseq str 1 (1- (length str))))
           (convert (&rest rest)
             (string-upcase (subseq (first rest) (fourth rest) (fifth rest)))))
    (let ((values (gensym))
          (pycode (gensym))
          (symbols (mapcar #'read-from-string
                           (mapcar #'peel (cl-ppcre:all-matches-as-strings "{.*?[:|}]" str))))
          (str-upper (cl-ppcre:regex-replace-all "{.*?[:|}]" str #'convert)))
      `(let* ((,values (list ,@symbols))
              (,pycode (py-code-concate ',symbols ,values)))
         (print `(format nil ,,pycode ,@,values))
         (py4cl:python-exec (eval `(format nil ,,pycode ,@,values)))
         (py4cl:python-eval (uiop:strcat "f\"" ,str-upper "\""))))))

(let ((Piyo 30)
      (fugA (+ 1 2 3))
      (HOGe "hoo"))
  (fstr "HogeHoge{Piyo}{fugA}{HOge}")) ; HogeHoge306hoo

上記のものは実際に使ってみると英文字の大文字・小文字で良くエラーを起こすので、clispで完結している下記のようなものの方が使いやすい。pythonのように:での整形はできないけれど、個人的には安定している方が好き。

(defmacro fstr (str)
  (labels ((peel (str) (subseq str 1 (1- (length str)))))
    (let ((symbols (mapcar #'(lambda (str)
                               (let ((val (read-from-string str)))
                                 (if (stringp val)
                                     str
                                     val)))
                           (mapcar #'peel (cl-ppcre:all-matches-as-strings "{.*?}" str))))
          (format (cl-ppcre:regex-replace-all "{.*?}" str "~A")))
      `(format nil ,format ,@symbols))))

(let ((piyo "hoge")
      (fuga (+ 1 2)))
  (fstr "hoge{piyo}{fuga}")) ; hogehoge3

最終手段

pythonで言うところの三重引用符に当たるマクロを書いて、pythonコードをそのまま読み込ませるのが一番早いという噂もある。下記のマクロでは#`...`のように、シャープバッククォートとバッククォートで囲んだ間がエスケープされる(pythonではバッククォートの利用がほとんどないはずなので)。

;; https://stackoverflow.com/questions/18045842/appending-character-to-string-in-common-lisp
(defun adjustable-string (s)
  (make-array (length s)
              :fill-pointer (length s)
              :adjustable t
              :initial-contents s
              :element-type (array-element-type s)))

(set-dispatch-macro-character #\# #\`
  #'(lambda (stream char1 char2)
      (do ((c (read-char stream) (read-char stream t nil)) (result (adjustable-string "")))
          ((or (not (characterp c)) (eq #\` c)) result)
        (vector-push-extend c result))))

(py4cl:python-exec #`
def hoge():
    a = "Here"
    return f"{a} can put anything except for back quote."
`)
(py4cl:python-eval "hoge()") ; "Here can put anything except for back quote." 

未紹介の機能

公式gitではpython-getattr, python-call-async, export-function, remote-objects, スライス, setf-ableといった内容も紹介されているが、本記事では触れない。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1