扩展类型¶
简介¶
注意
此页面使用两种不同的语法变体
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.")
cdef class Shrubbery:
cdef int width
cdef int height
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
cdef class Shrubbery:
cdef int width
cdef int height
属性可以是 Python 对象(通用或特定扩展类型),也可以是任何 C 数据类型。因此,您可以使用扩展类型来包装任意 C 数据结构并为它们提供类似 Python 的接口。
静态属性¶
扩展类型的属性直接存储在对象的 C 结构中。属性集在编译时固定;您不能在运行时简单地通过为它们赋值来向扩展类型实例添加属性,就像您可以对 Python 类实例那样。但是,您可以明确启用对动态分配属性的支持,或者使用普通的 Python 类对扩展类型进行子类化,然后支持任意属性分配。请参阅 动态属性。
扩展类型的属性可以通过两种方式访问:通过 Python 属性查找或通过从 Cython 代码直接访问 C 结构。Python 代码只能通过第一种方法访问扩展类型的属性,但 Cython 代码可以使用任一方法。
默认情况下,扩展类型属性只能通过直接访问,而不能通过 Python 访问,这意味着它们无法从 Python 代码中访问。要使它们从 Python 代码中访问,您需要将它们声明为 public
或 readonly
。例如
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')
cdef class Shrubbery:
cdef public int width, height
cdef readonly float depth
使 width 和 height 属性可从 Python 代码读写,而 depth 属性可读但不可写。
注意
您只能公开简单的 C 类型,例如整数、浮点数和字符串,以供 Python 访问。您也可以公开 Python 值属性。
动态属性¶
默认情况下,无法在运行时向扩展类型添加属性。您可以通过两种方法来避免此限制,这两种方法在从 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
cdef class Animal:
cdef int number_of_legs
def __init__(self, int number_of_legs):
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
cdef class Animal:
cdef int number_of_legs
cdef dict __dict__
def __init__(self, int number_of_legs):
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
cdef widen_shrubbery(sh, extra_width): # BAD
sh.width = sh.width + extra_width
由于 sh
参数没有指定类型,因此将通过 Python 属性查找访问 width 属性。如果该属性已声明为 public
或 readonly
,那么这将起作用,但效率非常低下。如果该属性是私有的,它将根本不起作用 - 代码将编译,但在运行时将引发属性错误。
解决方案是将 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
from my_module cimport Shrubbery
cdef widen_shrubbery(Shrubbery sh, 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
from my_module cimport Shrubbery
cdef Shrubbery another_shrubbery(Shrubbery sh1):
cdef Shrubbery sh2
sh2 = Shrubbery()
sh2.width = sh1.width
sh2.height = sh1.height
return sh2
注意
在这里,我们cimport 类 Shrubbery
(使用 cimport
语句或从特殊的 cython.cimports
包中导入),这是在编译时声明类型所必需的。为了能够 cimport 扩展类型,我们将类定义分成两部分,一部分在定义文件中,另一部分在相应的实现文件中。您应该阅读 共享扩展类型 以了解如何做到这一点。
类型测试和强制转换¶
假设我有一个方法 quest()
,它返回 Shrubbery
类型的对象。要访问它的宽度,我可以写
sh: Shrubbery = quest()
print(sh.width)
cdef Shrubbery sh = quest()
print(sh.width)
这需要使用局部变量并在赋值时执行类型测试。如果您知道 quest()
的返回值将是 Shrubbery
类型,您可以使用强制转换来写
print( cython.cast(Shrubbery, quest()).width )
print( (<Shrubbery>quest()).width )
如果 quest()
实际上不是一个 Shrubbery
,这可能会很危险,因为它会尝试访问宽度作为 C 结构体成员,而该成员可能不存在。在 C 级别,而不是引发一个 AttributeError
,要么返回一个无意义的结果(将该地址处的任何数据解释为一个整数),要么尝试访问无效内存会导致段错误。相反,可以写
print( cython.cast(Shrubbery, quest(), typecheck=True).width )
print( (<Shrubbery?>quest()).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 None
和 typing.Optional
只能在 Python 函数(使用 def
定义,不使用 @cython.cfunc
装饰器)中使用,不能在 C 函数(使用 cdef
定义或使用 @cython.cfunc
装饰)中使用。如果您需要检查 C 函数的参数是否为 None
,则需要自己进行检查。
注意
更多内容
扩展类型方法的
self
参数保证永远不会是None
。当将一个值与
None
进行比较时,请记住,如果x
是一个 Python 对象,x is None
和x is not None
非常高效,因为它们直接转换为 C 指针比较,而x == None
和x != 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
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)
cdef class CheeseShop:
cdef object cheeses
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 方法使用
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()
cdef class Parrot:
cdef void describe(self):
print("This parrot is resting.")
cdef class Norwegian(Parrot):
cdef void describe(self):
Parrot.describe(self)
print("Lovely plumage!")
cdef Parrot p1, p2
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
from libc.stdlib cimport free
cdef class OwnedPointer:
cdef void* ptr
def __dealloc__(self):
if self.ptr is not NULL:
free(self.ptr)
@staticmethod
cdef create(void* ptr):
p = OwnedPointer()
p.ptr = ptr
return p
注意
Cython 目前不支持使用 @classmethod
装饰器装饰 cdef
/@ccall
方法。
子类化¶
如果扩展类型继承自其他类型,则第一个基类必须是内置类型或其他扩展类型。
@cython.cclass
class Parrot:
...
@cython.cclass
class Norwegian(Parrot):
...
cdef class Parrot:
...
cdef 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
cimport cython
@cython.final
cdef class Parrot:
def describe(self): pass
cdef class Lizard:
@cython.final
cdef done(self): pass
尝试从最终类型创建 Python 子类或覆盖最终方法将在运行时引发 TypeError
。Cython 也会阻止在同一个模块内对最终类型进行子类化或覆盖最终方法,即创建使用最终类型作为基类型的扩展类型将在编译时失败。但是,请注意,此限制目前不会传播到其他扩展模块,因此 Cython 无法阻止最终扩展类型在 C 级被外部代码子类化。
前向声明扩展类型¶
扩展类型可以像 struct
和 union
类型一样进行前向声明。这通常是不必要的,并且违反了 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__() !
cdef class Penguin:
cdef object food
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!
cimport cython
@cython.freelist(8)
cdef class Penguin:
cdef object food
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)
from libc.stdlib cimport malloc, free
# Example C struct
ctypedef struct my_c_struct:
int a
int b
cdef class WrapperClass:
"""A wrapper class for a C/C++ data structure"""
cdef my_c_struct *_ptr
cdef bint ptr_owner
def __cinit__(self):
self.ptr_owner = False
def __dealloc__(self):
# De-allocate if not null and flag is set
if self._ptr is not NULL and self.ptr_owner is True:
free(self._ptr)
self._ptr = 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 NULL else None
@property
def b(self):
return self._ptr.b if self._ptr is not NULL else None
@staticmethod
cdef WrapperClass from_ptr(my_c_struct *_ptr, bint owner=False):
"""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.
cdef WrapperClass wrapper = WrapperClass.__new__(WrapperClass)
wrapper._ptr = _ptr
wrapper.ptr_owner = owner
return wrapper
@staticmethod
cdef WrapperClass new_struct():
"""Factory function to create WrapperClass objects with
newly allocated my_c_struct"""
cdef my_c_struct *_ptr = <my_c_struct *>malloc(sizeof(my_c_struct))
if _ptr is 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
cdef class ExplodingAnimal:
"""This animal will self-destruct when it is
no longer strongly referenced."""
cdef object __weakref__
在 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
返回后,x
和 y
都不可访问,但它们的引用计数都为 1,因此不会立即被释放。在定期时间,垃圾收集器会运行,它会注意到循环引用(使用 PyTypeObject.tp_traverse
槽)并将其打破。打破循环引用意味着从循环中的一个对象中删除所有对其他 Python 对象的引用(我们称之为清除一个对象)。清除几乎与释放相同,只是实际的对象尚未被释放。对于上面的示例中的 x
,x
的属性将从 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
cimport cython
@cython.trashcan(True)
cdef class Object:
cdef dict __dict__
垃圾桶的使用由子类继承(除非通过 @cython.trashcan(False)
显式禁用)。一些内置类型,如 list
,使用垃圾桶,因此它的子类默认使用垃圾桶。
禁用循环中断 (tp_clear
)¶
默认情况下,每个扩展类型都将支持 CPython 的循环垃圾收集器。如果任何 Python 对象可以被引用,Cython 将自动生成 PyTypeObject.tp_traverse
和 PyTypeObject.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)
@cython.no_gc_clear
cdef class DBCursor:
cdef DBConnection conn
cdef DBAPI_Cursor *raw_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.no_gc
cdef class UserInfo:
cdef str name
cdef tuple addresses
如果您确定地址只包含对字符串的引用,那么上述操作是安全的,并且可能会带来显著的加速,具体取决于您的使用模式。
控制腌制¶
默认情况下,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)
注意
一些重要事项
在这个例子中,使用了
ctypedef
类。这是因为,在 Python 头文件中,PyComplexObject
结构体是用typedef struct { ... } PyComplexObject;
在运行时,导入 Cython c-扩展模块时将执行一个检查,以确保
__builtin__.complex
的PyTypeObject.tp_basicsize
与sizeof(`PyComplexObject)
相匹配。如果 Cython c-扩展模块是用一个版本的complexobject.h
头文件编译的,但导入到一个头文件已更改的 Python 中,则此检查可能会失败。可以使用check_size
在名称规范子句中调整此检查。除了扩展类型的名称之外,还指定了可以找到其类型对象的模块。请参阅下面的隐式导入部分。
在声明外部扩展类型时,您不会声明任何方法。声明方法不是调用方法所必需的,因为调用是 Python 方法调用。此外,与
struct
和union
一样,如果您的扩展类声明位于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_option
是warn
(默认值)、error
或ignore
,仅用于外部扩展类型。如果为error
,则编译时找到的sizeof(object_struct)
必须与类型的运行时PyTypeObject.tp_basicsize
完全匹配,否则模块导入将失败并出现错误。如果为warn
或ignore
,则允许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;
请注意,该结构体使用 f0
、f1
、f2
,但在 Foo
中它们分别是 field0
、field1
和 field2
。我们得到了这种情况,包括一个包含该结构体的头文件,我们希望编写一个函数来对这些值求和。如果我们编写一个扩展模块 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] = []
cimport 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
cdef class MyDataclass:
# fields can be declared using annotations
a: cython.int = 0
b: cython.double = dataclasses.field(default_factory = lambda: 10, repr=False)
# fields can also be declared using `cdef`:
cdef str c # add `readonly` or `public` to if `c` needs to be accessible from Python
c = "hello" # assignment of default value on a separate line
# note: `@dataclass(frozen)` is not enforced on `cdef` attributes
# typing.InitVar and typing.ClassVar also work
d: dataclasses.InitVar[cython.double] = 5
e: typing.ClassVar[list] = []
你可以使用 C 级类型,例如结构体、指针或 C++ 类。但是,你可能会发现这些类型与自动生成的特殊方法不兼容 - 例如,如果它们不能从 Python 类型转换,则不能传递给构造函数,因此你必须使用 default_factory
来初始化它们。与 Python 实现一样,你也可以使用 field()
控制属性在哪些特殊函数中使用。