楔子
我们之前在介绍Cython语法的时候,一直永远都是一个pyx文件,如果是多个pyx文件改怎么办?怎么像Python那样进行导入呢?
Python提供了modules和packages来帮助我们组织项目,这允许我们将函数、类、变量等等,按照各自的功能或者实现的业务,分组到各自的逻辑单元中,从而使项目更容易理解和定位。并且模块和包也使得代码重用变得容易,在Python中我们使用import语句访问其它module和package中的函数。
而Cython也支持我们将项目分成多个模块,首先它完全支持import语句,并且含义与Python中的含义完全相同。这就允许我们在运行时访问外部纯Python模块中定义的Python对象,或者其它扩展模块中定义的可以被访问的Python对象。
但故事显然没有到此为止,因为只有import的话,Cython是不允许两个模块访问彼此的cdef、cpdef定义的函数、ctypedef、struct等等,也不允许访问对其它的扩展类型进行C一级的访问。
而为了解决这一问题,Cython提供了三种类型的文件来组织Cython文件一级C文件。到目前为止,我们一直使用扩展名为pyx的Cython源文件,它是包含代码的逻辑的实现文件。但是还有两种类型的文件,分别是扩展名为pxd的文件和扩展名为pxi的文件。
pxd文件你可以想象成类似于C中的头文件,用于存放一些声明之类的,而Cython的cimport就是从pxd文件中进行属性导入。
这一节我们就来介绍cimport语句的详细信息,以及pyx、pxd、pxi文件之间的相互联系,我们如何使用它们来构建更大的Cython项目。有了cimport和这三种类型的文件,我们就可以有效地组织Cython项目,而不会影响性能。
Cython的实现文件(pxd文件)和声明文件(pxi文件)
我们目前一直在处理pyx文件,它是我们编写具体Cython代码的文件,当然它和py文件是等价的。如果的Cython项目非常小,并且没有其它代码需要访问里面的C级结构,那么一个pyx文件足够了。但如果我们想要共享pyx文件中的C级结构,那么就需要pxd文件了。
假设我们有这样一个Cython文件:lover.pyx
# lover.pyx
from libc.stdlib cimport malloc, free
ctypedef double real
cdef class Girl:
cdef public :
str name # 姓名
long age # 年龄
str gender # 性别
cdef real *scores # 分数,这里我们的double数组长度为3,但是real *不能被访问,所以它不可以使用public
def __cinit__(self, *args, **kwargs):
self.scores = <real *> malloc(3 * sizeof(real))
def __init__(self, name, age, gender):
self.name = name
self.age = age
self.gender = gender
def __dealloc__(self):
if self.scores != NULL:
free(self.scores)
cpdef str get_info(self):
return f"name: {self.name}, age: {self.age}, gender: {self.gender}"
cpdef set_score(self, list scores):
# 虽然not None也可以写在参数后面,但是它只适用于Python函数
assert scores is not None and len(scores) == 3
cdef real score
cdef long idx
for idx, score in enumerate(scores):
self.scores[idx] = score
cpdef list get_score(self):
cpdef list res = [self.scores[0], self.scores[1], self.scores[2]]
return res
目前来讲,由于所有内容都在一个pyx文件里面,因此任何C级属性都可以自由访问。
>>> import cython_test
>>> cython_test.Girl('古明地觉', 16, 'female')
<cython_test.Girl object at 0x7f9716ee3308>
>>> g = cython_test.Girl('古明地觉', 16, 'female')
>>>
>>> g.get_info()
'name: 古明地觉, age: 16, gender: female'
>>> g.set_score([90.4, 97.3, 97.6])
>>> g.get_score()
[90.4, 97.3, 97.6]
>>>
访问非常地自由,没有任何限制,但是随着我们Girl这个类的功能越来越多的话,该怎么办呢?
所以我们需要创建一个pxd文件,就叫lover.pxd吧,然后把我们希望暴露给外界访问的C级结构放在里面。
# lover.pxd
ctypedef double real
cdef class Girl:
cdef public :
str name
long age
str gender
cdef real *scores
cpdef str get_info(self)
cpdef set_score(self, list scores)
cpdef list get_score(self)
我们看到在pxd文件中,我们只存放了C级结构的声明,像ctypedef、cdef、cpdef等等,并且函数的话我们只是存放了定义,函数体并没有写在里面,同理后面也不可以有冒号。另外,pxd文件是在编译时访问的,而且我们不可以在里面放类似于def这样的纯Python声明,否则会发生编译错误。
所以pxd文件只放相应的声明,而它们的具体实现是在pyx文件中,因此有人发现了,这个pxd文件不就是C中的头文件吗?答案确实如此。
然后我们对应的lover.pyx文件也需要修改,lover.pyx和lover.pxd具有相同的基名称,Cython会将它们视为一个命名空间。另外,如果我们在pxd文件中声明了一个函数,那么在pyx文件中不可以再次声明,否则会发生编译错误。怎么理解呢?
我们说类似于
cpdef func(): pass
这种形式,它是一个函数(有定义)
;但是cpdef func()
这种形式,它只是一个函数声明。所以函数声明和C中的函数声明也是类似的,在Cython中没有冒号、以及函数体的话,那么就是函数声明。而在Cython的pyx文件中也可以进行函数声明,比如C源文件中也是可以声明函数的,但是一般都会把声明写在h头文件中,在Cython中也是如此,会把C级结构、一些声明写在pxd文件中。而一旦声明,那么只能声明一次,不可以重复声明,具有相同基名称的pyx、pxd文件中,函数声明只能出现一次,如果在pxd文件中出现了,那么在pyx中就不可以重复声明了。并且对于函数,如果声明了,那么则需要有具体的实现。
注意:声明针对的是C一级的结构,所以对于def定义的Python函数是不可以进行声明的。
重新修改我们的pyx文件
# lover.pyx
from libc.stdlib cimport malloc, free
cdef class Girl:
def __cinit__(self, *args, **kwargs):
self.scores = <real *> malloc(3 * sizeof(real))
def __init__(self, name, age, gender):
self.name = name
self.age = age
self.gender = gender
def __dealloc__(self):
if self.scores != NULL:
free(self.scores)
cpdef str get_info(self):
return f"name: {self.name}, age: {self.age}, gender: {self.gender}"
cpdef set_score(self, list scores):
assert scores is not None and len(scores) == 3
cdef real score
cdef long idx
for idx, score in enumerate(scores):
self.scores[idx] = score
cpdef list get_score(self):
cpdef list res = [self.scores[0], self.scores[1], self.scores[2]]
return res
虽然结构没有什么变化,但是我们把一些C级数据拿到pxd文件中了,所以pyx文件中的可以直接删掉了,会自动到对应的pyd文件中找,因为它们有相同的基名称,Cython会将其整体看成一个命名空间。所以:这里pyx文件和pxd文件一定要有相同的基名称,只有这样才能够找得到,否则你会发现代码中real是没有被定义的,当然还有self的一些属性,因为它们必须要使用cdef在类里面进行声明。
但是哪些东西我们才应该写在pxd文件中呢?本质上讲,任何在C级别上,需要对其它模块公开的,我们才需要写在pxd文件中,比如:
C类型声明--ctypedef、结构体、共同体、枚举(后续系列中介绍)
外部的C、C++库的声明(后续系列中介绍)
cdef、cpdef模块级函数的声明
cdef class扩展类的声明
扩展类的cdef属性
使用cdef、cpdef方法的声明
C级内联函数或者方法的实现
但是,一个pxd文件不可以包含如下内容:
Python函数和非内联C级函数、方法的实现
Python类的定义
IF或者DEF宏的外部Python可执行代码
那么我们的pxd文件都带来了哪些功能呢?那就是lover.pyx文件可以被其它的pyx文件导入了,这几个pyx文件作为一个整体为Python提供更强大的功能,否则的话其它的pyx文件是无法导入的,导入方式是使用cimport。
cimport语句
然后我们在另一个pyx文件中导入这个lover.pyx,当然导入的话其实寻找的是pxd,然后调用的是pyx里面的函数。
# cython_test.pyx
from lover cimport Girl
cdef class NewGirl(Girl):
pass
然后进行编译,不过此时由于涉及到多个pyx文件,因此这些pyx都要进行编译才行。
from distutils.core import Extension, setup
from Cython.Build import cythonize
ext = [Extension("lover", ["lover.pyx"]), # 不用管pxd,会自动包含
Extension("cython_test", ["cython_test.pyx"])]
setup(ext_modules=cythonize(ext, language_level=3))
编译的命令和之前一样,编译之后会发现原来的目录中有两个pyd文件了。
将这两个文件拷贝出来,首先在cython_test.pyx中,是直接导入的lover,因此这两个pyd文件要在一个目录中。
>>> from cython_test import NewGirl
>>>
>>> g = NewGirl('古明地觉', 17, 'female')
>>> g.get_info()
'name: 古明地觉, age: 17, gender: female'
>>>
>>> g.set_score([90.1, 90.3, 93.5])
>>> g.get_score()
[90.1, 90.3, 93.5]
>>>
此时两个pyd文件就实现了导入,我们可以将这个cython_test.pyx写的更复杂一些。
from lover cimport Girl
cdef class NewGirl(Girl):
cdef public str where
def __init__(self, name, age, gender, where):
self.where = where
super().__init__(name, age, gender)
def new_get_info(self):
return super(NewGirl, self).get_info() + f", where: {self.where}"
>>> from cython_test import NewGirl
>>> # 自己定义了__init__,接收4个参数
>>> g = NewGirl('古明地觉', 17, 'female')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "cython_test.pyx", line 8, in cython_test.NewGirl.__init__
def __init__(self, name, age, gender, where):
TypeError: __init__() takes exactly 4 positional arguments (3 given)
>>>
>>> # 传递4个参数,前面3个会交给父类处理
>>> g = NewGirl('古明地觉', 17, 'female', '东方地灵殿')
>>> g.get_info() # 父类的方法
'name: 古明地觉, age: 17, gender: female'
>>>
>>> g.new_get_info() # 在父类的方法返回的结果之上,进行添加
'name: 古明地觉, age: 17, gender: female, where: 东方地灵殿'
>>>
因此我们看到使用起来基本上和Python之间是没有区别,主要就是如果涉及到多个pyx,那么这些pyx都要进行编译。并且想被导入,那么该pyx文件一定要有相同基名称的pxd文件。导入的时候使用cimport,会去pxd文件中找,然后具体实现则是去调用pyx文件。但即便pyx文件能被导入,但也未必就能使用其所有的C级结构,想使用的话必须要在对应的pxd文件中进行声明。
另外,可能有人发现了,我们这是绝对导入,对于目前演示来说使用绝对导入、相对导入是没有什么区别的。但实际上,pyd应该采用相对导入,因为它无法作为启动文件,只能被导入。所以我们在pyx文件中使用相对导入即可。
我们举个栗子,之前的lover.pyx和lover.pxd不变,在cython_test.pyx进行相对导入。
from .lover import Girl
但是:上面三个文件在一个单独的目录中,假设就叫lover吧。1.py就是我们执行编译的,它和lover目录是同级的。
然后编译扩展模块的时候可以用之前的方式编译,只不过Extension中文件路径要指定对。但是这里我们换一种方式吧,我们之前说支持通配符。
from distutils.core import setup
from Cython.Build import cythonize
# 将lover目录下的所有pyx文件进行编译,名字和pyx文件名是一致的。
# 如果是Extension的话,我们可以自己指定
setup(ext_modules=cythonize("lover/*.pyx", language_level=3))
当编译成功之后,对应的目录就会出现两个扩展模块,我们将其移动过来。
但是注意:由于涉及到相对导入,所以我们不可以在和cython_test.so同目录下导入,否则会报错。
>>> import cython_test
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "lover/cython_test.pyx", line 1, in init cython_test
from .lover import Girl
ImportError: attempted relative import with no known parent package
>>>
我们可以将这两个so文件移动到一个单独的目录中,就移动到lover中吧。
其实我们还可以在里面写入__init__
文件,这样才更符合Python中的包,但这里我们就不写了,只要可以相对导入即可。
>>> from lover.cython_test import Girl
>>>
>>> g = Girl("古明地觉", 17, "female")
>>> g.get_info()
'name: 古明地觉, age: 17, gender: female'
>>>
预定义的pxd文件
还记得我们之前的from libc.stdlib cimport malloc, free
这行代码吗?显然这是Cython提供的,没错它就在Cython模块主目录下的Includes目录中,libc这个包下面除了stdlib之外,还有stdio、math、string等pxd文件。除此之外,它还有一个libcpp包对应的pxd文件,里面包含了C++标准模板库(STL)
容器,如:string、vector、list、map、pair、set等等。
而CPython解释器的Include目录中定义了大量的C头文件,而Cython也提供了对应的pxd文件,可以方便地访问Python/C api。当然还有一个最重要的包就是numpy,Cython也是支持的,当然这些我们会在后面系列中介绍了。
from ... cimport...和from ... import ...的用法是一致的,区别就是前者多了一个c
下面我们就来演示一下常见的功能。
from libc cimport math
print(math.sin(math.pi / 2))
from libc.math cimport cos, pi
print(cos(pi / 2))
# 使用C++模板
from libcpp.vector cimport vector
cdef vector[int] *vi = new vector[int](10)
# 注意:如果使用import和cimport导入了相同的函数,那么会编译错误
"""
from math import sin
from lib.math cimport sin
这种方式是报错的
解决办法可以通过as
from math import sin as py_sin
from lib.math cimport sin as c_sin
"""
我们说pxd文件类似于C的头文件(h文件)
,它们有以下相似之处:
都通过使用外部代码声明C级结构
都允许我们将一个大文件分成不同的多个组件
都负责声明用于实现的C级接口
除此之外,还有一个pxi文件,但是个人觉得用处不是很大,这里就不说了。