py4cl 機能メモ

# 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の内容](https://github.com/bendudson/py4cl)を参考にしつつ、列挙していく。`(ql:quickload py4cl)`実行済みを前提とする。

## import系

### `import-module`
簡単なimportだけが可能で、`from ... imoprt ...`や`import datetime.datetime`は後述の`import-function`/`python-exec`を利用する必要がある。引数の文字列は展開されない。
```common-lisp
(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した関数に対しては、下記の様にしてキーワード引数も利用できる。
```common-lisp
(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式"`が実行可能である必要がある。 
```common-lisp
(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 ...`をするには、[これを使うしかない](https://github.com/bendudson/py4cl/issues/50)（使ったところで、扱いずらさはあるが）。
```common-lisp
(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) でないといけない。前述の要領で、キーワードを渡すこともできる。
```common-lisp
(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`の返り値が使えることもある。第二引数はメソッド名。
```common-lisp
(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`は評価されないのが特徴。
```common-lisp
(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`以外のコマンドを利用する場合は、下記の設定が必要。
```common-lisp
(setq py4cl:*python-command* "python3")
```

### python辞書はclハッシュテーブル
```common-lisp
(py4cl:python-eval "{'a': 1, 'b': 2}") ; #<HASH-TABLE :TEST EQUAL :COUNT 2 {100520F473}> 
```

### 変数をpythonと共有する
下記のマクロを利用することで、clispだけでなくpython側でも同じ変数を束縛（代入）させることができる。clispのシンボルの扱い上、python側では全ての変数は大文字としている点に注意。
```common-lisp
(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`で値を代入する必要がある。例えば下記。
```common-lisp
(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のように`:`での整形はできないけれど、個人的には安定している方が好き。
```common-lisp
(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ではバッククォートの利用がほとんどないはずなので）。
```common-lisp
;; 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といった内容も紹介されているが、本記事では触れない。