扩展类型

简介

注意

此页面使用两种不同的语法变体

  • Cython 特定的 cdef 语法,旨在使类型声明简洁且易于从 C/C++ 的角度阅读。

  • 纯 Python 语法,允许在 纯 Python 代码 中进行静态 Cython 类型声明,遵循 PEP-484 类型提示和 PEP 526 变量注释。

    要使用 Python 语法中的 C 数据类型,您需要在要编译的 Python 模块中导入特殊的 cython 模块,例如

    import cython
    

    如果您使用纯 Python 语法,我们强烈建议您使用最新的 Cython 3 版本,因为与 0.29.x 版本相比,这里已经进行了重大改进。

除了使用 Python 类语句创建普通的用户定义类之外,Cython 还允许您创建新的内置 Python 类型,称为 扩展类型。您可以使用 cdef 类语句或用 @cclass 装饰器装饰类来定义扩展类型。以下是一个示例

@cython.cclass
class Shrubbery:
    width: cython.int
    height: cython.int

    def __init__(self, w, h):
        self.width = w
        self.height = h

    def describe(self):
        print("This shrubbery is", self.width,
              "by", self.height, "cubits.")

如您所见,Cython 扩展类型定义看起来很像 Python 类定义。在其中,您可以使用 def 语句来定义可以从 Python 代码中调用的方法。您甚至可以像在 Python 中一样定义许多特殊方法,例如 __init__()

主要区别在于您可以使用

  • cdef 语句,

  • cython.declare() 函数或

  • 属性名称的注释。

@cython.cclass
class Shrubbery:
    width = cython.declare(cython.int)
    height: cython.int

属性可以是 Python 对象(通用或特定扩展类型),也可以是任何 C 数据类型。因此,您可以使用扩展类型来包装任意 C 数据结构并为它们提供类似 Python 的接口。

静态属性

扩展类型的属性直接存储在对象的 C 结构中。属性集在编译时固定;您不能在运行时简单地通过为它们赋值来向扩展类型实例添加属性,就像您可以对 Python 类实例那样。但是,您可以明确启用对动态分配属性的支持,或者使用普通的 Python 类对扩展类型进行子类化,然后支持任意属性分配。请参阅 动态属性

扩展类型的属性可以通过两种方式访问:通过 Python 属性查找或通过从 Cython 代码直接访问 C 结构。Python 代码只能通过第一种方法访问扩展类型的属性,但 Cython 代码可以使用任一方法。

默认情况下,扩展类型属性只能通过直接访问,而不能通过 Python 访问,这意味着它们无法从 Python 代码中访问。要使它们从 Python 代码中访问,您需要将它们声明为 publicreadonly。例如

import cython

@cython.cclass
class Shrubbery:
    width = cython.declare(cython.int, visibility='public')
    height = cython.declare(cython.int, visibility='public')
    depth = cython.declare(cython.float, visibility='readonly')

使 width 和 height 属性可从 Python 代码读写,而 depth 属性可读但不可写。

注意

您只能公开简单的 C 类型,例如整数、浮点数和字符串,以供 Python 访问。您也可以公开 Python 值属性。

注意

此外,publicreadonly 选项仅适用于 Python 访问,不适用于直接访问。扩展类型的所有属性始终可以通过 C 级访问读写。

动态属性

默认情况下,无法在运行时向扩展类型添加属性。您可以通过两种方法来避免此限制,这两种方法在从 Python 代码调用方法时都会增加开销。尤其是在调用在 .pyx 文件中使用 cpdef 声明的混合方法或使用 @ccall 装饰器时。

第一种方法是创建一个 Python 子类

@cython.cclass
class Animal:

    number_of_legs: cython.int

    def __cinit__(self, number_of_legs: cython.int):
        self.number_of_legs = number_of_legs


class ExtendableAnimal(Animal):  # Note that we use class, not cdef class
    pass


dog = ExtendableAnimal(4)
dog.has_tail = True

声明 __dict__ 属性是启用动态属性的第二种方法

@cython.cclass
class Animal:

    number_of_legs: cython.int
    __dict__: dict

    def __cinit__(self, number_of_legs: cython.int):
        self.number_of_legs = number_of_legs


dog = Animal(4)
dog.has_tail = True

类型声明

在您可以直接访问扩展类型的属性之前,Cython 编译器必须知道您拥有该类型的实例,而不仅仅是一个通用的 Python 对象。它已经在该类型方法的 self 参数的情况下知道这一点,但在其他情况下,您需要使用类型声明。

例如,在以下函数中

@cython.cfunc
def widen_shrubbery(sh, extra_width): # BAD
    sh.width = sh.width + extra_width

由于 sh 参数没有指定类型,因此将通过 Python 属性查找访问 width 属性。如果该属性已声明为 publicreadonly,那么这将起作用,但效率非常低下。如果该属性是私有的,它将根本不起作用 - 代码将编译,但在运行时将引发属性错误。

解决方案是将 sh 声明为 Shrubbery 类型,如下所示

import cython
from cython.cimports.my_module import Shrubbery

@cython.cfunc
def widen_shrubbery(sh: Shrubbery, extra_width):
    sh.width = sh.width + extra_width

现在 Cython 编译器知道 sh 具有名为 width 的 C 属性,并将生成代码以直接有效地访问它。相同的考虑适用于局部变量,例如

import cython
from cython.cimports.my_module import Shrubbery

@cython.cfunc
def another_shrubbery(sh1: Shrubbery) -> Shrubbery:
    sh2: Shrubbery
    sh2 = Shrubbery()
    sh2.width = sh1.width
    sh2.height = sh1.height
    return sh2

注意

在这里,我们cimportShrubbery(使用 cimport 语句或从特殊的 cython.cimports 包中导入),这是在编译时声明类型所必需的。为了能够 cimport 扩展类型,我们将类定义分成两部分,一部分在定义文件中,另一部分在相应的实现文件中。您应该阅读 共享扩展类型 以了解如何做到这一点。

类型测试和强制转换

假设我有一个方法 quest(),它返回 Shrubbery 类型的对象。要访问它的宽度,我可以写

sh: Shrubbery = quest()
print(sh.width)

这需要使用局部变量并在赋值时执行类型测试。如果您知道 quest() 的返回值将是 Shrubbery 类型,您可以使用强制转换来写

print( cython.cast(Shrubbery, quest()).width )

如果 quest() 实际上不是一个 Shrubbery,这可能会很危险,因为它会尝试访问宽度作为 C 结构体成员,而该成员可能不存在。在 C 级别,而不是引发一个 AttributeError,要么返回一个无意义的结果(将该地址处的任何数据解释为一个整数),要么尝试访问无效内存会导致段错误。相反,可以写

print( cython.cast(Shrubbery, quest(), typecheck=True).width )

这会在进行强制转换之前执行类型检查(可能会引发一个 TypeError),并允许代码继续执行。

要显式测试对象的类型,请使用 isinstance() 内置函数。对于已知的内置或扩展类型,Cython 将这些转换为快速且安全的类型检查,忽略对对象 __class__ 属性等的更改,因此在成功执行 isinstance() 测试后,代码可以依赖于扩展类型的预期 C 结构及其 C 级别属性(存储在对象的 C 结构体中)以及 cdef/@cfunc 方法。

扩展类型和 None

Cython 在 C 类类型声明中和使用 Python 注释时以不同的方式处理 None 值。

cdef 声明和 C 类函数参数声明(func(list x))中,当您将参数或 C 变量声明为具有扩展类型或 Python 内置类型时,Cython 将允许它采用 None 值以及其声明类型的值。这类似于 C 指针可以采用 NULL 值的方式,您需要为此付出同样的谨慎。只要您对它执行 Python 操作,就没有问题,因为将应用完整的动态类型检查。但是,当您访问扩展类型的 C 属性(如上面的 widen_shrubbery 函数中)时,您需要确保您使用的引用不是 None - 为了效率起见,Cython 不会检查这一点。

使用 C 类声明语法,当公开接受扩展类型作为参数的 Python 函数时,您需要特别小心

def widen_shrubbery(Shrubbery sh, extra_width): # This is
    sh.width = sh.width + extra_width           # dangerous!

我们的模块的用户可以通过为 sh 参数传递 None 来使它崩溃。

与 Python 一样,无论何时不清楚变量是否可以为 None,但代码需要非 None 值,显式检查可以提供帮助

def widen_shrubbery(Shrubbery sh, extra_width):
    if sh is None:
        raise TypeError
    sh.width = sh.width + extra_width

但由于预计这将是一个如此频繁的要求,Cython 语言提供了一种更方便的方法。声明为扩展类型的 Python 函数的参数可以具有 not None 子句

def widen_shrubbery(Shrubbery sh not None, extra_width):
    sh.width = sh.width + extra_width

现在,该函数将自动检查 sh 是否为 not None,以及检查它是否具有正确的类型。

当使用注释时,行为遵循 PEP-484 的 Python 类型语义。当变量仅用其纯类型进行注释时,不允许使用 None

def widen_shrubbery(sh: Shrubbery, extra_width):  # TypeError is raised
    sh.width = sh.width + extra_width             # when sh is None

为了允许使用 None,必须显式使用 typing.Optional[ ]。对于函数参数,当它们具有默认参数 None` 时,也会自动允许使用 None,例如 func(x: list = None) 不需要 typing.Optional

import typing
def widen_shrubbery(sh: typing.Optional[Shrubbery], extra_width):
    if sh is None:
        # We want to raise a custom exception in case of a None value.
        raise ValueError
    sh.width = sh.width + extra_width

在这里使用注解的好处是,它们默认情况下是安全的,因为您需要显式地允许它们的值为 None

注意

not Nonetyping.Optional 只能在 Python 函数(使用 def 定义,不使用 @cython.cfunc 装饰器)中使用,不能在 C 函数(使用 cdef 定义或使用 @cython.cfunc 装饰)中使用。如果您需要检查 C 函数的参数是否为 None,则需要自己进行检查。

注意

更多内容

  • 扩展类型方法的 self 参数保证永远不会是 None

  • 当将一个值与 None 进行比较时,请记住,如果 x 是一个 Python 对象,x is Nonex is not None 非常高效,因为它们直接转换为 C 指针比较,而 x == Nonex != None,或者仅仅将 x 作为布尔值使用(如 if x: ...)将调用 Python 操作,因此速度会慢得多。

特殊方法

虽然原理相似,但扩展类型的许多 __xxx__() 特殊方法与其 Python 对应方法之间存在很大差异。有一个 单独的页面专门介绍了这个主题,您应该在尝试在扩展类型中使用任何特殊方法之前仔细阅读它。

属性

您可以使用与普通 Python 代码相同的语法在扩展类中声明属性。

@cython.cclass
class Spam:
    @property
    def cheese(self):
        # This is called when the property is read.
        ...

    @cheese.setter
    def cheese(self, value):
        # This is called when the property is written.
        ...

    @cheese.deleter
    def cheese(self):
        # This is called when the property is deleted.

还有一种特殊的(已弃用)旧语法用于在扩展类中定义属性。

cdef class Spam:

    property cheese:

        "A doc string can go here."

        def __get__(self):
            # This is called when the property is read.
            ...

        def __set__(self, value):
            # This is called when the property is written.
            ...

        def __del__(self):
            # This is called when the property is deleted.

__get__()__set__()__del__() 方法都是可选的;如果省略它们,在尝试执行相应的操作时将引发异常。

这是一个完整的示例。它定义了一个属性,每次写入该属性时都会向列表中添加一个元素,读取该属性时会返回该列表,删除该属性时会清空该列表。

import cython

@cython.cclass
class CheeseShop:

    cheeses: object

    def __cinit__(self):
        self.cheeses = []

    @property
    def cheese(self):
        return f"We don't have: {self.cheeses}"

    @cheese.setter
    def cheese(self, value):
        self.cheeses.append(value)

    @cheese.deleter
    def cheese(self):
        del self.cheeses[:]

# Test input
from cheesy import CheeseShop

shop = CheeseShop()
print(shop.cheese)

shop.cheese = "camembert"
print(shop.cheese)

shop.cheese = "cheddar"
print(shop.cheese)

del shop.cheese
print(shop.cheese)
# Test output
We don't have: []
We don't have: ['camembert']
We don't have: ['camembert', 'cheddar']
We don't have: []

C 方法

扩展类型可以拥有 C 方法以及 Python 方法。与 C 函数类似,C 方法使用

  • cdef 而不是 def@cfunc 装饰器来声明C 方法,或者

  • cpdef 代替 def@ccall 装饰器用于混合方法

C 方法是“虚拟”的,可以在派生扩展类型中被覆盖。此外,cpdef/@ccall 方法甚至可以被 Python 方法覆盖,当它们被调用为 C 方法时。这与 cdef/@cfunc 方法相比,增加了少许调用开销。

import cython

@cython.cclass
class Parrot:

    @cython.cfunc
    def describe(self) -> cython.void:
        print("This parrot is resting.")

@cython.cclass
class Norwegian(Parrot):

    @cython.cfunc
    def describe(self) -> cython.void:
        Parrot.describe(self)
        print("Lovely plumage!")

cython.declare(p1=Parrot, p2=Parrot)
p1 = Parrot()
p2 = Norwegian()
print("p2:")
p2.describe()
# Output
p1:
This parrot is resting.
p2:
This parrot is resting.
Lovely plumage!

上面的例子也说明了,C 方法可以使用通常的 Python 技术调用继承的 C 方法,即

Parrot.describe(self)

cdef/@ccall 方法可以使用 @staticmethod 装饰器声明为静态方法。这对于构建接受非 Python 兼容类型的类特别有用。

import cython
from cython.cimports.libc.stdlib import free

@cython.cclass
class OwnedPointer:
    ptr: cython.pointer(cython.void)

    def __dealloc__(self):
        if self.ptr is not cython.NULL:
            free(self.ptr)

    @staticmethod
    @cython.cfunc
    def create(ptr: cython.pointer(cython.void)):
        p = OwnedPointer()
        p.ptr = ptr
        return p

注意

Cython 目前不支持使用 @classmethod 装饰器装饰 cdef/@ccall 方法。

子类化

如果扩展类型继承自其他类型,则第一个基类必须是内置类型或其他扩展类型。

@cython.cclass
class Parrot:
    ...

@cython.cclass
class Norwegian(Parrot):
    ...

Cython 必须能够访问基类型的完整定义,因此如果基类型是内置类型,则必须事先将其声明为外部扩展类型。如果基类型在另一个 Cython 模块中定义,则必须将其声明为外部扩展类型,或者使用 cimport 语句导入,或者从特殊的 cython.cimports 包中导入。

支持多重继承,但是第二个及后续的基类必须是普通的 Python 类(而不是扩展类型或内置类型)。

Cython 扩展类型也可以在 Python 中被子类化。Python 类可以继承自多个扩展类型,前提是遵循通常的 Python 多重继承规则(即所有基类的 C 布局必须兼容)。

有一种方法可以阻止扩展类型在 Python 中被子类化。这可以通过 final 指令来实现,通常使用装饰器设置在扩展类型或 C 方法上。

import cython

@cython.final
@cython.cclass
class Parrot:
   def describe(self): pass

@cython.cclass
class Lizard:

   @cython.final
   @cython.cfunc
   def done(self): pass

尝试从最终类型创建 Python 子类或覆盖最终方法将在运行时引发 TypeError。Cython 也会阻止在同一个模块内对最终类型进行子类化或覆盖最终方法,即创建使用最终类型作为基类型的扩展类型将在编译时失败。但是,请注意,此限制目前不会传播到其他扩展模块,因此 Cython 无法阻止最终扩展类型在 C 级被外部代码子类化。

前向声明扩展类型

扩展类型可以像 structunion 类型一样进行前向声明。这通常是不必要的,并且违反了 DRY 原则(不要重复自己)。

如果您要前向声明一个具有基类的扩展类型,则必须在前向声明及其后续定义中都指定基类,例如,

cdef class A(B)

...

cdef class A(B):
    # attributes and methods

快速实例化

Cython 提供两种方法来加速扩展类型的实例化。第一个是直接调用 __new__() 特殊静态方法,正如 Python 中所知。对于扩展类型 Penguin,您可以使用以下代码

import cython

@cython.cclass
class Penguin:
    food: object

    def __cinit__(self, food):
        self.food = food

    def __init__(self, food):
        print("eating!")

normal_penguin = Penguin('fish')
fast_penguin = Penguin.__new__(Penguin, 'wheat')  # note: not calling __init__() !

请注意,通过 __new__() 的路径 *不会* 调用类型的 __init__() 方法(同样,如 Python 中所知)。因此,在上面的示例中,第一次实例化将打印 eating!,但第二次不会。这只是 __cinit__() 方法比普通 __init__() 方法更安全的原因之一,用于初始化扩展类型并将它们带入正确和安全的状态。有关差异,请参阅 初始化方法部分

第二个性能改进适用于经常被连续创建和删除的类型,因此它们可以从空闲列表中受益。Cython 为此提供了装饰器 @cython.freelist(N),它为给定类型创建了一个大小为 N 的静态空闲列表。示例

import cython

@cython.freelist(8)
@cython.cclass
class Penguin:
    food: object
    def __cinit__(self, food):
        self.food = food

penguin = Penguin('fish 1')
penguin = None
penguin = Penguin('fish 2')  # does not need to allocate memory!

从现有 C/C++ 指针进行实例化

从现有(指向)数据结构(通常由外部 C/C++ 函数返回)实例化扩展类非常常见。

由于扩展类只能在其构造函数中接受 Python 对象作为参数,因此需要使用工厂函数或工厂方法。例如

import cython
from cython.cimports.libc.stdlib import malloc, free

# Example C struct
my_c_struct = cython.struct(
    a = cython.int,
    b = cython.int,
)

@cython.cclass
class WrapperClass:
    """A wrapper class for a C/C++ data structure"""
    _ptr: cython.pointer(my_c_struct)
    ptr_owner: cython.bint

    def __cinit__(self):
        self.ptr_owner = False

    def __dealloc__(self):
        # De-allocate if not null and flag is set
        if self._ptr is not cython.NULL and self.ptr_owner is True:
            free(self._ptr)
            self._ptr = cython.NULL

    def __init__(self):
        # Prevent accidental instantiation from normal Python code
        # since we cannot pass a struct pointer into a Python constructor.
        raise TypeError("This class cannot be instantiated directly.")

    # Extension class properties
    @property
    def a(self):
        return self._ptr.a if self._ptr is not cython.NULL else None

    @property
    def b(self):
        return self._ptr.b if self._ptr is not cython.NULL else None

    @staticmethod
    @cython.cfunc
    def from_ptr(_ptr: cython.pointer(my_c_struct), owner: cython.bint=False) -> WrapperClass:
        """Factory function to create WrapperClass objects from
        given my_c_struct pointer.

        Setting ``owner`` flag to ``True`` causes
        the extension type to ``free`` the structure pointed to by ``_ptr``
        when the wrapper object is deallocated."""
        # Fast call to __new__() that bypasses the __init__() constructor.
        wrapper: WrapperClass  = WrapperClass.__new__(WrapperClass)
        wrapper._ptr = _ptr
        wrapper.ptr_owner = owner
        return wrapper

    @staticmethod
    @cython.cfunc
    def new_struct() -> WrapperClass:
        """Factory function to create WrapperClass objects with
        newly allocated my_c_struct"""
        _ptr: cython.pointer(my_c_struct) = cython.cast(
                cython.pointer(my_c_struct), malloc(cython.sizeof(my_c_struct)))
        if _ptr is cython.NULL:
            raise MemoryError
        _ptr.a = 0
        _ptr.b = 0
        return WrapperClass.from_ptr(_ptr, owner=True)

然后,要从现有的 my_c_struct 指针创建 WrapperClass 对象,可以在 Cython 代码中使用 WrapperClass.from_ptr(ptr)。要同时分配一个新结构并将其包装,可以使用 WrapperClass.new_struct

如果需要,可以从同一个指针创建多个 Python 对象,这些对象都指向同一个内存中的数据,但必须注意在取消分配时,如上所示。此外,ptr_owner 标志可用于控制哪个 WrapperClass 对象拥有指针并负责取消分配 - 在示例中,默认情况下将其设置为 False,可以通过调用 from_ptr(ptr, owner=True) 来启用。

GIL *不能* 在 __dealloc__ 中释放,或者如果使用另一个锁,则在这种情况下或出现多个取消分配的竞争条件时,可能会发生竞争条件。

作为对象构造函数的一部分,__cinit__ 方法具有 Python 签名,这使得它无法接受 my_c_struct 指针作为参数。

尝试在 Python 签名中使用指针会导致类似以下错误

Cannot convert 'my_c_struct *' to Python object

这是因为 Cython 无法自动将指针转换为 Python 对象,与 int 等本机类型不同。

请注意,对于本机类型,Cython 将复制值并创建一个新的 Python 对象,而在上面的情况下,数据不会被复制,内存取消分配是扩展类的责任。

使扩展类型可弱引用

默认情况下,扩展类型不支持对其进行弱引用。可以通过声明一个名为 __weakref__ 的类型为对象的 C 属性来启用弱引用。例如

@cython.cclass
class ExplodingAnimal:
    """This animal will self-destruct when it is
    no longer strongly referenced."""

    __weakref__: object

在 CPython 中控制取消分配和垃圾回收

注意

本节仅适用于 Python 的常用 CPython 实现。其他实现(如 PyPy)的工作方式不同。

介绍

首先,了解在 CPython 中触发 Python 对象取消分配有两种方法:CPython 对所有对象使用引用计数,任何引用计数为零的对象都会立即取消分配。这是取消分配对象的最常见方法。例如,考虑

>>> x = "foo"
>>> x = "bar"

执行第二行后,字符串 "foo" 不再被引用,因此它被取消分配。这是使用 PyTypeObject.tp_dealloc 插槽完成的,该插槽可以在 Cython 中通过实现 __dealloc__ 来定制。

第二种机制是循环垃圾收集器。它的目的是解决循环引用问题,例如:

>>> class Object:
...     pass
>>> def make_cycle():
...     x = Object()
...     y = [x]
...     x.attr = y

当调用 make_cycle 时,会创建一个循环引用,因为 x 引用 y,反之亦然。即使在 make_cycle 返回后,xy 都不可访问,但它们的引用计数都为 1,因此不会立即被释放。在定期时间,垃圾收集器会运行,它会注意到循环引用(使用 PyTypeObject.tp_traverse 槽)并将其打破。打破循环引用意味着从循环中的一个对象中删除所有对其他 Python 对象的引用(我们称之为清除一个对象)。清除几乎与释放相同,只是实际的对象尚未被释放。对于上面的示例中的 xx 的属性将从 x 中删除。

请注意,只需清除循环中的一个对象就足够了,因为清除一个对象后就不再有循环了。一旦循环被打破,通常的基于引用计数的释放将实际从内存中删除这些对象。清除在 PyTypeObject.tp_clear 槽中实现。正如我们刚刚解释的,循环中只有一个对象实现 PyTypeObject.tp_clear 就足够了。

启用释放垃圾桶

在 CPython 中,可以创建深度递归对象。例如:

>>> L = None
>>> for i in range(2**20):
...     L = [L]

现在假设我们删除了最后的 L。然后 L 释放 L[0],它释放 L[0][0],依此类推,直到我们达到 2**20 的递归深度。这种释放是在 C 中完成的,如此深的递归很可能会导致 C 调用堆栈溢出,从而导致 Python 崩溃。

CPython 为此发明了一种机制,称为垃圾桶。它通过延迟一些释放来限制释放的递归深度。

默认情况下,Cython 扩展类型不使用垃圾桶,但可以通过将 trashcan 指令设置为 True 来启用它。例如:

import cython
@cython.trashcan(True)
@cython.cclass
class Object:
    __dict__: dict

垃圾桶的使用由子类继承(除非通过 @cython.trashcan(False) 显式禁用)。一些内置类型,如 list,使用垃圾桶,因此它的子类默认使用垃圾桶。

禁用循环中断 (tp_clear)

默认情况下,每个扩展类型都将支持 CPython 的循环垃圾收集器。如果任何 Python 对象可以被引用,Cython 将自动生成 PyTypeObject.tp_traversePyTypeObject.tp_clear 槽。这通常是你想要的。

至少有一个原因可能导致你不想这样做:如果你需要在 __dealloc__ 特殊函数中清理一些外部资源,并且你的对象碰巧在一个循环引用中,垃圾收集器可能已经触发了对 PyTypeObject.tp_clear 的调用来清除该对象(参见 Introduction)。

在这种情况下,当调用 __dealloc__ 时,任何对象引用都将消失。现在你的清理代码失去了访问它需要清理的对象的权限。为了解决这个问题,你可以使用 no_gc_clear 指令来禁用清除特定类的实例。

@cython.no_gc_clear
@cython.cclass
class DBCursor:
    conn: DBConnection
    raw_cursor: cython.pointer(DBAPI_Cursor)
    # ...
    def __dealloc__(self):
        DBAPI_close_cursor(self.conn.raw_conn, self.raw_cursor)

此示例尝试在 Python 对象被销毁时通过数据库连接关闭游标。DBConnection 对象通过来自 DBCursor 的引用保持活动状态。但是,如果游标碰巧在一个循环引用中,垃圾收集器可能会删除数据库连接引用,这使得无法清理游标。

如果您使用 no_gc_clear,那么任何给定的引用循环中至少要包含一个没有 no_gc_clear 的对象。否则,循环将无法被打破,这会导致内存泄漏。

禁用循环垃圾回收

在极少数情况下,可以保证扩展类型不会参与循环,但编译器无法证明这一点。如果该类永远不会引用自身,即使是间接引用也不行,就会出现这种情况。在这种情况下,您可以使用 no_gc 指令手动禁用循环收集,但请注意,如果扩展类型实际上可以参与循环,这样做会导致内存泄漏。

@cython.no_gc
@cython.cclass
class UserInfo:
    name: str
    addresses: tuple

如果您确定地址只包含对字符串的引用,那么上述操作是安全的,并且可能会带来显著的加速,具体取决于您的使用模式。

控制腌制

默认情况下,Cython 将生成一个 __reduce__() 方法,以允许腌制扩展类型,当且仅当它的每个成员都可以转换为 Python 并且它没有 __cinit__ 方法时。要强制执行此行为(即,如果一个类无法被腌制,则在编译时抛出错误),请使用 @cython.auto_pickle(True) 装饰该类。也可以使用 @cython.auto_pickle(False) 进行注释,以获得旧的行为,即在任何情况下都不生成 __reduce__ 方法。

手动实现 __reduce____reduce_ex__ 方法也将禁用此自动生成,并且可用于支持更复杂类型的腌制。

公共和外部扩展类型

扩展类型可以声明为 extern 或 public。extern 扩展类型声明使在外部 C 代码中定义的扩展类型可用于 Cython 模块。public 扩展类型声明使在 Cython 模块中定义的扩展类型可用于外部 C 代码。

注意

Cython 目前不支持在纯 Python 模式下声明为 extern 或 public 的扩展类型。这并不被认为是一个问题,因为 public/extern 扩展类型通常在 .pxd 文件中声明,而不是在 .py 文件中声明。

外部扩展类型

extern 扩展类型允许您访问在 Python 核心或非 Cython 扩展模块中定义的 Python 对象的内部结构。

注意

在以前版本的 Pyrex 中,extern 扩展类型也用于引用在另一个 Pyrex 模块中定义的扩展类型。虽然您仍然可以这样做,但 Cython 为此提供了一种更好的机制。请参阅 在 Cython 模块之间共享声明

以下是一个示例,它将让您访问内置 complex 对象的 C 级成员

cdef extern from "complexobject.h":

    struct Py_complex:
        double real
        double imag

    ctypedef class __builtin__.complex [object PyComplexObject]:
        cdef Py_complex cval

# A function which uses the above type
def spam(complex c):
    print("Real:", c.cval.real)
    print("Imag:", c.cval.imag)

注意

一些重要事项

  1. 在这个例子中,使用了 ctypedef 类。这是因为,在 Python 头文件中,PyComplexObject 结构体是用

    typedef struct {
        ...
    } PyComplexObject;
    

    在运行时,导入 Cython c-扩展模块时将执行一个检查,以确保 __builtin__.complexPyTypeObject.tp_basicsizesizeof(`PyComplexObject) 相匹配。如果 Cython c-扩展模块是用一个版本的 complexobject.h 头文件编译的,但导入到一个头文件已更改的 Python 中,则此检查可能会失败。可以使用 check_size 在名称规范子句中调整此检查。

  2. 除了扩展类型的名称之外,还指定了可以找到其类型对象的模块。请参阅下面的隐式导入部分。

  3. 在声明外部扩展类型时,您不会声明任何方法。声明方法不是调用方法所必需的,因为调用是 Python 方法调用。此外,与 structunion 一样,如果您的扩展类声明位于 cdef extern from 块中,您只需要声明您希望访问的那些 C 成员。

名称规范子句

类声明中方括号内的部分是仅适用于 extern 或 public 扩展类型的特殊功能。此子句的完整形式为

[object object_struct_name, type type_object_name, check_size cs_option]

其中

  • object_struct_name 是类型 C 结构体的名称。

  • type_object_name 是类型静态声明的类型对象的名称。

  • cs_optionwarn(默认值)、errorignore,仅用于外部扩展类型。如果为 error,则编译时找到的 sizeof(object_struct) 必须与类型的运行时 PyTypeObject.tp_basicsize 完全匹配,否则模块导入将失败并出现错误。如果为 warnignore,则允许 object_struct 小于类型的 PyTypeObject.tp_basicsize,这表明运行时类型可能是更新模块的一部分,并且外部模块的开发人员以向后兼容的方式扩展了对象(仅在对象末尾添加新字段)。如果为 warn,在这种情况下将发出警告。

这些子句可以按任何顺序编写。

如果扩展类型声明位于 cdef extern from 块内,则需要对象子句,因为 Cython 必须能够生成与头文件中的声明兼容的代码。否则,对于 extern 扩展类型,对象子句是可选的。

对于 public 扩展类型,对象和类型子句都是必需的,因为 Cython 必须能够生成与外部 C 代码兼容的代码。

属性名称匹配和别名

有时,在 object_struct_name 中指定的类型的 C 结构体可能使用与 PyTypeObject 中的字段不同的标签。这在手动编写的 C 扩展中很容易发生,其中 PyTypeObject_Foo 有一个 getter 方法,但名称与 PyFooObject 中的名称不匹配。例如,在 NumPy 中,python 级别的 dtype.itemsize 是 C 结构体字段 elsize 的 getter。Cython 支持别名字段名称,以便可以在 Cython 代码中编写 dtype.itemsize,它将被编译成直接访问 C 结构体字段,而不会通过 dtype.__getattr__('itemsize') 的 C-API 等效项。

例如,我们可能有一个扩展模块 foo_extension

cdef class Foo:
    cdef public int field0, field1, field2;

    def __init__(self, f0, f1, f2):
        self.field0 = f0
        self.field1 = f1
        self.field2 = f2

但 C 结构体位于文件 foo_nominal.h

typedef struct {
     PyObject_HEAD
     int f0;
     int f1;
     int f2;
 } FooStructNominal;

请注意,该结构体使用 f0f1f2,但在 Foo 中它们分别是 field0field1field2。我们得到了这种情况,包括一个包含该结构体的头文件,我们希望编写一个函数来对这些值求和。如果我们编写一个扩展模块 wrapper

cdef extern from "foo_nominal.h":

    ctypedef class foo_extension.Foo [object FooStructNominal]:
        cdef:
            int field0
            int field1
            int field2

def sum(Foo f):
    return f.field0 + f.field1 + f.field2

那么 wrapper.sum(f)(其中 f = foo_extension.Foo(1, 2, 3))仍然会使用 C-API 等效项

return f.__getattr__('field0') +
       f.__getattr__('field1') +
       f.__getattr__('field1')

而不是我们想要的 C 等效项 return f->f0 + f->f1 + f->f2。我们可以通过使用以下方法对字段进行别名

cdef extern from "foo_nominal.h":

    ctypedef class foo_extension.Foo [object FooStructNominal]:
        cdef:
            int field0 "f0"
            int field1 "f1"
            int field2 "f2"

def sum(Foo f) except -1:
    return f.field0 + f.field1 + f.field2

现在 Cython 将用对 FooStructNominal 字段的直接 C 访问替换掉缓慢的 __getattr__。这在直接处理 Python 代码时非常有用。即使 Python 和 C 中的字段名称不同,也不需要对 Python 进行任何更改即可实现显著的加速。当然,应该确保字段是等效的。

C 内联属性

与 Python 属性属性类似,Cython 提供了一种在外部扩展类型上声明 C 级属性的方法。这通常用于通过更快的 C 级数据访问来隐藏 Python 属性,但也可以在从 Cython 使用现有类型时用于向这些类型添加某些功能。声明必须使用 cdef inline

例如,上面的 complex 类型也可以像这样声明

cdef extern from "complexobject.h":

    struct Py_complex:
        double real
        double imag

    ctypedef class __builtin__.complex [object PyComplexObject]:
        cdef Py_complex cval

        @property
        cdef inline double real(self):
            return self.cval.real

        @property
        cdef inline double imag(self):
            return self.cval.imag


def cprint(complex c):
    print(f"{c.real :.4f}{c.imag :+.4f}j")  # uses C calls to the above property methods.

隐式导入

Cython 要求您在 extern 扩展类声明中包含模块名称,例如

cdef extern class MyModule.Spam:
    ...

类型对象将从指定的模块隐式导入,并绑定到该模块中的相应名称。换句话说,在这个例子中,一个隐式的

from MyModule import Spam

语句将在模块加载时执行。

模块名称可以是带点的名称,以引用包层次结构中的模块,例如

cdef extern class My.Nested.Package.Spam:
    ...

您还可以使用 as 子句指定一个替代名称,在该名称下导入类型,例如

cdef extern class My.Nested.Package.Spam as Yummy:
   ...

这对应于隐式导入语句

from My.Nested.Package import Spam as Yummy

类型名称与构造函数名称

在 Cython 模块内部,扩展类型的名称具有两个不同的用途。在表达式中使用时,它指的是一个模块级全局变量,该变量保存类型的构造函数(即它的类型对象)。但是,它也可以用作 C 类型名称来声明该类型的变量、参数和返回值。

当您声明

cdef extern class MyModule.Spam:
    ...

名称 Spam 同时扮演这两个角色。可能还有其他名称可以用来引用构造函数,但只有 Spam 可以用作类型名称。例如,如果您要显式导入 MyModule,您可以使用 MyModule.Spam() 来创建一个 Spam 实例,但您不能使用 MyModule.Spam 作为类型名称。

当使用 as 子句时,as 子句中指定的名称也同时承担这两个角色。因此,如果您声明

cdef extern class MyModule.Spam as Yummy:
    ...

那么 Yummy 既是类型名称,也是构造函数的名称。同样,还有其他方法可以获取构造函数,但只有 Yummy 可以用作类型名称。

公共扩展类型

扩展类型可以声明为公共类型,在这种情况下,将生成一个 .h 文件,其中包含其对象结构体和类型对象的声明。通过在您编写的外部 C 代码中包含 .h 文件,该代码可以访问扩展类型的属性。

数据类扩展类型

Cython 支持扩展类型,其行为类似于 Python 3.7+ 标准库中定义的数据类。使用数据类的主要好处是它可以自动生成简单的 __init____repr__ 和比较函数。Cython 实现尽可能地与 Python 标准库实现保持一致,因此这里只简要概述了差异 - 如果你打算使用它们,请阅读 标准库模块的文档

数据类可以使用 @dataclasses.dataclass 装饰器在 Cython 扩展类型(标记为 cdef 的类型或使用 cython.cclass 装饰器创建的类型)上声明。或者,@cython.dataclasses.dataclass 装饰器可以应用于任何类,将其同时转换为扩展类型和数据类。如果你需要在字段上定义特殊属性,请使用 dataclasses.field(或者 cython.dataclasses.field 也可以)。

import cython
try:
    import typing
    import dataclasses
except ImportError:
    pass  # The modules don't actually have to exists for Cython to use them as annotations

@dataclasses.dataclass
@cython.cclass
class MyDataclass:
    # fields can be declared using annotations
    a: cython.int = 0
    b: double = dataclasses.field(default_factory = lambda: 10, repr=False)


    c: str = 'hello'


    # typing.InitVar and typing.ClassVar also work
    d: dataclasses.InitVar[double] = 5
    e: typing.ClassVar[list] = []

你可以使用 C 级类型,例如结构体、指针或 C++ 类。但是,你可能会发现这些类型与自动生成的特殊方法不兼容 - 例如,如果它们不能从 Python 类型转换,则不能传递给构造函数,因此你必须使用 default_factory 来初始化它们。与 Python 实现一样,你也可以使用 field() 控制属性在哪些特殊函数中使用。