扩展类型
原文: http://docs.cython.org/en/latest/src/userguide/extension_types.html
介绍
除了使用 Python 类语句创建普通的用户定义类之外,Cython 还允许您创建新的内置 Python 类型,称为扩展类型。使用 cdef
类语句或者 @cython.cclass
装饰器 可以定义扩展类型。这是一个例子:
from __future__ import print_function
cdef class Shrubbery:
cdef int width, 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()
函数 或者
- 属性名称的注解
# 传统风格
cdef class Shrubbery:
cdef int width
cdef int height
# 纯Python风格
@cython.cclass
class Shrubbery:
width = declare(cython.int)
height: cython.int
属性可以是 Python 对象(通用或特定扩展类型),或者它们可以是任何 C 数据类型。因此,您可以使用扩展类型来包装任意 C 数据结构,并为它们提供类似 Python 的接口。
静态属性
扩展类型的属性直接存储在对象的 C 结构中。这组属性在编译时是固定的;您无法在运行时像使用Python类实例一样,通过赋值向扩展类型实例添加属性。但是,您可以显式启用对动态分配的属性的支持,或者使用普通的 Python 类将扩展类型子类化,然后支持任意属性分配。参见 动态属性 。
有两种方法可以访问扩展类型的属性:通过 Python 属性查找,或在 Cython 代码中直接访问 C 结构。 Python 代码只能通过第一种方法访问扩展类型的属性,但 Cython 代码可以使用任一方法。
默认情况下,扩展类型属性只能通过直接访问访问,而不能通过 Python 访问访问,这意味着无法从 Python 代码访问它们。要使它们可以从 Python 代码访问,您需要将它们声明为 public 或 readonly 。例如:
# 传统风格
cdef class Shrubbery:
cdef public int width, height
cdef readonly float depth
# 纯Python风格
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')
使得宽度和高度属性可以从 Python 代码中读写,深度属性可读但不可写。
- 注意: 您只能把简单的 C 类型,例如整数,浮点数和字符串暴露给Python访问。您还可以暴露 使用 Python 数值的属性。
- 注意:此外, public 和 readonly 选项仅适用于 Python 访问,而不适用于直接访问。扩展类型的所有属性始终可通过 C 级访问进行读写。
动态属性
默认情况下,无法在运行时向扩展类型添加属性。您有两种方法可以绕开这种限制,无论哪种方法,在当从 Python 代码调用方法时都会增加开销。特别是在调用cpdef
在 .pyx
文件声明 或者是 用 @ccall
装饰器装饰的混合方法时。
第一种方法是创建一个 Python 子类:
# 经典风格
cdef class Animal:
cdef int number_of_legs
def __cinit__(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
# 纯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__
属性是启用动态属性的第二种方式:
# 经典风格
cdef class Animal:
cdef int number_of_legs
cdef dict __dict__
def __cinit__(self, int number_of_legs):
self.number_of_legs = number_of_legs
dog = Animal(4)
dog.has_tail = True
# 纯Python风格
@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
参数。但在其他情况下,您将不得不使用类型声明。
例如,在以下功能中:
# 经典风格
cdef widen_shrubbery(sh, extra_width): # BAD
sh.width = sh.width + extra_width
# 纯Python风格
@cython.cfunc
def widen_shrubbery(sh, extra_width): # BAD
sh.width = sh.width + extra_width
因为sh
参数没有给出类型,所以访问 width 属性将使用 Python 属性查找的方式。如果属性已被声明为 public 或 readonly ,那么这将起作用,但效率非常低。如果属性是私有的,它根本不起作用 - 代码将编译,但是在运行时会引发属性错误。
解决方案是将sh
声明为Shrubbery
类型,如下所示:
# 经典风格
from my_module cimport Shrubbery
cdef widen_shrubbery(Shrubbery sh, extra_width):
sh.width = sh.width + extra_width
# 纯Python风格
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 属性,并将生成代码以直接有效地访问它。局部变量也适用,例如:
# 经典风格
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
# 纯python风格
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
注意:我们这里引用
Shrubbery
类(通过cimport
语句或者是 引用cython.cimports
包), 这是在编译时声明类型所必需的。为了能够引用扩展类型,我们将类定义分为两部分,一部分在定义文件中,另一部分在相应的实现文件中。你可以阅读 共享扩展类型 来学习如何这样做。
类型测试和转换
假设我有一个方法quest()
,它返回Shrubbery
类型的对象。要访问它的宽度我可以写:
# 经典风格
cdef Shrubbery sh = quest()
print(sh.width)
# 纯Python风格
sh: Shrubbery = quest()
print(sh.width)
这需要使用局部变量并在赋值时执行类型测试。如果你明确知道 quest()
的返回值将是Shrubbery
类型,你可以使用强制转换来写:
# 经典风格
print( (<Shrubbery>quest()).width )
# 纯Python风格
print( cython.cast(Shrubbery, quest()).width )
如果quest()
实际上不是Shrubbery
,可能会发生危险,因为它会尝试访问"width"这个可能不存在的 C 结构成员。在 C 级别,会返回一个无意义的结果(将该地址处的任何数据解释为 int)或者尝试访问无效内存并产生一个段错误,不是抛出 AttributeError
。相反,人们可以写:
# 经典风格
print( (<Shrubbery?>quest()).width )
# 纯Python风格
print( cython.cast(Shrubbery, quest(), typecheck=True).width )
在进行强制转换并允许代码继续之前执行类型检查(可能引发 TypeError
)。
要显式测试对象的类型,请使用isinstance()
内置函数。对于已知的内置或扩展类型,Cython 将这些转换为快速且安全的类型检查,忽略对象的__class__
属性等的更改。因此在成功进行isinstance()
测试后,代码可以依赖预期的 C 结构扩展类型及其 cdef
/ @cfunc
属性和方法。
扩展类型和无(None)
在使用Python注解或者是C风格的类型声明是,Cython处理 None
值的方式是不一样的。
当您将参数或 C 变量声明为扩展类型或者Python内置类型时,Cython 将允许 cdef
声明和C风格的函数参数 (func(list x)
) 声明采用 None
值和声明类型的值。这类似于 C 指针可以采用 NULL
值得的方式,因此需要谨慎行事。您只对它执行 Python 操作就没有问题,因为将应用完整的动态类型检查。但是,当您访问扩展类型的 C 属性时(如上面的 widen_shrubbery 函数),由您来确保您使用的引用不是 None
- 为了提高效率,Cython 不会检查这个。
用C风格的声明时,暴露将扩展类型作为参数的 Python 函数时,您需要特别小心:
def widen_shrubbery(Shrubbery sh, extra_width): # 这个代码
sh.width = sh.width + extra_width # 很危险
那么我们模块的用户可以通过传递 None
给 sh
参数来使其崩溃。
在Python中,如果不清楚变量是否可以为 None
,但是代码要求一个非-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_widt
现在该函数将自动检查sh
是否为not None
并检查它是否具有正确的类型。
在使用注解时,处理行为将遵循Python的PEP-484 类型语义(而不是上述行为)。当数值只被注解为普通类型时,不允许接收 None
。
def widen_shrubbery(sh: Shrubbery, extra_width): # 当sh为None时
sh.width = sh.width + extra_width # 会触发TypeError - 类型错误
要允许 None
值必须显式的使用 typing.Optional[ ]
。对于函数参数来说,当它们有一个默认参数为 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 代码中相同的语法在扩展类中声明属性:
# 经典风格
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.
# 纯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__()
方法都是可选的;如果省略它们,则在尝试相应操作时将引发异常。
这是一个完整的例子。它定义了一个属性,每次写入时都会添加到列表中,在读取列表时返回列表,并在删除列表时清空列表:
# cheesy.pyx
# 经典风格
cdef class CheeseShop:
cdef object cheeses
def __cinit__(self):
self.cheeses = []
@property
def cheese(self):
return "We don't have: %s" % 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)
# 纯python风格
import cython
@cython.cclass
class CheeseShop:
cheeses: object
def __cinit__(self):
self.cheeses = []
@property
def cheese(self):
return "We don't have: %s" % 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方法
扩展类型可以有Python方法,也可以有C方法。像声明C函数一样,C方法可以这么声明:
- C方法:
cdef
或者@cfunc
装饰器,而不是cdef
- 混合方法:
cpdef
或者@ccall
装饰器
C函数是“虚”的,在扩展类型中可以被覆盖。除此之外,cpdef / @ccall
在被当作C方法调用时甚至可以被 Python 方法覆盖。这与调用 cdef / @cfunc
方法比增加了一点额外开销(译者:有点像虚函数表)。
# 经典风格
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()
# 纯Python风格
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兼容类型的类时很有用。
# 经典风格
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
# 纯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
方法。
子类化
扩展类型可以从内置类型或其他扩展类型继承:
# 经典风格
cdef class Parrot:
...
cdef class Norwegian(Parrot):
...
# 纯Python风格
@cython.cclass
class Parrot:
...
@cython.cclass
class Norwegian(Parrot):
...
Cython必须拥有基本类型的完整定义。因此如果基类型是内置类型,则它必须先前已声明为 extern 扩展类型。如果基类型在另一个 Cython 模块中定义,则必须将其声明为 extern 扩展类型或使用 cimport
语句 或者 从 cython.cimports
库(纯Python风格)导入。
支持多重继承,但是第二个和之后的父类必须是普通的Python类(不可以是内置类型或者扩展类型)。
Python中可以继承Cython的扩展类型。 Python 类可以从多个扩展类型继承,前提是遵循通常的多重继承的 Python 规则(即所有基类的 C 布局必须兼容)。
有一种方法可以防止扩展类型在 Python 中被子类型化。这是通过 final
指令完成的,通常使用装饰器在扩展类型上设置:
# 经典风格
cimport cython
@cython.final
cdef class Parrot:
def describe(self): pass
cdef class Lizard:
@cython.final
cdef done(self): pass
# 纯Python风格
import cython
@cython.final
@cython.cclass
class Parrot:
def describe(self): pass
@cython.cclass
class Lizard:
@cython.final
@cython.cfunc
def done(self): pass
尝试从 final 类型集成 或者覆盖一个 final 方法时将引发 TypeError
。 Cython 还将阻止在同一模块内部对最终类型进行子类型化,即创建使用最终类型的扩展类型,因为其基类型将在编译时失败。但请注意,此限制目前不会传播到其他扩展模块,因此即使是最终扩展类型仍可以通过外部代码在 C 级进行子类型化。
前向声明扩展类型
扩展类型可以是前向声明的,如 struct
和 union
类型。这通常不是必要的,违反了 DRY 原则(不要重复自己)。
如果要向前声明具有基类的扩展类型,则必须在前向声明及其后续定义中都指定基类,例如:
cdef class A(B)
...
cdef class A(B):
# attributes and methods
快速实例化
Cython 提供了两种加速扩展类型实例化的方法。第一个是直接调用__new__()
特殊静态方法,如 Python 所知。对于扩展类型Penguin
,您可以使用以下代码:
# 经典风格
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__() !
# 纯Python风格
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!
,但第二个实例化不会打印eating!
。这只是__cinit__()
方法比扩展类型的正常__init__()
方法,在初始化扩展类型并保证正确且安全的状态方面,要更安全和更可取的原因之一。
第二个性能改进适用于经常连续创建和删除的类型,以便它们可以从空闲列表中受益。 Cython 为此提供了装饰器@cython.freelist(N)
,它为给定类型创建了一个静态大小的N
实例空闲列表。例:
# 经典风格
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') # 不用分配内存
# 纯Python风格
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') # 不用分配内存
从现有的 C / C ++指针实例化
想要从现有的指向一个数据结构的指针来实例化一个扩展类是很常见(该指针通常由外部 C / C ++函数返回)。
由于扩展类只能在其构造函数中接受 Python 对象作为参数,因此必须使用工厂函数。例如,
# 经典风格
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
一个C/C++ 数据结构的包装器类
"""
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
# 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.
一个从给定 my_c_struct指针出发,创建包装器类的工厂函数。
Setting ``owner`` flag to ``True`` causes
the extension type to ``free`` the structure pointed to by ``_ptr``
when the wrapper object is deallocated.
把``owner`` flag标识设置为``True``会令扩展类型释放``_ptr``指向的结构
"""
# Call to __new__ bypasses __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
用新分配的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)
# 纯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__
的 object
类型的 C 属性来允许弱引用。例如,:
# 经典风格
cdef class ExplodingAnimal:
"""This animal will self-destruct when it is
no longer strongly referenced."""
cdef object __weakref__
# 纯Python风格
@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"
,因此将其取消分配。这是使用tp_dealloc
插槽完成的,可以通过实现__dealloc__
来在 Cython 中自定义。
第二种机制是循环垃圾收集器。这是为了解决循环引用的问题,比如下面:
>>> class Object:
... pass
>>> def make_cycle():
... x = Object()
... y = [x]
... x.attr = y
调用make_cycle
时,会创建一个引用循环,因为x
引用y
,反过来也是。即使在make_cycle
返回后无法访问x
或y
,两者的引用计数均为 1,因此不会立即取消分配。在常规时间,垃圾收集器运行,它会注意到参考周期(使用tp_traverse
插槽)并将其中断。打破引用循环意味着在循环中获取一个对象并将其中的所有引用移除到其他 Python 对象(我们称之为清除一个对象)。清除与解除分配几乎是一样的,只是实际对象尚未释放。对于上例中的x
,x
的属性将从x
中删除。
请注意,在参考周期中只清除一个对象就足够了,因为在清除一个对象后不再有一个周期。一旦循环中断,通常基于引用计数的释放将实际从内存中删除对象。清除在tp_clear
插槽中实现。正如我们刚刚解释的那样,循环中的一个对象实现tp_clear
就足够了。
启用垃圾释放箱
在 CPython 中,可以创建深度递归对象。例如:
>>> L = None
>>> for i in range(2**20):
... L = [L]
现在假设我们删除了最后的L
。然后L
解除分配L[0]
,L[0]
解除分配L[0][0]
,依此类推,直到达到2**20
的递归深度。这种解除分配是在 C 中完成的,这种深度递归可能会溢出 C 调用堆栈,从而导致 Python 崩溃。
CPython 发明了一种称为垃圾箱的机制。它通过延迟一些解除分配来限制解除分配的递归深度。
默认情况下,Cython 扩展类型不使用垃圾桶,但可以通过将trashcan
指令设置为True
来启用它。例如:
# 经典风格
cimport cython
@cython.trashcan(True)
cdef class Object:
cdef dict __dict__
# 纯Python风格
import cython
@cython.trashcan(True)
@cython.cclass
class Object:
__dict__: dict
子类可以继承(父类使用)垃圾箱的行为(除非用@cython.trashcan(False)
明确禁用)。像list
这样的内置类型使用垃圾桶,它的子类也会默认使用。
禁用循环中断(tp_clear
)
默认情况下,每种扩展类型都支持 CPython 的循环垃圾收集器。对于任何可以引用的 Python 对象,Cython 将自动生成tp_traverse
和tp_clear
插槽。通常情况下这能满足你的需求。
有一个你可能不想要这么做的原因是:如果你需要清理__dealloc__
特殊功能中的一些外部资源而你的对象恰好处于引用周期,垃圾收集器可能已经触发了一个tp_clear
清除对象。
在这种情况下,调用__dealloc__
时,任何对象引用都会消失。现在,您的清理代码无法访问它必须清理的对象。要解决此问题,您可以使用no_gc_clear
指令来禁止清除特定类的实例:
# 经典风格
@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风格
@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
cdef class UserInfo:
cdef str name
cdef tuple addresses
# 纯Python风格
@cython.no_gc
@cython.cclass
class UserInfo:
name: str
addresses: tuple
如果您可以确定地址仅包含对字符串的引用,则上述内容将是安全的,且可能会产生显着的加速(取决于你的使用方式)。
控制对象序列化(Pickling)
默认情况下,Cython 将生成一个__reduce__()
方法以便,当且仅当其每个成员都可以转换为 Python 且没有__cinit__
方法时,允许将扩展类型序列化。使用@cython.auto_pickle(True)
装饰这个类以允许序列化行为(即,如果无法对类进行 pickle,则在编译时抛出错误)。也可以用@cython.auto_pickle(False)
注解来保证在任何情况下都不生成__reduce__
方法。
手动实现__reduce__
或 __reduce_ex__
方法也将禁用此自动生成,并可用于支持更复杂类型的对象序列化。
公共和外部扩展类型
扩展类型可以声明为 extern 或 public。外部扩展类型声明使外部 C 代码中定义的扩展类型可用于 Cython 模块。公共扩展类型声明使得在 Cython 模块中定义的扩展类型可用于外部 C 代码。
注意:纯Python风格下,Cython目前还不支持将扩展类型声明为extern或者public。开发人员认为这是合理的,因为public/extern的扩展类型主要是在
.pxd
文件而不是.py
文件中声明。
外部扩展类型
外部扩展类型允许您访问 Python 核心或非 Cython 扩展模块中定义的 Python 对象的内部。
注意:在以前版本的 Pyrex 中,extern 扩展类型也用于引用另一个 Pyrex 模块中定义的扩展类型。虽然你仍然可以做到这一点,但 Cython 为此提供了更好的机制。参见 在 Cython 模块之间共享声明。
下面这个例子演示了如何从内置复杂对象中获取 C 级别的成员:
from __future__ import print_function
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
结构声明为:py typedef struct { ... } PyComplexObject;
在运行期间,导入Cython C-扩展模块将会检查__builtin__.complex
的tp_basicsize
是否与sizeof(
PyComplexObject)符合。如果使用一个版本的
complexobject.h头文件编译 Cython extension 模块但导入到有不一样头文件的 Python 中,此检查可能会失败。可以使用名称规范子句中的
check_size来调整此检查。 2. 除了扩展类型的名称外,还指定了可以在其中找到类型对象的模块。请参阅下面的隐式导入部分。 3. 声明外部扩展类型时,不要声明任何方法。调用它们不需要声明方法,因为调用是 Python 方法调用。而且,与
struct和
union一样,如果你的扩展类声明在块中的
cdef` extern 内,你只需要声明您希望访问的 C 成员。
名称规范子句
类声明中的方括号部分是仅适用于外部或公共扩展类型的特殊特性。该子句的完整形式是:
[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)
必须与类型的运行时tp_basicsize
完全匹配,否则模块导入将失败并显示错误。如果是warn
或ignore
,则允许object_struct
小于类型的tp_basicsize
,这表示运行时类型可能是更新模块的一部分,并且外部模块的开发人员向后扩展了对象兼容的方式(仅在对象的末尾添加新字段)。如果warn
,在这种情况下将发出警告。
子句可以按任何顺序书写。
如果扩展类型声明是写在cdef
extern块中的,需要写 object 子句,因为 Cython 必须能够生成与头文件中的声明兼容的代码。不然的话,对于 extern 扩展类型,object 子句是可选的。
对于公共扩展类型,object 和 type 子句都是必需的,因为 Cython 必须能够生成与外部 C 代码兼容的代码。
属性名称匹配和别名
有时,object_struct_name
中指定的类型的 C 结构可能会使用与 PyTypeObject
中不同的字段标签。PyTypeObject_Foo
具有 getter 方法,但名称与 PyFooObject
中的名称不匹配的情况,在手写的 C 扩展中很容易发生。例如,在 NumPy 中,python-level dtype.itemsize
是 C struct 字段 elsize
的 getter。 Cython 支持字段设置别名,以便可以在 Cython 代码中编写 dtype.itemsize
,这些代码将被编译为 C struct 字段的直接访问,而无需通过相当于 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
但是文件foo_nominal.h
中的 C 结构:
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 feild2
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 将用对 CooStructNominal 字段的较慢的 __getattr__
替换为直接的C访问。这在直接处理 Python 代码时很有用。即使 Python 和 C 中的字段名称不同,也不需要对 Python 进行任何更改以实现显着的加速。当然,你应该确保字段是等效的。
内联特性(Property)
和Python的特性(Property)属性(Attribute)相似,Cython提供声明外部扩展类型的C级别特性的方法。这在用更快的C级别数据访问来遮蔽Python属性时很有用,还可以用来给,在Cython中使用现有的类型,增加特定的功能。
举例来说,上面的 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") # 使用 C 调用上面的特性方法.
隐式导入
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:
...
它对应于隐式 import 语句:
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 可用作类型名称。
公共扩展类型
扩展类型可以声明为 public,在这种情况下会生成一个包含其对象 struct 和 type 对象声明的 .h
文件。通过将 .h
文件包含在您编写的外部 C 代码中,该代码可以访问扩展类型的属性。
数据类扩展类型
Cython支持和Python 3.7+ 标准库定义的数据类(dataclasses)行为类似的扩展类型。使用数据类的主要好处是它能自动生成简单的 __init__
, __repr__
和 比较函数。Cython的实现几乎做到了和在Python标准库实现中尽可能的一模一样,因此这里的文件仅仅记录不同之处 -- 如果你准备使用数据类,你可以阅读 Python标准库文档 -- 数据类 中对应的内容。
数据类可以在Cython扩展类型(用 cdef
定义或者是使用 @cython.class
装饰器的类型)上加 @dataclasses.dataclass
装饰器来声明。或者, @cython.dataclasses.dataclass
装饰器也可以用来加在任何类上让它变成一个扩展类以及一个数据类。如果你需要在字段上定义特殊特性,你可以使用 dataclasses.field
或者 cython.dataclasses.field
。
# 经典风格
cimport cython
try:
import typing
import dataclasses
except ImportError:
pass # 这些模块不需要真实存在,Cyton也可以把它们用做注解
@dataclasses.dataclass
cdef class MyDataclass:
# 字段可以用注解声明
a: cython.int = 0
b: double = dataclasses.field(default_factory = lambda: 10, repr=False)
# 也可以用 `cdec` 声明
cdef str c
c = "hello" # 另起一行来赋默认值
# typing.InitVar 和 typing.ClassVar 也可以
d: dataclasses.InitVar[cython.double] = 5
e: typing.ClassVar[list] = []
# 纯Python风格
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类的构造器参数,那么它们就不可能被转化为这个Python类。因此你必须使用
default_factory
来初始化它们。就像是在python实现中一样,你可以用 field()
来控制属性中使用哪个特殊函数。