Cython 既是一个优化的静态编译器,也是一个 Python 的超集的编程语言的名称。作为
编译器,它可以使用 Python/C API 执行源到源的编译,把本地 Python 代码及其 Cython 方言
编译为 Python C 扩展。它允许你结合 Python 和 C 的威力,而不需要手动处理 Python/C API。
1.Cython 作为源码编译器
对于使用 Cython 创建的扩展,你将获得的主要优势是使用它提供的语言超集。总之,
你可以利用源到源的编译,使用纯 Python 代码创建扩展。这是 Cython 最简单的方法,因
为它几乎不需要对代码进行任何修改,就可以显著的提升性能,并且开发成本也非常低。
Cython 提供了一个简单实用的 cythonize 函数,允许你轻松地将编译过程与
distutils 或 setuptools 集成。假设我们想把一个纯的 Python 实现的 fibonacci()函
数编译成一个 C 扩展。如果它位于 fibonacci 模块中,最小的 setup.py 脚本,如下所示:
from setuptools import setup
from Cython.Build import cythonize
setup(
name='fibonacci',
ext_modules=cythonize(['fibonacci.py'])
)
Cython 用作 Python 语言的源代码编译工具有另一个好处。源到源编译到扩展可以是源
分发安装过程的完全可选部分。如果需要安装软件包的环境没有 Cython 或任何其他构建前
提条件,则可以将其安装为普通的纯 Python 包。用户不需要关注以这种方式分发的代码有
任何功能性的行为差异。
分发使用 Cython 构建的扩展的常见方法是打包 Python/Cython 源以及从这些源文件生
成中的 C 代码。这样,根据当前构建前提条件,包可以以 3 种不同的方式安装。
● 如果安装环境没有可用的 Cython,则扩展 C 代码是从提供的 Python/Cython 源中生成。
● 如果 Cython 不可用,但有可用的构建前提条件(C 编译器,Python/C API 头),扩
展是从分散式的预生成的 C 文件中构建。
● 如果前面的先决条件都不可用,并且扩展是从纯 Python 源创建的,则模块将像普
通 Python 代码一样安装,并跳过编译步骤。
注意,包含生成的 C 文件以及 Cython 源文件,这是 Cython 文档中推荐的分发 Cython 扩展
的方式。文档中还提到,默认情况下应该禁用 Cython 编译,因为用户在他的环境中可能没有所
需的 Cython 版本,这可能会导致意想不到的编译问题。不过,随着环境隔离的出现,现今这似
乎是一个不太令人担忧的问题。此外,Cython 是一个有效的 Python 包,它在 PyPI 上可用,因此
你可以很容易地定义特定版本的项目依赖。包含这样一个先决条件,无疑是一个有严重影响的决
定,应该非常仔细地考虑。更安全的解决方案是利用 setuptools 包中 extras_require 特
性的强大功能,并允许用户决定是否要使用具有特定环境变量的 Cython,如下所示:
import os
from distutils.core import setup
from distutils.extension import Extension
try:
# 只有当 Cython 可用时
# cython 源到源的编译才可以使用
import Cython
# 并且特定的环境变量明确说明
# 使用 Cython 生成 c 源码
USE_CYTHON = bool(os.environ.get("USE_CYTHON"))
except ImportError:
USE_CYTHON = False
ext = '.pyx' if USE_CYTHON else '.c'
extensions = [Extension("fibonacci", ["fibonacci"+ext])]
if USE_CYTHON:
from Cython.Build import cythonize
extensions = cythonize(extensions)
setup(
name='fibonacci',
ext_modules=extensions,
extras_require={
# 通过'[with-cython]'这个特性
# 当包被安装时
可以设置特定的 Cython 的版本的依赖
'cython': ['cython==0.23.4']
}
)
在安装包时,pip 安装工具支持 extras 选项,该选项通过向包名称添加[extra-name]
后缀进行安装。对于上述示例,从本地源安装时,可以使用以下命令启用可选的 Cython 依
赖与编译器:
$ USE_CYTHON=1 pip install .[with-cython]
2.Cython 作为一门语言
Cython 不仅是一个编译器,而且也是一个 Python 语言的超集。超集意味着任何有效的
Python 代码是允许的,并且它还具有一些额外的特性,如支持调用 C 函数或声明 C 类型的
变量和类属性。所以任何用 Python 编写的代码都可以用作为 Cython 语言使用。这解释了
为什么可以通过 Cython 编译器很容易地将普通的 Python 模块编译到 C。
但我们不会止步于这个简单的事实。我们引用的 fibonacci()函数也是 Python 超集
中的扩展的有效代码,我们将尝试做一点改进。我们不会对函数设计进行实质的优化,只
是做一些小的改动,使用 Cython 编写,我们就可以从中受益。
Cython 源文件使用不同的文件扩展名。它的扩展名是.pyx 而不是.py。假设我们仍
然想要实现我们的斐波纳契数列。fibonacci.pyx 的内容看起来可能是像这样的:
"""提供斐波纳契数列函数的 Cython 模块"""
def fibonacci(unsigned int n):
"""递归计算返回斐波那契数列的第 n 项"""
if n < 2:
return n
else:
return fibonacci(n - 1) + fibonacci(n - 2)
正如你可以看到的,唯一真正改变的是 fibonacci()函数的签名。由于 Cython 中的
可选静态类型,我们可以将 n 参数声明为 unsigned int,这会稍微改进我们函数的工作
方式。此外,它比以前用手写扩展时做的更多。如果 Cython 函数的参数声明为静态类型,
那么扩展将通过抛出合适的异常来自动处理转换和溢出的异常,如下所示:
>>> from fibonacci import fibonacci
>>> fibonacci(5)
5
>>> fibonacci(-1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "fibonacci.pyx", line 21, in fibonacci.fibonacci (fibonacci.c:704)
OverflowError: can't convert negative value to unsigned int
>>> fibonacci(10 ** 10)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "fibonacci.pyx", line 21, in fibonacci.fibonacci (fibonacci.c:704)
OverflowError: value too large to convert to unsigned int
我们已经知道 Cython 只能进行源到源的编译,而生成的代码使用相同的 Python/C API,
我们在手动为扩展编写 C 代码时也使用到了这些 API。注意 fibonacci()是一个递归函
数,所以它经常调用它自己。这意味着虽然我们为输入参数声明了一个静态类型,但在递
归调用期间,它会像对待其他 Python 函数一样对待自己。因此,n-1 和 n-2 将被打包成
Python 对象,然后传递到 fibonacci()的内部实现,该实现会再次返回 unsigned int
类型。这将会一次又一次地重复发生,直到我们达到最终的递归深度。但涉及到比真正需
要更多的参数处理时,就可能出现问题。
我们可以通过将更多的工作委托给一个不知道 Python 结构的纯 C 函数来减少 Python
函数调用和参数处理的开销。之前,我们在使用纯 C 创建 C 扩展时这样做过,同样,也可
以在 Cython 中这样做。我们可以使用 cdef 关键字声明接受和返回 C 类型的 C 风格函数:
cdef long long fibonacci_cc(unsigned int n):
if n < 2:
return n
else:
return fibonacci_cc(n - 1) + fibonacci_cc(n - 2)
def fibonacci(unsigned int n):
""" 递归计算返回斐波那契数列的第 n 项
"""
return fibonacci_cc(n)
我们可以进一步优化。有了一个简单的 C 示例,我们终于展示了如何在调用我们的纯
C 函数期间释放 GIL,因此这个扩展对于多线程应用程序更加友好。在前面的例子中,我
们使用了来自 Python/C API 头文件中的 Py_BEGIN_ALLOW_THREADS 和 Py_END_ALLOW_
THREADS 预处理器宏,便于让 Python 调用这些代码。Cython 语法更短,更容易记住。在
代码中使用简单的 with nogil 语句可以释放 GIL,如下所示:
def fibonacci(unsigned int n):
""" 递归计算返回斐波那契数列的第 n 项 """
with nogil:
result = fibonacci_cc(n)
return fibonacci_cc(n)
你也可以标记整个 C 风格函数是无 GIL 的安全的调用:
cdef long long fibonacci_cc(unsigned int n) nogil:
if n < 2:
return n
else:
return fibonacci_cc(n - 1) + fibonacci_cc(n - 2)
重要的是要知道这样的函数不能有 Python 对象作为参数或返回类型。每当标记为
nogil 的函数需要执行任何 Python/C API 调用时,它必须使用 with gil 语句获取 GIL。