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。