在PyCall扩展包中,模仿Python的
import
语句,提供了一个可以导入Python模块的@pyimport
宏。并且,为能在Julia中使用模块内的函数和常量做了封装,以及支持在Julia与Python间的自动类型转换。
同时,它还提供了对Python对象进行底层操作的设施。其中包括能与不透明的Python对象相对应的‘PyObjec‘类型,以及在Julia语言中对Python函数进行调用且做类型转换的pycall
。
安装
在Julia中,只需要使用Pkg.add("PyCall")
,就可以通过包管理进行安装。需要你安装Julia0.2及之后的版本。
最新的开发版本可以从https://github.com/stevengj/PyCall.jl取得。如果你想要在安装之后再做版本切换,你可以cd
,然后
~/.julia/PyCallgit pull git://github.com/stevengj/PyCall.jl master
使用
下面是一个简单的调用Python中math.sin
的例子,并和Julia内建的sin
进行了比较:
using PyCall
@pyimport math
math.sin(math.pi / 4) - sin(pi / 4) # returns 0.0
数值、布尔、字符串、IO stream、函数、元组、数组或列表、以及包含这些类型的字典等,它们都会自动进行类型的转换(Python函数会被转换或传递为Julia的函数,反之亦然)。其它类型则是通过通用的PyObject提供的。
Python编写的子模块必须通过单独@pyimport
导入,并且必须提供一个标识符,以使其能在Julia中使用。例如:
@pyimport numpy.random as nr
nr.rand(3,4)
上例中,Julia利用了从Python的Numpy数组API转换过来的多维数组。从Julia向Python传递数据默认不做拷贝。而从Python编程接口取得数据,则取得的只是拷贝。Python到Julia数组间的无拷贝转换可以通过下面的PyArray
类型实现。
关键字参数也可以在两者间传递。例如,matplotlib的pyplot使用关键字参数描述绘图的选项,并且这个功能可以在Julia中通过下面的方式访问:
@pyimport matplotlib.pyplot as plt
x = linspace(0,2*pi,1000); y = sin(3*x + 4*cos(2*x));
plt.plot(x, y, color="red", linewidth=2.0, linestyle="--")
plt.show()
尽管如此,为了更好地与绘图终端集成,且为了避免使用一次show
函数,建议使用matplotlib时调用Julia的PyPlot模块。
任意的Julia函数都可以作为参数传递给Python的某个子程序。例如,为找到cos(x) - x的根,我们可以从scipy.optimize调用牛顿求解器:
@pyimport scipy.optimize as so
so.newton(x -> cos(x) - x, 1)
在Julia中导入的Python模块的最大不同是,对象的属性(或成员)的访问要通过 o[:attribute]
,而不是o.attribute
。并且,你需要使用get(o,
,而不能使用
key)o[key]
。这是因为Julia还不允许对 .
操作符进行重载。可以在下面PyObject
小节查看这方面的内容。此外,pywrap
函数提供了创建匿名模块来模仿.
访问方式(这其实和@pyimport
的作用相同)。例如,我们可以像下面这样使用Biopython:
@pyimport Bio.Seq as s
@pyimport Bio.Alphabet as a
my_dna = s.Seq("AGTACACTGGT", a.generic_dna)
my_dna[:find]("ACT")
而在Python中,最后一步应该是my_dna.find("ACT")
。
排除故障
这里有一些常见问题的解决办法:
- 正如上面提到的,使用
foo[:bar]
而不是foo.bar
来访问Python对象的属性和方法。 - 有些时候调用Python的函数会导致失败,因为PyCall没有将其识别为一个可调用的对象(因为在Python中有许多类型的对象是可以调用的)。解决的办法是使用
pycall(foo,
来代替
PyAny, args...)foo(args...)
。如果你想要调用Python中的foo.bar(args...)
,那是使用pycall(foo["bar"],
是比较好的方式。它使用
PyAny, args...)foo["bar"]
代替foo[:bar]
,可以避免bar属性中的所有自动转换。 - 如果PyCall不能够找到你想要的Python版本,可以尝试将环境变量
PYTHON
设置为可执行python
的完整路径。注意,目前PyCall还不能与Canopy/EPD
Python一起工作。我们建议使用Anaconda代替。 - PyCall默认还不支持将当前目录作为Pyhon的搜索路径。如果你想要从当前目录加载Python模块,则执行
unshift!(PyVector(pyimport("sys")["path"]),
。
"")
Python对象编程接口
在Julia中,@pyimport
宏通过下面的PyObject
,在许多子程序之上建立了对Python对象的操作。这些子程序可以用来对Julia和Python之间传递的类型和数据进行强大的操作,以及访问Python其它的功能(特别是稍后将提到的协程的底层接口)。
类型
PyObject
PyCall模块也提供了一个代表Python对象引用的新类型PyObject
,即对Python的C API的一个封装。
PyObject(o)
为Julia的一些类型提供了构造函数。相应地,PyCall同时也提供了convert(T,
来将PyObjects转换为一个Julia的类型
o::PyObject)T
。目前,支持的类型有:数值(整型、实数和复数)、布尔型、字符串、函数,以及它们组成的元组、数组或序列。正在计划提供更多的支持(Julia符号被转换成Python字符串)。
o::PyObject
、o[:attribute]
与Python中的o.attribute
是等同的,并做了自动类型转换。如果想要在取得一个PyObject
的属性时不做类型转换,则采用o["attribute"]
代替。
o::PyObject
、get(o,
与Python中的
key)o[key]
是等同的,并做了自动类型转换。如果想要像PyObject
一样做get操作,并不做类型转换,则使用get(o,
代替,或者使用更为通用的
PyObject, key)get(o, SomeType, key)
。
如果你想在找不到关键字的时候提供一个默认值,则可以调用get(o, key, default)
或者get(o,
。
SomeType, key, default)
类似地,set!(o, key, val)
等同于Python中的o[key]
,
= valdelete!(o, key)
等同于Python中的del
o[key]
PyArray
支持将Numpy的多维数组(ndarray
)转换为Julia的Array
类型。不过,转换之后使用的是数据的拷贝。
另外,PyCall模块提供了一个新的类型PyArray
(是AbstractArray
的一个子类),它实现了对一个NumPy数组的非拷贝封装(目前仅支持对包含数值类型和对象类型的数组)。使用方法是,对于返回ndarray
的pycall
,则使用PyArray
作为返回值的类型。对于一个ndarray
对象,则对其调用PyArray(o::PyObject)
进行转换。从技术上讲,PyArray
可以对任何的使用Numpy数组接口提供数据指针和形状信息的Python对象使用。
按照惯例,当向Python传递数组的时候,Julia的Array
类型会被转换为PyObject
类型,而且不会通过NumPy创建一个拷贝。比如Julia的Array
作为pycall
的参数传递时就是这样。
PyVector
PyCall模块提供了一个新的类型PyVector
(是AbstractVector
的一个子类),它实现了对任意Python列表或序列对象的非拷贝封装。与PyArray
不同,PyVector
类型不仅限于对NumPy
数组使用(尽管相对于PyVector
来说,PyArray
通常效率更高)。使用方法是,将使用PyArray
作为返回列表或序列对象(包括元组)的pycall
的返回值类型。或者,对一个序列的对象o
调用PyVector(o::PyObject)
。
v::PyVector
(即PyVector类型的变量v
)支持通过v[index]
对元素进行引用和分配,以及配合delete!
和pop!
进行操作。copy(v)
可以将v
转换为一个普通的Julia Vector
。
PyDict
PyCall模块同时提供了一个新的类型PyDict
(是Association
的一个子类),它实现了对任意Python字典对象(或者任何一个实现了mapping协议的对象)的非拷贝封装,使用上与PyVector
类似。使用方法是,将PyDict
作为返回字典的pycall
的返回值的类型。或者对一个字典对象o
调用PyDict(o::PyObject)
。PyDict
默认是一个自动根据运行时所给参数的类型进行构造的Any
的字典(或许实际上是
=> AnyPyAny => PyAny
,当然还有可能是其他,比如PyDict{Int32,ASCIIString})。但是,如果你已经确切地知道了所要创建字典的参数类型,那么你可以选择使用PyDict{K,V}
,固定构造器参数K
和V
的类型来创建。
目前,想Python传递一个Julia字典,将会创建一个Julia字典的拷贝。
PyTextIO
Julia的IO
streams会被转换成实现了RawIOBase接口的Python对象。在Python中,RawIOBase可以用来进行二进制读取和输出。尽管如此,有些Python代码(特别是unpickling的代码)还是期望一个stream能够实现TextIOBase接口。它与RawIOBase主要的不同是read
和readall
函数返回的是字符串而不是字节数组。如果你要向一个text-IO对象传递IO
stream,则调用PyTextIO(io::IO)
进行转换。
目前,尚且没有好的办法可以让Python在接收stream参数后,自动地确定是返回字符串还是二进制的数据。并且,与Python不同,Julia打开文件的时候不会单独地区分"text"或"binary"模式。所以,我们无法简单地从文件打开的方式来确定转换方式。
PyAny
在进行类型转换的时候,PyAny
类型可以告诉PyCall通过在运行时侦测Python数据类型,然后将其转换为Julia的本地类型。也就是说,pycall(func,
和
PyAny, ...)convert(PyAny, o::PyObject)
是自动将运行的结果转换为Julia的类型(如果可以转换合法的话)。不过,有的时候这样虽然很方便,但是可能会降低一些程序的性能(这是由于运行时类型检查存在开销,以及Julia
JIT编译器不能作类型推断)。
调用Python
在大多数时候,@pyimport
都可以在运行时对Python类型进行检测,并自动地将其转换为合适的Julia类型。尽管如此,通过下面更底层的函数可以进行更强大的类型转换控制。比如,使用非拷贝的PyArray
来转换Python多维数组,而不是将其拷贝至Array
。
在已确切知晓Python返回类型的情况下使用pycall
,将有助于提升程序的性能。因为这样可以减少运行时类型推断的开销,同时可以为Julia编译器提供更多的类型信息。
pycall(function::PyObject, returntype::Type,
args...)使用指定的参数
args...
调用指定的Python函数function
,并且指定Python调用的返回值类型returntype
。指定参数的类型为标准的Julia类型,并且可以被自动转换为相应Python类型。被调用的Python函数通常是从某个模块中查找。返回值类型
returntype
可以是一个PyObject
(即未转换的Python对象的引用),或者是一个能自动转换类型的PyAny
。pyimport(s)
导入参数
s
指定的Python模块(s
可以是一个字符串或者符号),并且返回模块的引用(即一个PyObject
)。假设将返回模块的引用赋值给变量s,就可以使用
s[name]
或者符号来查找模块中的函数或者其它符号。如果s
是一个原生PyObject
,则name可以为字符串;如果是s
只是一个自动转换类型,则name可以为符号(即:name
)。与
@pyimport
宏所不同的是,这中方式不是定义一个Julia模块,并且其的成员不能用s.name
访问。pyeval(s::String, rtype=PyAny; locals...)
将参数
s
当作一个Ptyhon代码串进行求值,并将返回值转换为rtype
指定的类型(默认是PyAny
)。余下的其它参数用来定义在表达式中被使用的关键字及值。例如:pyeval("x
返回
+ y", x=1, y=2)3
。pybuiltin(s)
查找一个Python内建模块(builtin)中的全局成员
s
。s
可以为一个字符串或者符号。如果为字符串,则pybuiltin(s)
返回的是一个PyObject
。如果为符号,则将所找到的成员转换为PyAny
返回。pywrap(o::PyObject)
返回一个经过封装的匿名模块
w
,它支持使用w.member
的方式访问参数o
指定的PyObject
。例如,@pyimport
等同于
module as namename = pywrap(pyimport("module"))
。如果一个Python模块中含有与Julia的保留字冲突的标识符,则它们不能以
w.member
形式访问。因此,必须使用w.pymember(:member)
(这个方法对所有的转换为PyAny
的类型都有效),或者w.pymember("member")
的方式访问(这个方法用于原生的PyObject
)。
初始化
当你调用任何高层的PyCall子程序时,Python解释器(与名为python
的可执行程序相对应)就会默认地进行初始化,并在退出Julia前,一直留在内存中。
尽管如此,你或许想要改变这些默认的行为。比如改变默认的Python解释器的版本,通过ccall
直接调用底层函数,或者想要释放Python消耗的内存。那么,可以采用以下的方式办到:
- 设置Python解释器的版本
PyCall使用环境变量
PYTHON
来指定Python的可执行版本。或者在没有指定环境变量时,使用"python"
确定使用的是哪个Python库。你可以使用操作系统通常的方式设置环境变量(比如在Unix中,可以在运行Julia之前,在shell中设置),或者在Julia中通过ENV["PYTHON"]
设置。另外,你可以显式地调用下面的
= "..."pyinitialize
。 pyinitialize(s::String)
使用参数
s
指定的,与python
公共库或可执行文件名相对应的Python库,初始化Python解释器。调用
pyinitialize()
时,默认的是运行pyinitialize(get(ENV,"PYTHON","python"))。但是在罕见的情况下,你还是需要进行改变。在通过
ccall使用任何底层的Python函数之前,你必须显式地调用
pyinitialize。而在使用高层次的函数时,则会自动对其进行调用。对
pyinitialize`的多次调用是安全,因为后续的调用什么事都不会做。pyfinalize()
结束Python解释器,并释放相关的内存。
在调用了这个函数之后,你就不能再使用
pyinitialize
重新启动Python解释器(这样会抛出一个异常)。原因是一些Python模块(比如numpy)的初始化程序被调用多次将会崩溃。后续多次地调用
pyfinalize
不会再做任何事。在调用了pyfinalize
之后,绝不要再对Python函数和没有拷贝为Julia原始类型的数据进行访问。- Python的版本好本存储在全局变量
pyversion::VersionNumber
中。
GUI事件循环
对于有GUI的Python包,特别是像matplotlib (或者MayaVi、Chaco)这样的绘图包,可以非常方便地在Julia中以异步任务的方式启动一个GUI事件循环,比如鼠标点击事件。所以,GUI响应时并没有阻止Julia的输入提示符。PyCall包含了实现一些常用跨平台GUI工具的事件循环的函数。这些工具的Python模块有:wxWidgets,GTK+,via
the PyQt4或者PySide。
你可以用以下方式设置一个GUI事件循环:
pygui_start(gui::Symbol=pygui())
:这里的参数
gui
所指定的工具,可以使用:wx
、:gtk
或者:qt
启动一个相应的事件循环。参数的默认值是pygui()
返回的当前的默认GUI。给参数gui
传递一个指定值,也会改变默认的GUI,这相当于调用了下面的pygui(gui)
。你有可能会同时启动多个GUI工具的事件循环。那么,对同一个GUI工具库多次调用pygui()
是没有用的,只不过每次都会设置一下pygui
默认的返回值。pygui()
:返回当前默认的GUI工具名的符号。如果没有设定默认的GUI,则返回的是
:wx
、:gtk
或:qt
等Python包中最新安装的那个。pygui(gui::Symbol)
会改变默认的GUI。pygui_stop(gui::Symbol=pygui())
:停止参数
gui
所代表的GUI的事件循环,参数的默认值是当前pygui
的返回值。成功则返回true
,否则返回false
。
在使用提供GUI的python库时,其实在导入之前,就已经可以很方便地启动一个GUI工具的事件循环。但是,有的时候还是需要显式地指定使用的是哪一个库,以及哪种交互模式。为了让这更加简单,可以使用一些对流行Python库做封装的Julia模块,比如Julia的PyPlot 模块。
访问Python的底层API
If you want to call low-level functions in the Python C API, you can
do so using ccall
. Just remember to call pyinitialize()
first,
and:
如果想要调用Python底层的C API,你可以使用ccall
。不过,要记住首先得调用pyinitialize()
,并且:
- 使用
pysym(func::Symbol)
来获取传递给ccall
的函数的指针,符号func
代表的是Python的API。比如,你可以使用ccall(pysym(:Py_IsInitialized),
的方式调用
Int32, ())int Py_IsInitialized()
。 - PyCall使用
PyPtr
定义PythonObject*
类型的别名,并且会将PythonObject
参数转换为此类型。PythonObject(p::PyPtr)
可以用于创建一个PyPtr
类型值的Julia访问封装器。 - 上面提到过,使用
PythonObject
以及convert
程序,可以实现Julia中的类型和PythonObject*
引用的相互转换。 - 当一个Python函数返回一个新的引用的时候,Julia就会立即将
PyPtr
类型的返回值转换为PythonObject
对象。目的是为了获取它们的在Python中的引用计数,以确定在Julia中什么时候对其进行垃圾回收。例如PythonObject(ccall(func,
。
PyPtr, ...))重要提醒:涉及返回借引用(borrowed reference)的Python程序时,你需要使用
pyincref(PyObject(...))
来获得一个新的引用。 - 你可以使用
pyincref(o::PyObject)
和pydecref(o::PyObject)
来手动地增加或减少引用计数。主要是因为有些时候底层函数可能会拿到一个未公开的引用或者返回一个借来的引用。 pyerr_check(msg::String)
可以用于检查程序是否抛出了一个Python异常,然后相应地抛出一个Julia异常。这个异常同时包含了msg
和Python异常对象。pyerr_clear()
可以用于清除Python异常状态。pytype_query(o::PyObject)
函数,如果能够转换其返回值,则将返回值转换为一个原始Julia类型,否则返回一个PyObject
。pyisinstance(o::PyObject, t::Symbol)
可以用于查询参数
o
是否是参数t
指定的Python类型,t
是Python的C
API中的一个全局PyTypeObject
标识符。例如,pyisinstance(o,
o
:PyDict_Type)用于检查参数是否是一个Python字典。 另外,
pyisinstance(o::PyObject,
t::PyObject)同样用于检查是否是一个Python类型对象
t。
pytypeof(o::PyObject)返回参数
o的Python类型。它与Python里的
type(o)`作用相同。
作者
这个扩展包是由Steven G. Johnson编写.
翻译至PyCall的github文档https://github.com/stevengj/PyCall.jl,转载请注明出处。