py4cl
2024-10-12
Call Python libraries from Common Lisp
1Introduction
Py4CL is a bridge between Common Lisp and Python, which enables Common Lisp to interact with Python code. It uses streams to communicate with a separate python process, the approach taken by cl4py. This is different to the CFFI approach used by burgled-batteries, but has the same goal.
1.1Installing
Depends on:
- Currently tested with SBCL, CCL and ECL (after 2016-09-06). CLISPdoesn't (yet) have
uiop:launch-program
. - ASDF3 version 3.2.0 (Jan 2017) or later, as
uiop:launch-program
is used to run and communicate with python asyncronously. - Trivial-garbage, available through Quicklisp.
- Python 2 or 3
- (optional) The NumPy python library for multidimensional arrays
Clone this repository into ~/quicklisp/local-projects/
or other
location where it can be found by ASDF:
$ git clone https://github.com/bendudson/py4cl.git
then load into Lisp with
(ql:quickload :py4cl)
1.2Tests
Tests use clunit, and run on Travis using cl-travis. Most development is done under Arch linux with SBCL and Python3. To run the tests yourself:
(asdf:test-system :py4cl)
or(ql:quickload :py4cl/tests)
(py4cl/tests:run)
2Examples
Py4CL allows python modules to be imported as Lisp packages, python functions to be called from lisp, and lisp functions called from python. In the example below, SciPy's odeint function is used to integrate ODEs defined by a Lisp function. The result is a Lisp array, which is then plotted using the matplotlib plotting library.
(ql:quickload :py4cl)
(py4cl:import-module "numpy" :as "np")
(py4cl:import-module "scipy.integrate" :as "integrate")
;; Integrate some ODEs
(defparameter *data*
(integrate:odeint
(lambda (y time)
(vector (aref y 1) ; dy[0]/dt = y[1]
(- (aref y 0)))) ; dy[1]/dt = -y[0]
#(1.0 0.0) ; Initial state
(np:linspace 0.0 (* 2 pi) 20))) ; Vector of times
; (array-dimensions *data*) => (20 2)
;; Make a plot, save and show it in a window
(py4cl:import-module "matplotlib.pyplot" :as "plt")
(plt:plot *data*)
(plt:xlabel "Time")
(plt:savefig "result.pdf")
(plt:show)
More detailed examples of using python packages using py4cl
:
3Reference
3.1Direct evaluation of python code: python-eval
, python-exec
For direct access to the python subprocess, python-eval
evaluates an expression, converting the result to a suitable lisp
type. Note that there are nicer, more lispy wrappers around this function,
described below, but they are mostly built on top of python-eval
.
(asdf:load-system "py4cl")
(py4cl:python-eval "[i**2 for i in range(5)]") ; => #(0 1 4 9 16)
0 | 1 | 4 | 9 | 16 |
(py4cl:python-eval "{'hello':'world', 'answer':42}") ; => #<HASH-TABLE :TEST EQUAL :COUNT 2>
#<HASH-TABLE :TEST EQUAL :COUNT 2 {10036F03F3}>
Data is passed between python and lisp as text. The python function
lispify
converts values to a form which can be read by the lisp
reader; the lisp function pythonize
outputs strings which can be
eval
'd in python. The following type conversions are done:
Lisp type | Python type |
---|---|
NIL | None |
integer | int |
ratio | fractions.Fraction |
real | float |
complex | complex float |
string | str |
hash map | dict |
list | tuple |
vector | list |
array | NumPy array |
symbol | Symbol class |
function | function |
Note that python does not have all the numerical types which lisp has, for example complex integers.
Because python-eval
and python-exec
evaluate strings as python
expressions, strings passed to them are not escaped or converted as
other types are. To pass a string to python as an argument, call py4cl::pythonize
(let ((my-str "testing"))
(py4cl:python-eval "len(" (py4cl::pythonize my-str) ")" ))
7
Note that this escaping is done automatically by higher-level interfaces like
python-call
and chain
:
(let ((my-str "testing"))
(py4cl:python-call "len" my-str))
7
(let ((my-str "testing"))
(py4cl:chain (len my-str)))
7
If python objects cannot be converted into a lisp value, then they are stored and a handle is returned to lisp. This handle can be used to manipulate the object, and when it is garbage collected the python object is also deleted (using the trivial-garbage package).
(destructuring-bind (fig ax) (plt:subplots)
;; fig is #S(PY4CL::PYTHON-OBJECT :TYPE "<class 'matplotlib.figure.Figure'>" :HANDLE 6)
(py4cl:python-eval ax ".plot(" #(0 1 0 1) ")")
(plt:show))
The interface to python objects is nicer using chain
(see below):
(destructuring-bind (fig ax) (plt:subplots)
(py4cl:chain ax (plot #(0 1 0 1)))
(plt:show))
The python process can be explicitly started and stopped using
python-start
and python-stop
, but py4cl
functions start python
automatically if needed by calling python-start-if-not-alive
.
3.2Calling python functions: python-call
python-call
can be used to pass arguments to any python callable,
such as a function in a module:
(py4cl:python-exec "import math")
(py4cl:python-call "math.sqrt" 42)
6.4807405
or a lambda function:
(py4cl:python-call "lambda x: 2*x" 21)
42
Keywords are translated, with the symbol made lowercase:
(py4cl:python-call "lambda a=0, b=1: a-b" :b 2 :a 1)
-1
3.3Calling python methods: python-method
Python methods on objects can be called by using the python-method
function. The first argument
is the object (including strings, arrays, tuples); the second argument is either a string or a symbol
specifying the method, followed by any arguments:
(py4cl:python-method "hello {0}" 'format "world") ; => "hello world"
hello world
(py4cl:python-method '(1 2 3) '__len__) ; => 3
3
3.4Getting python attributes: python-getattr
The attributes of a python object can be accessed using the generic
functon python-getattr
, with the python object as first argument and
a string as the name of the attribute:
(py4cl:python-getattr '(1 2 3) "__doc__")
Note: Methods for this function can also be defined for lisp classes, enabling python code to access attributes of lisp objects. See below for details.
3.5Chaining python methods: chain
In python it is quite common to apply a chain of method calls, data
member access, and indexing operations to an object. To make this work
smoothly in Lisp, there is the chain
macro (Thanks to @kat-co and
parenscript for the inspiration). This consists of a target object,
followed by a chain of operations to apply. For example
(py4cl:chain "hello {0}" (format "world") (capitalize)) ; => "Hello world"
Hello world
which is converted to python
return "hello {0}".format("world").capitalize()
Hello world
The only things which are treated specially by this macro are lists and symbols at the top level. The first element of lists are treated as python method names, top-level symbols are treated as data members. Everything else is evaluated as lisp before being converted to a python value.
If the first argument is a list, then it is assumed to be a python function to be called; otherwise it is evaluated before converting to a python value. For example
(py4cl:chain (slice 3) stop)
3
is converted to the python:
return slice(3).stop
3
Symbols as first argument, or arguments to python methods, are evaluated, so the following works:
(let ((format-str "hello {0}")
(argument "world"))
(py4cl:chain format-str (format argument))) ; => "hello world"
hello world
Arguments to methods are lisp, since only the top level forms in chain
are treated specially:
(py4cl:chain "result: {0}" (format (+ 1 2))) ; => "result: 3"
result: 3
Indexing with []
brackets is commonly used in python, which calls the __getitem__
method.
This method can be called like any other method
(py4cl:chain "hello" (__getitem__ 4)) ; => "o"
o
but since this is a common method an alias []
is supported:
(py4cl:chain "hello" ([] 4)) ; => "o"
o
which is converted to the python
return "hello"[4]
o
For simple cases where the index is a value like a number or string (not a symbol or a list), the brackets can be omitted:
(py4cl:chain "hello" 4) ; => "o"
o
Slicing can be done by calling the python slice
function:
(py4cl:chain "hello" ([] (py4cl:python-call "slice" 2 4))) ; => "ll"
ll
which could be imported as a lisp function (see below):
(py4cl:import-function "slice")
(py4cl:chain "hello" ([] (slice 2 4))) ; => "ll"
ll
This of course also works with multidimensional arrays:
(py4cl:chain #2A((1 2 3) (4 5 6)) ([] 1 (slice 0 2))) ;=> #(4 5)
4 | 5 |
Sometimes the python functions or methods may contain upper case characters; class names often start with a capital letter. All symbols are converted to lower case, but the case can be controlled by passing a string rather than a symbol as the first element:
;; Define a class
(py4cl:python-exec
"class TestClass:
def doThing(self, value = 42):
return value")
;; Create an object and call the method
(py4cl:chain ("TestClass") ("doThing" :value 31)) ; => 31
Note that the keyword is converted, converting to lower case.3.6Printing from python
Since standard output is used for communication between lisp and python, this is
redirected (to a StringIO
buffer) while user python code is running. The
output from python functions is then sent to lisp, to be printed to
*standard-output*
. This means that anything printed by the python process may
only appear in chunks, as it is sent to lisp. The following does however work as
expected:
(py4cl:chain (print "hello world"))
; => prints "hello world", returns NIL
hello world
In python print_function
is imported from __future__
, so should be available
as a function in python 2.6+, as well as in version 3+.
3.7Asynchronous python functions: python-call-async
One of the advantages of using streams to communicate with a separate
python process, is that the python and lisp processes can run at the
same time. python-call-async
calls python but returns a closure
immediately. The python process continues running, and the result can
be retrieved by calling the returned closure.
(defparameter thunk (py4cl:python-call-async "lambda x: 2*x" 21))
(funcall thunk) ; => 42
42
If the function call requires callbacks to lisp, then these will only
be serviced when a py4cl
function is called. In that case the python
function may not be able to finish until the thunk is called. This
should not result in deadlocks, because all py4cl
functions can
service callbacks while waiting for a result.
3.8Importing functions: import-function
Python functions can be made available in Lisp by using import-function
. By
default this makes a function which can take any number of arguments, and then
translates these into a call to the python function.
(asdf:load-system "py4cl")
(py4cl:python-exec "import math")
(py4cl:import-function "math.sqrt")
(math.sqrt 42) ; => 6.4807405
6.4807405
If a different symbol is needed in Lisp then the :as
keyword can be
used with either a string or symbol:
(py4cl:import-function "sum" :as "pysum")
(pysum '(1 2 3)) ; => 6
6
This is implemented as a macro which defines a function which in turn calls python-call
.
3.9Importing modules: import-module
Python modules can be imported as lisp packages using import-module
.
For example, to import the matplotlib plotting library, and make its functions
available in the package PLT
from within Lisp:
(asdf:load-system "py4cl")
(py4cl:import-module "matplotlib.pyplot" :as "plt") ; Creates PLT package
T
This will also import it into the python process as the module plt
, so that
python-call
or python-eval
can also make use of the plt
module.
Like python-exec
, python-call
and other similar functions,
import-module
starts python if it is not already running, so that
the available functions can be discovered.
The python docstrings are made available as Lisp function docstrings, so we can see them
using describe
:
(describe 'plt:plot)
Functions in the PLT
package can be used to make simple plots:
(plt:plot #(1 2 3 2 1) :color "r")
(plt:show)
NIL
Notes:
import-module
should be used as a top-level form, to ensure thatthe package is defined before it is used.
- If using
import-module
within org-mode babel then the importshould be done in a separate code block to the first use of theimported package, or a condition will be raised like "Package NPdoes not exist."
3.10Exporting a function to python: export-function
Lisp functions can be passed as arguments to python-call
or imported functions:
(py4cl:python-exec "from scipy.integrate import romberg")
(py4cl:python-call "romberg"
(lambda (x) (/ (exp (- (* x x)))
(sqrt pi)))
0.0 1.0) ; Range of integration
0.4213504
Lisp functions can be made available to python code using export-function
:
(py4cl:python-exec "from scipy.integrate import romberg")
(py4cl:export-function (lambda (x) (/ (exp (- (* x x)))
(sqrt pi))) "gaussian")
(py4cl:python-eval "romberg(gaussian, 0.0, 1.0)") ; => 0.4213504
0.4213504
3.11Manipulating objects remotely: remote-objects
If a sequence of python functions and methods are being used to manipulate data, then data may be passed between python and lisp. This is fine for small amounts of data, but inefficient for large datasets.
The remote-objects
and remote-objects*
macros provide unwind-protect
environments
in which all python functions return handles rather than values to lisp. This enables
python functions to be combined without transferring much data.
The difference between these macros is remote-objects
returns a handle, but
remote-objects*
evaluates the result, and so will return a value if possible.
(py4cl:remote-objects (py4cl:python-eval "1+2")) ; => #S(PY4CL::PYTHON-OBJECT :TYPE "<class 'int'>" :HANDLE 0)
#S(PY4CL::PYTHON-OBJECT :TYPE "<class 'int'>" :HANDLE 4)
(py4cl:remote-objects* (py4cl:python-eval "1+2")) ; => 3
3
The advantage comes when dealing with large arrays or other datasets:
(time (np:sum (np:arange 1000000)))
; => 3.672 seconds of real time
; 390,958,896 bytes consed
(time (py4cl:remote-objects* (np:sum (np:arange 1000000))))
; => 0.025 seconds of real time
; 32,544 bytes consed
3.12setf
-able places
The python-eval
function is setf
-able, so that python objects can
be assigned to by using setf
. Since chain
uses python-eval
, it is also
setf
-able. This can be used to set elements in an array, entries in a dict/hash-table,
or object data members, for example:
(py4cl:import-module "numpy" :as "np")
T
(py4cl:remote-objects*
(let ((array (np:zeros '(2 2))))
(setf (py4cl:chain array ([] 0 1)) 1.0
(py4cl:chain array ([] 1 0)) -1.0)
array))
; => #2A((0.0 1.0)
; (-1.0 0.0))
#2A((0.0 1.0) (-1.0 0.0))
Note that this modifies the value in python, so the above example only
works because array
is a handle to a python object, rather than an
array which is stored in lisp. The following therefore does not work:
(let ((array (np:zeros '(2 2))))
(setf (py4cl:chain array ([] 0 1)) 1.0
(py4cl:chain array ([] 1 0)) -1.0)
array)
; => #2A((0.0 0.0)
; (0.0 0.0))
#2A((0.0 0.0) (0.0 0.0))
The np:zeros
function returned an array to lisp; the array was then
sent to python and modified in python. The modified array is not
returned, since this would mean transferring the whole array. If the
value is in lisp then just use the lisp functions:
(let ((array (np:zeros '(2 2))))
(setf (aref array 0 1) 1.0
(aref array 1 0) -1.0)
array)
; => #2A((0.0 1.0)
; (-1.0 0.0))
#2A((0.0 1.0) (-1.0 0.0))
3.13Passing lisp objects to python: python-getattr
Lisp structs and class objects can be passed to python, put into data structures and returned:
(py4cl:import-function "dict") ; Makes python dictionaries
(defstruct test-struct
x y)
(let ((map (dict :key (make-test-struct :x 1 :y 2)))) ; Make a dictionary, return as hash-map
;; Get the struct from the hash-map, and get the Y slot
(test-struct-y
(py4cl:chain map "key"))) ; => 2
2
In python this is handled using an object of class UnknownLispObject
, which
contains a handle. The lisp object is stored in a hash map
*lisp-objects*
. When the python object is deleted, a message is sent to remove
the object from the hash map.
To enable python to access slots, or call methods on a struct or class, a
handler function needs to be registered. This is done by providing a method
for generic function python-getattr
. This function will be called when a
python function attempts to access attributes of an object (__getattr__
method).
;; Define a class with some slots
(defclass test-class ()
((value :initarg :value)))
;; Define a method to handle calls from python
(defmethod py4cl:python-getattr ((object test-class) slot-name)
(cond
((string= slot-name "value") ; data member
(slot-value object 'value))
((string= slot-name "func") ; method, return a function
(lambda (arg) (* 2 arg)))
(t (call-next-method)))) ; Otherwise go to next method
(let ((instance (make-instance 'test-class :value 21)))
;; Get the value from the slot, call the method
;; python: instance.func(instance.value)
(py4cl:chain instance (func (py4cl:chain instance value)))) ; => 42
42
Inheritance then works as usual with CLOS methods:
;; Class inheriting from test-class
(defclass child-class (test-class)
((other :initarg :other)))
;; Define method which passes to the next method if slot not recognised
(defmethod py4cl:python-getattr ((object child-class) slot-name)
(cond
((string= slot-name "other")
(slot-value object 'other))
(t (call-next-method))))
(let ((object (make-instance 'child-class :value 42 :other 3)))
(list
(py4cl:chain object value) ; Call TEST-CLASS getattr method via CALL-NEXT-METHOD
(py4cl:chain object other))) ;=> (42 3)
42 | 3 |