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))
。OBJ
とARGS
は評価されて、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といった内容も紹介されているが、本記事では触れない。