扩展类型(又称 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

cpdef 命令(或 Python 语法中的 @cython.ccall)使该方法的两个版本可用;一个用于从 Cython 使用的快速版本,另一个用于从 Python 使用的慢速版本。

现在,我们可以添加 Function 类的子类,它们在同一个 evaluate() 方法中实现不同的数学函数。

然后

sin_of_square.py
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)

这比为 cdef 方法提供 Python 包装器略多:与 cdef 方法不同,cpdef 方法可以完全被 Python 子类中的方法和实例属性覆盖。与 cdef 方法相比,这会增加一些调用开销。

为了使类定义对其他模块可见,从而允许在实现它们的模块之外进行有效的 C 级使用和继承,我们在一个 .pxd 文件中定义它们,该文件与模块同名。请注意,我们在这里使用的是 Cython 语法,而不是 Python 语法。

sin_of_square.pxd
cdef class Function:
    cpdef double evaluate(self, double x) except *

cdef class SinOfSquareFunction(Function):
    cpdef double evaluate(self, double x) except *

通过这种实现不同函数作为具有快速、Cython 可调用方法的子类的方式,我们现在可以将这些 Function 对象传递给用于数值积分的算法,该算法在值区间内评估任意用户提供的函数。

使用它,我们现在可以更改我们的积分示例

integrate.py
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))

我们甚至可以传入一个在 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 的新实现的一些说明。

  • 这里的快速方法分派之所以有效,是因为 evaluateFunction 中声明。如果 evaluateSinOfSquareFunction 中引入,代码仍然可以工作,但 Cython 会使用更慢的 Python 方法分派机制。

  • 同样,如果参数 f 没有类型化,而只是作为 Python 对象传递,则会使用更慢的 Python 分派。

  • 由于参数是类型化的,我们需要检查它是否为 None。在 Python 中,当查找 evaluate 方法时,这会导致 AttributeError,但 Cython 会尝试访问 None 的(不兼容的)内部结构,就好像它是一个 Function 一样,从而导致崩溃或数据损坏。

有一个名为 nonecheck编译器指令,它会打开对这种情况的检查,但会降低速度。以下是编译器指令如何用于动态打开或关闭 nonecheck 的方法。

nonecheck.py
# 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!

cdef 类中的属性与普通类中的属性行为不同。

  • 所有属性都必须在编译时预先声明。

  • 默认情况下,属性只能从 Cython(类型化访问)访问。

  • 可以声明属性以将动态属性公开到 Python 空间。

wave_function.py
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