从 Cython 0.29 迁移到 3.0

Cython 3.0 是编译器和语言的重大修订,它带来了一些向后不兼容的更改。本文档列出了重要的更改,并解释了如何在现有代码中处理这些更改。

Python 3 语法/语义

Cython 3.0 现在默认使用 Python 3 语法和语义,以前需要将 language_level 指令 <编译器指令> 设置为 33str。新的默认设置现在是 language_level=3str,这意味着 Python 3 语义,但无前缀字符串是 str 对象,即 Python 3 下的 Unicode 文本字符串和 Python 2.7 下的字节字符串。

您可以通过设置 language_level=2 将代码恢复到以前的(Python 2.x)语义。

由于语言级别,进一步的语义更改包括

  • / 除法使用真正的(浮点)除法运算符,除非启用了 cdivision

  • print 是一个函数,而不是一个语句。

  • 在 Py2.x 中,没有基类定义的 Python 类(class C: ...)也是“新式”类(如果您从未听说过“旧式类”,那么您可能很乐意没有它们)。

  • 注释(类型提示)现在存储为字符串。(PEP 563

  • StopIteration 生成器中的处理已根据 PEP 479 进行了更改。

Python 语义

一些 Python 兼容性错误已修复,例如:

绑定函数

绑定指令 现在默认启用。这使得 Cython 编译的 Python (def) 函数在签名自省、注释等方面与普通(未编译)Python 函数基本兼容。

它还使它们在 Python 类中作为方法绑定到属性赋值,因此得名。如果这不是预期的,即如果一个函数实际上是一个函数而不是一个方法,您可以通过设置 binding=False 或选择性地添加装饰器 @cython.binding(False) 来禁用绑定(以及所有其他 Python 函数功能)。在纯 Python 模式下,装饰器在 Cython 0.29.16 中尚不可用,但编译后的代码不会受到影响。

但是,我们建议保留新的函数功能,而是使用标准 Python staticmethod() 内置函数来处理绑定问题。

def func(self, b): ...

class MyClass(object):
    binding_method = func

    no_method = staticmethod(func)

命名空间包

Cython 现在支持根据 PEP-420 从命名空间包加载 pxd 文件。这可能会影响导入路径。

NumPy C-API

Cython 以前生成依赖于已弃用的 NumPy-1.7 之前的 C-API 的代码。Cython 3.0 现在不再是这种情况。

现在您可以定义宏 NPY_NO_DEPRECATED_API=NPY_1_7_API_VERSION 来消除编译后的 C 模块使用已弃用 API 的长期构建警告。您可以选择在每个文件

# distutils: define_macros=NPY_NO_DEPRECATED_API=NPY_1_7_API_VERSION

或在 setup.py 中的 Extensions 中设置它

Extension(...
    define_macros=[("NPY_NO_DEPRECATED_API", "NPY_1_7_API_VERSION")]
)

不同 C-API 使用方式的一个副作用是,您的代码现在可能需要调用 NumPy C-API 初始化函数,而之前它可以不调用该函数。

为了减少用户在此方面的影响,Cython 3.0 现在将在看到 numpy 被 cimport,但该函数未被使用时自动调用它。在(希望很少见)的情况下,如果这妨碍了您的操作,可以通过模拟函数的使用而实际上不调用它来禁用内部 C-API 初始化,例如

# Explicitly disable the automatic initialisation of NumPy's C-API.
<void>import_array

类私有名称混淆

Cython 已更新为更密切地遵循 Python 类私有名称规则。本质上,任何以 __ 开头但不以 __ 结尾的名称在类中都会与类名混淆。大多数用户代码应该不受影响 - 与 Python 中不同的是,未混淆的全局名称仍然会匹配,以确保能够访问以 __ 开头的 C 名称

cdef extern void __foo()

class C: # or "cdef class"
   def call_foo(self):
       return __foo() # still calls the global name

不再起作用的是覆盖 cdef class 中以 __ 开头的的方法

cdef class Base:
    cdef __bar(self):
        return 1

    def call_bar(self):
        return self.__bar()

cdef class Derived(Base):
    cdef __bar(self):
        return 2

这里 Base.__bar 被混淆为 _Base__bar,而 Derived.__bar 被混淆为 _Derived__bar。因此,call_bar 将始终调用 _Base__bar。这与已建立的 Python 行为相匹配,并适用于 defcdefcpdef 方法和属性。

算术特殊方法

Cython 3.0 中 cdef 类的算术特殊方法(例如 __add____pow__)的行为已更改。它们现在支持这些方法的单独“反转”版本(例如 __radd____rpow__),这些版本的行为与纯 Python 中的行为相同。主要的不兼容更改是,现在假定第一个操作数(通常是 __self__)的类型是定义类的类型,而不是依赖用户测试和转换每个操作数的类型。

可以使用 指令 c_api_binop_methods=True 恢复旧的行为。更多详细信息请参见 算术方法

异常值和 noexcept

默认情况下,不是 externcdef 函数会安全地传播 Python 异常。以前,它们需要显式声明具有 异常值 才能防止它们吞没异常。可以使用新的 noexcept 修饰符来声明确实不会引发异常的 cdef 函数。

在现有代码中,您主要应该注意没有声明异常值的 cdef 函数

cdef int spam(int x):
    pass

cdef void silent(int x):
    pass

如果您错误地遗漏了异常值,即该函数应该传播 Python 异常,那么新的行为将为您处理这种情况,并正确传播任何异常。这是 Cython 代码中的一个常见错误,也是更改行为的主要原因。

另一方面,如果您没有声明异常值是因为您希望避免异常从该函数传播出去,那么新的行为将导致生成效率稍低的代码,现在涉及异常检查。为了防止这种情况,您必须显式声明该函数为 noexcept

cdef int spam(int x) noexcept:
    pass

cdef void silent(int x) noexcept:
    pass

对于也是 externcdef 函数的行为保持不变,因为 extern 函数不太可能引发 Python 异常,而更倾向于成为普通的 C 函数。这减轻了此更改对与 C 库进行交互的代码的影响。

对于声明了显式异常值的任何 cdef 函数(例如,cdef int spam(int x) except -1)的行为也保持不变。

这里有一个在使用 nogil 函数且隐式异常规范为 except * 时容易遇到的性能陷阱。这种情况最常发生在返回值类型为 void 时(但原则上适用于大多数非数值返回值类型)。在这种情况下,Cython 被迫在每次调用后短暂地重新获取 GIL 来检查异常状态。为了避免这种开销,要么将签名更改为 noexcept(如果您已确定这样做是合适的),要么改为返回一个 int,让 Cython 使用 int 作为错误标志(默认情况下,-1 会触发异常检查)。

注意

可以通过将 legacy_implicit_noexcept 编译器指令 设置为 True 来启用不默认传播异常的不安全旧行为。

注解类型

Cython 3 在识别注解中的类型方面取得了重大改进,阅读 纯 Python 教程 了解一些改进非常值得。

一个值得注意的向后不兼容的更改是,x: int 现在被类型化为 x 是一个精确的 Python int(Cython 0.29 会接受任何 Python 对象作为 x),除非语言级别显式设置为 2。为了减轻影响,Cython 3.0 在 Python 2.x 下仍然接受 Python intlong 值。

您可能会遇到的一个潜在问题是,像 typing.List 这样的类型现在在注解中被理解(以前它们被忽略),并且被解释为表示精确 list。这比 PEP-484 中指定的解释更严格,PEP-484 也允许子类。

为了更轻松地处理您的类型注解解释与 Cython 解释不同的情况,Cython 3 现在支持在每个类或每个函数级别设置 annotation_typing 指令

C++ 后缀递增/递减运算符

Cython 3 区分前/后缀递增和前/后缀递减运算符(Cython 0.29 将两者都实现为前(递增/递减)运算符)。这只有在使用 cython.operator.postdecrement / cython.operator.postincrement 时才会产生影响。当遇到错误时,需要添加相应的运算符

cdef cppclass Example:
    Example operator++(int)
    Example operator--(int)

C++ 中的公共声明

在 C++ 模式下,公共声明在 Cython 3 中使用 extern "C++" 导出为 C++ API。可以通过使用 CYTHON_EXTERN_C 宏设置导出关键字来更改此行为,以允许 Cython 模块在 C++ 中实现,但可以从 C 调用。

** 幂运算符

Cython 3 已更改幂运算符的行为,使其更像 Python。结果是

  1. a**b 两个整数可能返回浮点类型,

  2. a**b 一个或多个非复数浮点数可能返回复数。

可以通过将 cpow 编译指令 设置为 True 来恢复旧的行为。

DEF / IF 的弃用

条件编译功能 已被弃用,不应再在新的代码中使用。预计在未来的某个版本中将其移除。

DEF 的用法应替换为

  • 全局 cdef 常量

  • 全局枚举(C 或 Python)

  • C 宏,例如在 逐字 C 代码 中定义

  • 通常的 Python 机制,用于在模块之间共享值和用法

IF 的用法应替换为

  • 运行时条件和条件 Python 导入(即通常的 Python 模式)

  • 从 Cython extern 结构定义中省略未使用的 C 结构字段名称(它不必完整)

  • 在不同的 Cython 名称下重新定义 extern 结构类型,使用不同的(例如版本/平台相关)属性,但使用 相同的 cname 字符串

  • 将可选的(非平凡的)功能分离到可选的 Cython 模块中,并在需要时导入/使用它们(使用常规的运行时 Python 导入)

  • 代码生成,作为最后的手段