公共 Cython API#

截至 2020 年 4 月,SciPy 中的以下模块通过公共 cdef Cython API 声明公开功能

  • scipy.linalg.cython_blas

  • scipy.linalg.cython_lapack

  • scipy.optimize.cython_optimize

  • scipy.special.cython_special

这使用了Cython 的声明共享特性,其中共享的 cdef 项在 *.pxd 文件中声明,这些文件与二进制 SciPy 安装中的相应 DLL/SO 文件一起分发。

应用程序二进制接口#

然而,在 SciPy 中使用这些特性需要 SciPy 贡献者在维护应用程序二进制接口 (ABI) 稳定性方面格外小心。这类似于用 C 开发库,与纯 Python 中向后兼容性的工作方式不同。

与 Python 的主要区别在于,当用户编写的代码被编译时,会使用头文件 .pxd 文件中的声明,但它们还必须与用户代码被导入时 SciPy 中可用的内容相匹配。

用户代码可以使用一个版本的 SciPy 进行编译,并且编译后的二进制文件(使用 .pxd 文件中声明的二进制接口)可以与系统上安装的不同 SciPy 版本一起使用。如果接口不兼容,则会引发异常,或者发生运行时内存损坏和崩溃。

在导入时,Cython 会检查已安装的 SciPy SO/DLL 文件中的函数签名是否与用户在编译期间使用的 .pxd 文件中的签名匹配,如果不匹配,则会引发 Python 异常。如果 SciPy 代码结构正确(见下文),则此检查仅对用户代码中实际导入的函数执行。

我们依赖此特性来提供运行时安全检查,这使用户更容易通过 Python 异常检测到不兼容的 SciPy 版本,而不是难以追踪的运行时崩溃。

ABI 稳定性目标#

SciPy 旨在在 Cython 代码中保持 ABI 稳定性,其含义如下

通过使用一个版本的 SciPy 编译用户源代码生成的二进制文件,与可以使用该源代码编译的任何其他 SciPy 版本兼容。

尝试在运行时使用不兼容版本的 SciPy 将在用户模块导入时导致 Python 异常。

尝试在编译时使用不兼容版本的 SciPy 将导致 Cython 错误。

这意味着用户可以使用任何兼容版本的 SciPy 来编译二进制文件,而无需关注 ABI,即

ABI 兼容性 = API 兼容性

Cython API 的向后/向前兼容性将使用与 Python API 类似的弃用/删除策略进行处理,请参阅弃用

在 SciPy 中实现 ABI 稳定性#

以下在 SciPy 中开发 Cython API 的规则对于维持上述 ABI 稳定性目标是必要的

  • 允许添加新的 cdef 声明(函数、结构体、类型等)。

  • 允许删除 cdef 声明,但应遵循一般的弃用/删除策略。

  • 可以更改函数的 cdef 声明。

    但是,更改会导致向后不兼容的 API 更改,这会破坏任何使用已更改签名的代码,并且应遵循一般的弃用/删除策略。

  • 任何其他内容(例如 structenum 和类型)的 cdef 声明是最终的。一旦声明在已发布的 SciPy 版本的公共 Cython API 中公开,就不得更改

    如果需要更改,则需要通过添加具有不同名称的新声明并删除旧声明来进行。

  • 公共 API 中不允许使用 cdef 类(待定:cdef 类的向后兼容性需要更多研究,但在我们不确定时不能允许使用)

  • 对于每个公共 API 模块(如 scipy.linalg.cython_blas),请使用单个接口 .pxd 声明文件。

    公共接口声明文件不应包含 cimport 语句。如果包含,Cython 的签名检查将检查所有 cimport 的函数,而不仅仅是用户代码使用的函数,因此更改其中一个函数会破坏整个 API。

  • 如果数据结构是必要的,请首选公共 API 中的不透明结构体。接口声明不应包含任何结构体成员的声明。数据结构的分配、释放和属性访问应使用函数完成。

弃用公共 Cython API#

要弃用公共 Cython API 函数,例如

# scipy/something/foo.pxd
cdef public int somefunc()

# scipy/something/foo.pyx
cdef public int somefunc():
    return 42

您可以使用 scipy._lib.deprecation.deprecate_cython_api 函数在相应的 .pyx 文件的末尾执行弃用

# scipy/something/foo.pyx
cdef public int somefunc():
    return 42

from scipy._lib.deprecation import deprecate_cython_api
import scipy.something.foo as mod
deprecate_cython_api(mod, "somefunc", new_name="scipy.something.newfunc",
                     message="Deprecated in Scipy 1.5.0")
del deprecate_cython_api, mod

之后,cimport somefunc 的 Cython 模块将在导入时发出 DeprecationWarning

没有办法弃用 Cython 数据结构和类型。但是,在删除 API 中使用它们的所有函数后,可以删除它们,并经过弃用周期。

可以通过在顶层发出 DeprecationWarning 来弃用整个 Cython 模块,类似于 Python 模块。