Skip to content

扩展类型

原文: 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 代码访问,您需要将它们声明为 publicreadonly 。例如:

# 传统风格
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 数值的属性。
  • 注意:此外, publicreadonly 选项仅适用于 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 属性查找的方式。如果属性已被声明为 publicreadonly ,那么这将起作用,但效率非常低。如果属性是私有的,它根本不起作用 - 代码将编译,但是在运行时会引发属性错误。

解决方案是将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           # 很危险

那么我们模块的用户可以通过传递 Nonesh参数来使其崩溃。

在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 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 代码中相同的语法在扩展类中声明属性:

# 经典风格
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 级进行子类型化。

前向声明扩展类型

扩展类型可以是前向声明的,如 structunion 类型。这通常不是必要的,违反了 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返回后无法访问xy,两者的引用计数均为 1,因此不会立即取消分配。在常规时间,垃圾收集器运行,它会注意到参考周期(使用tp_traverse插槽)并将其中断。打破引用循环意味着在循环中获取一个对象并将其中的所有引用移除到其他 Python 对象(我​​们称之为清除一个对象)。清除与解除分配几乎是一样的,只是实际对象尚未释放。对于上例中的xx的属性将从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_traversetp_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__.complextp_basicsize 是否与 sizeof(PyComplexObject)符合。如果使用一个版本的complexobject.h头文件编译 Cython extension 模块但导入到有不一样头文件的 Python 中,此检查可能会失败。可以使用名称规范子句中的check_size来调整此检查。 2. 除了扩展类型的名称外,还指定了可以在其中找到类型对象的模块。请参阅下面的隐式导入部分。 3. 声明外部扩展类型时,不要声明任何方法。调用它们不需要声明方法,因为调用是 Python 方法调用。而且,与structunion一样,如果你的扩展类声明在块中的cdef` extern 内,你只需要声明您希望访问的 C 成员。

名称规范子句

类声明中的方括号部分是仅适用于外部或公共扩展类型的特殊特性。该子句的完整形式是:

[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) 必须与类型的运行时 tp_basicsize 完全匹配,否则模块导入将失败并显示错误。如果是 warnignore ,则允许 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;

请注意,结构使用f0f1f2,但它们是 Foo 中的 field0field1field2。我们遇到过这种情况以及带有该结构的头文件,此时我们编写一个函数来对值进行求和。如果我们这么编写扩展模块 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() 来控制属性中使用哪个特殊函数。



回到顶部