扩展类型(又称 cdef 类)¶
注意
此页面使用两种不同的语法变体
Cython 特定的
cdef
语法,旨在使类型声明简洁明了,并易于从 C/C++ 的角度进行阅读。纯 Python 语法,允许在 纯 Python 代码 中进行静态 Cython 类型声明,遵循 PEP-484 类型提示和 PEP 526 变量注释。
要在 Python 语法中使用 C 数据类型,您需要在要编译的 Python 模块中导入特殊的
cython
模块,例如import cython
如果您使用纯 Python 语法,我们强烈建议您使用最新的 Cython 3 版本,因为与 0.29.x 版本相比,这里已经进行了重大改进。
为了支持面向对象的编程,Cython 支持像 Python 中一样编写普通的 Python 类
class MathFunction(object):
def __init__(self, name, operator):
self.name = name
self.operator = operator
def __call__(self, *operands):
return self.operator(*operands)
然而,基于 Python 所谓的“内置类型”,Cython 支持第二种类型的类:扩展类型,有时也称为“cdef 类”,因为它们使用 Cython 语言关键字进行声明。与 Python 类相比,它们有一些限制,但通常比通用 Python 类更节省内存,速度更快。主要区别在于它们使用 C 结构来存储其字段和方法,而不是 Python 字典。这使它们能够在其字段中存储任意 C 类型,而无需为它们提供 Python 包装器,并能够在 C 级直接访问字段和方法,而无需通过 Python 字典查找。
普通的 Python 类可以从 cdef 类继承,但反之则不行。Cython 需要了解完整的继承层次结构才能布置其 C 结构,并将其限制为单继承。另一方面,普通的 Python 类可以从 Cython 代码和纯 Python 代码中的任意数量的 Python 类和扩展类型继承。
@cython.cclass
class Function:
@cython.ccall
def evaluate(self, x: float) -> float:
return 0
cdef class Function:
cpdef double evaluate(self, double x) except *:
return 0
cpdef
命令(或 Python 语法中的 @cython.ccall
)使该方法的两个版本可用;一个用于从 Cython 使用的快速版本,另一个用于从 Python 使用的慢速版本。
现在,我们可以添加 Function
类的子类,它们在同一个 evaluate()
方法中实现不同的数学函数。
然后
from cython.cimports.libc.math import sin
@cython.cclass
class Function:
@cython.ccall
def evaluate(self, x: float) -> float:
return 0
@cython.cclass
class SinOfSquareFunction(Function):
@cython.ccall
def evaluate(self, x: float) -> float:
return sin(x ** 2)
from libc.math cimport sin
cdef class Function:
cpdef double evaluate(self, double x) except *:
return 0
cdef class SinOfSquareFunction(Function):
cpdef double evaluate(self, double x) except *:
return sin(x ** 2)
这比为 cdef 方法提供 Python 包装器略多:与 cdef 方法不同,cpdef 方法可以完全被 Python 子类中的方法和实例属性覆盖。与 cdef 方法相比,这会增加一些调用开销。
为了使类定义对其他模块可见,从而允许在实现它们的模块之外进行有效的 C 级使用和继承,我们在一个 .pxd
文件中定义它们,该文件与模块同名。请注意,我们在这里使用的是 Cython 语法,而不是 Python 语法。
cdef class Function:
cpdef double evaluate(self, double x) except *
cdef class SinOfSquareFunction(Function):
cpdef double evaluate(self, double x) except *
通过这种实现不同函数作为具有快速、Cython 可调用方法的子类的方式,我们现在可以将这些 Function
对象传递给用于数值积分的算法,该算法在值区间内评估任意用户提供的函数。
使用它,我们现在可以更改我们的积分示例
from cython.cimports.sin_of_square import Function, SinOfSquareFunction
def integrate(f: Function, a: float, b: float, N: cython.int):
i: cython.int
if f is None:
raise ValueError("f cannot be None")
s: float = 0
dx: float = (b - a) / N
for i in range(N):
s += f.evaluate(a + i * dx)
return s * dx
print(integrate(SinOfSquareFunction(), 0, 1, 10000))
from sin_of_square cimport Function, SinOfSquareFunction
def integrate(Function f, double a, double b, int N):
cdef int i
cdef double s, dx
if f is None:
raise ValueError("f cannot be None")
s = 0
dx = (b - a) / N
for i in range(N):
s += f.evaluate(a + i * dx)
return s * dx
print(integrate(SinOfSquareFunction(), 0, 1, 10000))
我们甚至可以传入一个在 Python 空间中定义的新 Function
,它会覆盖基类中 Cython 实现的方法。
>>> import integrate
>>> class MyPolynomial(integrate.Function):
... def evaluate(self, x):
... return 2*x*x + 3*x - 10
...
>>> integrate(MyPolynomial(), 0, 1, 10000)
-7.8335833300000077
由于 evaluate()
这里是一个 Python 方法,它需要 Python 对象作为输入和输出,因此它比直接调用 Cython 方法的 C 调用慢了几倍,但仍然比纯 Python 变体快。这表明当整个计算循环从 Python 代码移到 Cython 模块时,速度提升可以有多大。
关于我们对 evaluate
的新实现的一些说明。
这里的快速方法分派之所以有效,是因为
evaluate
在Function
中声明。如果evaluate
在SinOfSquareFunction
中引入,代码仍然可以工作,但 Cython 会使用更慢的 Python 方法分派机制。同样,如果参数
f
没有类型化,而只是作为 Python 对象传递,则会使用更慢的 Python 分派。由于参数是类型化的,我们需要检查它是否为
None
。在 Python 中,当查找evaluate
方法时,这会导致AttributeError
,但 Cython 会尝试访问None
的(不兼容的)内部结构,就好像它是一个Function
一样,从而导致崩溃或数据损坏。
有一个名为 nonecheck
的编译器指令,它会打开对这种情况的检查,但会降低速度。以下是编译器指令如何用于动态打开或关闭 nonecheck
的方法。
# cython: nonecheck=True
# ^^^ Turns on nonecheck globally
import cython
@cython.cclass
class MyClass:
pass
# Turn off nonecheck locally for the function
@cython.nonecheck(False)
def func():
obj: MyClass = None
try:
# Turn nonecheck on again for a block
with cython.nonecheck(True):
print(obj.myfunc()) # Raises exception
except AttributeError:
pass
print(obj.myfunc()) # Hope for a crash!
# cython: nonecheck=True
# ^^^ Turns on nonecheck globally
import cython
cdef class MyClass:
pass
# Turn off nonecheck locally for the function
@cython.nonecheck(False)
def func():
cdef MyClass obj = None
try:
# Turn nonecheck on again for a block
with cython.nonecheck(True):
print(obj.myfunc()) # Raises exception
except AttributeError:
pass
print(obj.myfunc()) # Hope for a crash!
cdef 类中的属性与普通类中的属性行为不同。
所有属性都必须在编译时预先声明。
默认情况下,属性只能从 Cython(类型化访问)访问。
可以声明属性以将动态属性公开到 Python 空间。
from cython.cimports.sin_of_square import Function
@cython.cclass
class WaveFunction(Function):
# Not available in Python-space:
offset: float
# Available in Python-space:
freq = cython.declare(cython.double, visibility='public')
# Available in Python-space, but only for reading:
scale = cython.declare(cython.double, visibility='readonly')
# Available in Python-space:
@property
def period(self):
return 1.0 / self.freq
@period.setter
def period(self, value):
self.freq = 1.0 / value
from sin_of_square cimport Function
cdef class WaveFunction(Function):
# Not available in Python-space:
cdef double offset
# Available in Python-space:
cdef public double freq
# Available in Python-space, but only for reading:
cdef readonly double scale
# Available in Python-space:
@property
def period(self):
return 1.0 / self.freq
@period.setter
def period(self, value):
self.freq = 1.0 / value