Cython 中的特殊函数

这次来介绍一下 Cython 中的特殊函数定义,Cython 相比 Python 本身的特殊函数之外还增加了一些新的函数,用来满足对 C 特性的支持,其中有些内容还经常令人混淆。关于 Python 中特殊变量和特殊函数名的内容,请参考 Python 官方文档

def, cdefcpdef

首先最开始需要分清的便是 Cython 中的三种函数类型。def 定义的对象(包括变量、函数、类型)都是普通的 Python 对象,是 Python 可以直接调用的,因此其参数都只能是 Python 类型或对象;cdef 定义的对象则是 C/C++ 层面的,可以直接用 C/C++ 对象作为参数,因此不能被普通 Python 代码调用,这样减少了很多 overhead 因此可以提高运行效率。另外尽管 cdef 的函数不是 Python 对象,无法当作变量使用,但还是可以获取函数指针的。而 cpdef 则是同时兼具两方面特性,其本质是用 cdef 定义函数后再用 def 定义一个函数封装,使得在 Cython 中调用时可以调用高效的 cdef 版本,而在 Python 中调用的是与 Python 兼容的 def 版本。

__init____cinit__

在理清了上面几个关键字后另一个经常令人疑惑的点便是__init____cinit__的区别。__cinit____dealloc__都是 Cython 特有的特殊函数。官方文档在其用法上解释的并不清楚,只是说__cinit__可以用来进行 C/C++ 级别的初始化。实际上,使用__cinit__的重要原因是源于其特性:__cinit__会像 C++ 一样自动执行基类的__cinit__,因此它保证会在构造时被执行一次(且只被执行一次)。由于 Python 中的__init__函数默认不会调用基类的__init__,因此如果想保证类型中的 cdef 成员被初始化,避免可能的堆栈问题(如指针没有初始化),那么就可以选择使用__cinit__。如果理解了这一点就可以知道,什么时候需要使用__cinit__了。

但是使用__cinit__的时候有很多限制需要了解:

  1. __cinit__有时会带来额外的开销,这篇博客中有一些分析
  2. __cinit__的参数声明和__init__必须一致,因为会同时被调用。因此通常__cinit__的参数中会留下 *kargs**kvargsStackoverflow 上也有人问过这个情况
  3. __cinit__中如果要用 malloc 分配内存,记得在__dealloc__中销毁。__dealloc__相当于 C++ 版本的__del__
  4. __cinit____init__一样也只能使用 def 声明,不能用__cdef____cpdef__。具体原因我并不清楚。

运算符重载

其他大多数的特殊函数定义和用法几乎和 Python 相同,但是需要特别指出的是运算符重载的部分。以加法为例,在 Python 中加法 a + b 的实现方式是:

  1. 如果 a 中定义了__add__,那么调用 a.__add__(b)
  2. 如果 a 中没有定义,而 b 中定义了__radd__,那么调用 b.__radd(a)

而在 Python 的 C 扩展类里(包含 Cython 和 pybind11 的实现),其实现方式是寻找接受 ab 类型的__add__重载,也就是说本质上在 C 扩展类中定义的__add__都是__add__的重载,这也是与 C++ 的 operator 重载理念一致,只不过这个__add__仍然需要定义在类里。在 Cython 文档中给出的运算符列表里,参数里带 self 的函数都是按照 Python 中的方法实现的,self 不能指定类型;而以 x, y 这种形式为参数的则是按照 C 扩展类执行方式的函数,xy 都可以指定类型。

另外 Cython 还定义了一个特殊的运算符函数__richcmp__,这个是 Python 中没有的,不过其功能只是把比较符号(>,<,=)的实现合并了,与 Python 的__eq____lt__等函数没有本质区别。这在官方文档中也有说明

__getbuffer__

Cython 中有两个版本的 Buffer 协议,一种是提案 PEP-3118 定义的,另一种是 Python 官方定义之前 Cython 自己的定义方式。其中前者在之前介绍 Cython 封装的文章中已有介绍,就不多赘述。其相关的特殊函数是__getbuffer____releasebuffer__,这两个函数也都是 Cython 特有的。而后者比较难用,已经被标记为 depricated 废弃了,也不介绍了。

属性(property)

Cython 中还提供了一套非常方便的属性定义方法。原本在 Python 中定义属性非常但疼,例如下面的代码定义了名为 length 的属性,使得你可以通过 square.length 的方法访问它

1
2
3
4
5
6
7
8
9
10
class Square:
@property
def length(self):
return self._length
@length.setter
def length(self, value):
self._length = value
@length.deleter
def length(self):
self._length = 0

而在 Cython 中定义属性就更简单了,它除了支持上面的方法外还有另一种更加直观的定义方式(虽然这个方式也已经被标记为 depricated 了):

1
2
3
4
5
6
7
8
cdef class Square:
property length:
def __get__(self):
return self._length
def __set__(self, value):
self._length = value
def __del__(self):
self._length = 0

Cython 的类型还有各种其他的奇奇怪怪的小特性,在 Cython 的这两篇文档里有详细介绍:Extension Types, Special Methods of Extension Types,仅供参考~