Cython 3.0 文档
来源: docs.cython.org/en/latest/
入门
- Cython - 概述
- 安装 Cython
- 构建 Cython 代码
- 使用 distutils 构建一个 Cython 模块
- 使用 Jupyter 笔记本
- 使用 Sage 笔记本
- 使用更快静态类型代码
- 指定变量类型
- 指定函数类型
- 确定所添加类型的位置
Cython - 概述
[Cython] 是一种编程语言,它使 Python 语言的 C 语言扩展与 Python 本身一样简单。它旨在成为 [Python] 语言的超集,为其提供高级,面向对象,函数式和动态编程。它的主要特性是支持任意的静态类型声明作为语言的一部分。源代码被转换为优化过的 C / C ++代码并编译为 Python 扩展模块。这一特性使得程序可以执行非常快速并且能与外部 C 语言库的紧密集成,同时还能是程序员高能保持众所周知的 Python 语言开发效率。
主要的 Python 执行环境通常被称为 CPython,因为它是用 C 语言编写的。其他主要实现使用 Java(Jython [Jython] ),C#(IronPython [IronPython] )和 Python 本身(PyPy [PyPy] )。用 C 和 CPython 编程有助于包装许多通过 C 语言提供接口的外部库。然而,即是是需要在 C 中编写必要的胶水代码这仍然是值得的,特别是对于那些只熟悉 Python 这样的高级语言的程序员而不熟悉像 C 这样的接近底层的语言。
最初基于着名的 Pyrex [Pyrex] ,Cython 项目通过源代码编译器将 Python 代码转换为等效的 C 代码来解决这个问题。此代码在 CPython 运行时环境中执行,但是却以编译后的 C 程序那般速度执行,并且能够直接调用 C 语言库。同时,它保留了 Python 源代码的原始接口,这使得它可以直接使用 Python 语言代码。这些双重特性使 Cython 的这两个主要使用场景成为可能:使用快速二进制模块来扩展 CPython 解释器,以及将 Python 代码与外部 C 库连接。
与此同时 Cython 可以编译(大多数)常规 Python 代码,而且生成的 C 代码通常可以从 Python 和 C 类型的任意静态类型声明中获得主要(并且有时很惊人)的速度上的提升。这些允许 Cython 将 C 语义分配给代码的一部分,并将它们转换为非常高效率的 C 代码。因此,类型声明可用于两个目的:将代码段从动态 Python 语义转换为静态和快速 C 语义,还用于直接操作外部库中定义的类型。因此,Cython 将这两个世界合并为一种非常广泛适用的编程语言。
[Cython]](#id1) | G. Ewing,R。W. Bradshaw,S。Behnel,D。S. Seljebotn 等人,Cython 编译器,
cython.org/
。
[IronPython] | Jim Hugunin 等人,archive.codeplex.com/?p=IronPython
。
[Jython] | J. Huginin,B。Warsaw,F.Bock,et al。,Jython:Python for the Java platform,www.jython.org
。
[PyPy] | PyPy Group,PyPy:用 Python 编写的 Python 实现,pypy.org/
。
[派热克斯] | G. Ewing,Pyrex:Python 的 C-Extensions,www.cosc.canterbury.ac.nz/greg.ewing/python/Pyrex/
[Python] | G. van Rossum 等人,Python 编程语言,www.python.org/
。
安装 Cython
许多学术性 Python 发行版,例如 Anaconda [Anaconda] ,Enthought Canopy [Canopy] 和 Sage [Sage] ,都自带有 Cython 并且不需要设置。但请注意,如果您的发行版发布的 Cython 版本太旧,您仍然可以使用下面的说明更新 Cython。除非脚注另有说明,否则本教程中的所有内容都应与 Cython 0.11.2 及更高版本一起使用。
与大多数 Python 软件不同,Cython 需要在系统上存在 C 编译器。获取 C 编译器的细节因使用的系统而异:
- Linux 通常自带 GNU C 编译器(gcc),或通过包系统轻松获得。例如,在 Ubuntu 或 Debian 上,输入命令
sudo apt-get install build-essential
将获取您需要的所有内容。- Mac OS X 要检索 gcc,一个选项是安装 Apple 的 XCode,可以从 Mac OS X 的安装 DVD 或
developer.apple.com /
获得。- Windows 一个流行的选择是使用开源 MinGW(Windows 的 gcc 分发版)。有关手动设置 MinGW 的说明,请参阅附录.Enthought Canopy 和 Python(x,y)捆绑 MinGW,另一个选择是使用 Microsoft 的 Visual C.然后必须使用与编译安装的 Python 相同的版本。
安装 Cython 的最简单方法是使用pip
:
pip install Cython
最新的 Cython 版本始终可以从 cython.org/
下载。解压缩 tarball 或 zip 文件,输入目录,然后运行:
python setup.py install
对于一次性构建,例如对于 CI /测试。当所在平台 PyPI 并没有提供轮子包(wheel package)时。安装未编译(较慢)的 Cython 版本比编译整个源代码来安装要快得多。安装且不编译 Cython 的命令:
pip install Cython --install-option="--no-cython-compile"
[python]
docs.anaconda.com/anaconda/
[Canopy]www.enthought.com/product/canopy/
[Sage] W. Stein 等,Sage Mathematics Software,www.sagemath.org/
构建 Cython 代码
与 Python 不同,Cython 代码必须编译。这发生在两个阶段:
.pyx
文件由 Cython 编译为.c
文件,它含有 Python 扩展模块的代码。.c
文件由 C 编译器编译为.so
文件(或 Windows 上的.pyd
),可直接被import
引入到 一个 Python 会话中. Distutils 或 setuptools 负责这部分。虽然 Cython 可以在某些情况下为你调用它们。
要完全理解 Cython + distutils / setuptools 构建过程,可能需要阅读更多关于分发 Python 模块的内容。
有几种方法可以构建 Cython 代码:
- 写一个 distutils / setuptools
setup.py
。这是正常和推荐的方式。- 使用 Pyximport,导入 Cython
.pyx
文件就像它们是.py
文件一样(使用 distutils 在后台编译和构建)。这种方法比编写setup.py
更容易,但不是很灵活。因此,如果您需要某些编译选项,则需要编写setup.py
。- 手动运行
cython
命令行工具程序来将.pyx
文件编译生成.c
文件,然后手动将.c
文件编译成适合从 Python 导入的共享库或 DLL。(这些手动步骤主要用于调试和实验。)- 使用 [Jupyter] 笔记本或 [Sage] 笔记本,两者都允许 Cython 代码内联。这是开始编写 Cython 代码并运行它的最简单方法。
目前,使用 distutils 或 setuptools 是构建和分发 Cython 文件的最常用方式。其他方法在参考手册的 源文件和编译 部分中有更详细的描述。
使用 distutils 构建 一个 Cython 模块
想象一下文件hello.pyx
中的一个简单的“hello world”脚本:
def say_hello_to(name):
print("Hello %s!" % name)
以下是相应的setup.py
脚本:
from distutils.core import setup
from Cython.Build import cythonize
setup(name='Hello world app',
ext_modules=cythonize("hello.pyx"))
要构建,请运行python setup.py build_ext --inplace
。然后只需启动一个 Python 会话并执行from hello import say_hello_to
并根据需要使用导入的函数。
如果您使用 setuptools 而不是 distutils,则需要注意,运行python setup.py install
时的默认操作是创建一个压缩的egg
文件,当您尝试从依赖包中使用它们时,这些文件无法与pxd
文件一起用于pxd
文件。为防止这种情况,请在setup()
的参数中包含zip_safe=False
。
使用 Jupyter 笔记本来构建
Cython 可以通过 Web 浏览器通过 Jupyter 笔记本方便地和交互式地使用。要安装 Jupyter 笔记本,例如进入 virtualenv,使用 pip:
(venv)$ pip install jupyter
(venv)$ jupyter notebook
要启用对 Cython 编译的支持,请按照 安装指南 中的说明安装 Cython,并从 Jupyter 笔记本中加载Cython
扩展:
%load_ext Cython
然后,使用%%cython
标记为单元格添加前缀以进行编译:
%%cython
cdef int a = 0
for i in range(10):
a += i
print(a)
您可以通过传递--annotate
选项来显示 Cython 的代码分析:
%%cython --annotate
...
有关%%cython
魔法参数的更多信息,请参阅 使用 Jupyter 笔记本 进行编译。
使用 Sage 笔记本
对于 Sage 数学发行版的用户,Sage 笔记本允许通过在单元格顶部键入%cython
并进行评估来透明地编辑和编译 Cython 代码。导入到运行会话中的 Cython 单元格中定义的变量和函数。
使用更快静态类型代码
Cython 是一个 Python 编译器。这意味着它可以在不进行更改的情况下编译普通的 Python 代码(除了一些尚未支持的语言功能的一些明显例外,请参阅 Cython 限制 )。但是,对于影响性能的关键代码,添加静态类型声明通常很有用,因为它们将允许 Cython 脱离 Python 代码的动态特性并生成更简单,更快速的 C 代码 - 有时会快几个数量级。
但必须注意,类型声明可以使源代码更加冗长,从而降低可读性。因此,不鼓励在没有充分理由的情况下使用它们,例如基准测试证明它们在性能关键部分确实使代码更快。通常情况下,正确使用的一些类型会有很长的路要走。
所有 C 类型都可用于类型声明:整数和浮点类型,复数,结构,联合和指针类型。 Cython 可以在分配时自动和正确地转换类型。这还包括 Python 的任意大小整数类型,其中转换为 C 类型时溢出的值将在运行时引发 Python OverflowError
。 (但是,在进行算术运算时,它不会检查溢出。)在这种情况下,生成的 C 代码将正确且安全地处理 C 类型的平台相关大小。
类型通过 cdef 关键字声明。
指定变量类型
考虑以下纯 Python 代码:
def f(x):
return x ** 2 - x
def integrate_f(a, b, N):
s = 0
dx = (b - a) / N
for i in range(N):
s += f(a + i * dx)
return s * dx
在 Cython 中简单地编译它只能提供 35%的加速。这比没有好,但添加一些静态类型可以产生更大的差异。
使用其他类型声明,这可能如下所示:
def f(double x):
return x ** 2 - x
def integrate_f(double a, double b, int N):
cdef int i
cdef double s, dx
s = 0
dx = (b - a) / N
for i in range(N):
s += f(a + i * dx)
return s * dx
由于迭代器变量i
是用 C 语法定义的,因此 for 循环将被编译为纯 C 代码。键入a
,s
和dx
非常重要,因为它们涉及 for 循环中的算术运算;键入b
和N
会产生较小的差异,但在这种情况下,要保持一致并输入整个函数并不是一件额外需要做的工作。
这导致纯 Python 版本的速度提高了 4 倍。
指定函数类型
Python 函数调用可能很昂贵 - 在 Cython 中是双倍的,因为可能需要转换到 Python 对象和从 Python 对象进行调用。在上面的示例中,假设参数在 f()
内部和调用它时都是 C 中的 double 类型,但是float
类型必须为参数构造一个 Python 对象才能传递它。
因此,Cython 提供了声明 C 风格函数的语法,即 cdef 关键字:
cdef double f(double x) except? -2:
return x ** 2 - x
通常应该添加某种形式的 except-modifier,否则 Cython 将无法传播函数(或它调用的函数)中引发的异常。 except? -2
表示如果返回-2
将检查错误(尽管?
表示-2
也可以用作有效返回值)。或者,较慢的except *
始终是安全的。如果函数返回 Python 对象或者保证在函数调用中不会引发异常,则可以省略 except 子句。
cdef 的副作用是 Python 空间不再提供该函数,因为 Python 不知道如何调用它。也无法再在运行时更改f()
。
使用cpdef
关键字而不是cdef
,还会创建一个 Python 包装器,以便该函数可以从 Cython(快速,直接传递类型值)和 Python(包装 Python 对象中的值)中获得。事实上,cpdef
不仅提供了一个 Python 包装器,它还安装了逻辑,允许方法被 python 方法覆盖,即使从 cython 中调用也是如此。与cdef
方法相比,这确实增加了很小的开销。
加速:超过纯 Python 的 150 倍。
确定所添加类型的位置
因为静态类型通常是提高速度的关键所在,所以初学者往往倾向于所见之处指定各种变量的类型。这降低了可读性和灵活性,甚至可以降低速度(例如,通过添加不必要的类型检查,转换或缓慢的缓冲区解包)。另一方面,忘记键入关键循环变量很容易破坏性能。帮助完成此任务的两个基本工具是分析和注释。分析应该是任何优化工作的第一步,并且可以告诉您在哪里花费时间。 Cython 的注释可以告诉你为什么你的代码需要时间。
使用-a
开关到cython
命令行程序(或跟随 Sage 笔记本的链接)会导致 Cython 代码的 HTML 报告与生成的 C 代码交错。线条根据“类型”的级别着色 - 白线转换为纯 C,而需要 Python C-API 的线条为黄色(因为它们转化为更多的 C-API 交互,因此更暗)。转换为 C 代码的行在前面有一个加号(+
),可以单击以显示生成的代码。
当优化速度函数以及确定何时 释放 GIL 时,此报告非常有用:通常,nogil
块可能只包含“白色”代码。
请注意,Cython 根据其赋值(包括作为循环变量目标)推断出局部变量的类型,这也可以减少在任何地方显式指定类型的需要。例如,将dx
声明为 double 类型是不必要的,就像在最后一个版本中声明s
的类型一样(其中f
的返回类型已知为 C 语言的 double 类型。)一个值得注意的例外然而,算术表达式中使用的是整数类型,因为 Cython 无法确保不会发生溢出(因此在需要 Python 的 bignums 时会回退到object
)。要允许推断 C 整数类型,请将infer_types
指令 设置为True
。对于熟悉此语言功能的读者,此指令的工作类似于 C ++中的auto
关键字。减少输入所有内容的需求可能会有很大帮助,但也可能导致意外。特别是如果一个人不熟悉 c 类型的算术表达式。这些可以在中找到的快速概述。_
Tutorials
- 基础教程
- Cython 的基础知识
- Cython Hello World
- 斐波那契乐趣
- Primes
- 使用 C ++进行推广
- 语言详细信息
- 调用 C 函数
- 动态链接
- 外部声明
- 命名参数
- 使用 C 库
- 定义外部声明
- 编写包装类
- 内存管理
- 编译和链接
- 扩展类型(又名.cdef 类)
- pxd 文件
- 警告
- 分析
- Cython Profiling Basics
- 分析教程
- Unicode 和传递字符串
- Cython 代码中的 Python 字符串类型
- 字符串文字
- 关于 C 字符串的一般说明
- 传递字节串
- 接受 Python 代码中的字符串
- 处理“const”
- 将字节解码为文本
- 将文本编码为字节
- C ++字符串
- 自动编码和解码
- 源代码编码
- 单字节和字符
- 窄版本构建
- 迭代
- Windows 和宽字符 API
- 内存分配
- 纯 Python 模式
- 增加.pxd
- 魔法属性
- 提示与技巧
- 使用 NumPy
- 添加类型
- 高效索引
- 进一步调整索引
- 更通用的代码
- 使用 Python 数组
- 内存视图的安全使用
- 零开销,不安全访问原始 C 指针
- 克隆,扩展阵列
- API 参考
- 进一步阅读
- 相关工作
- 附录:在 Windows 上安装 MinGW
基础教程
原文:
docs.cython.org/en/latest/src/tutorial/cython_tutorial.html
Cython 的基础知识
Cython 的基本特性可归纳如下:Cython 是具有 C 数据类型的 Python。
Cython 是 Python:几乎任何 Python 代码都是有效的 Cython 代码。 (有一些 限制 ,但这种近似现在将起作用。)Cython 编译器将其转换为 C 代码,它对 Python / C API 进行等效调用。
但 Cython 远不止于此,因为参数和变量可以声明为具有 C 数据类型。操作 Python 值和 C 值的代码可以自由混合,只要有可能就会自动进行转换。 Python 操作的引用计数维护和错误检查也是自动的,并且即使在操作 C 数据的过程中,您也可以使用 Python 的异常处理工具(包括 try-except 和 try-finally 语句)的全部功能。
Cython Hello World
由于 Cython 几乎可以接受任何有效的 python 源文件,因此入门中最困难的事情之一就是弄清楚如何编译扩展。
所以让我们从规范的 python hello 世界开始:
print("Hello World")
将此代码保存在名为helloworld.pyx
的文件中。现在我们需要创建setup.py
,它就像一个 python Makefile(有关更多信息,请参阅 源文件和编译 )。你的setup.py
看起来像:
from distutils.core import setup
from Cython.Build import cythonize
setup(
ext_modules = cythonize("helloworld.pyx")
)
要使用它来构建您的 Cython 文件,请使用命令行选项:
$ python setup.py build_ext --inplace
这将在您的本地目录中将文件保留在 unix 中的helloworld.so
或 Windows 中的helloworld.pyd
中。现在使用这个文件:启动 python 解释器并简单地导入它就好像它是一个普通的 python 模块:
>>> import helloworld
Hello World
恭喜!您现在知道如何构建 Cython 扩展。但到目前为止,这个例子并没有真正让人感觉为什么会想要使用 Cython,所以让我们创建一个更现实的例子。
pyximport
:面向开发人员的 Cython 编译
如果您的模块不需要任何额外的 C 库或特殊的构建设置,那么您可以使用最初由 Paul Prescod 开发的 pyximport 模块在导入时直接加载.pyx 文件,而无需每个都运行setup.py
文件你改变代码的时候了。它随 Cython 一起发货和安装,可以像这样使用:
>>> import pyximport; pyximport.install()
>>> import helloworld
Hello World
Pyximport 模块还具有对普通 Python 模块的实验性编译支持。这允许您在 Python 导入的每个.pyx 和.py 模块上自动运行 Cython,包括标准库和已安装的软件包。 Cython 仍然无法编译很多 Python 模块,在这种情况下,导入机制将回退到加载 Python 源模块。 .py 导入机制安装如下:
>>> pyximport.install(pyimport=True)
请注意,建议不要让 Pyximport 在最终用户端构建代码,因为它会挂钩到他们的导入系统。满足最终用户的最佳方式是以轮包装格式提供预先构建的二进制包。
斐波那契乐趣
从官方 Python 教程中,一个简单的 fibonacci 函数定义为:
from __future__ import print_function
def fib(n):
"""Print the Fibonacci series up to n."""
a, b = 0, 1
while b < n:
print(b, end=' ')
a, b = b, a + b
print()
现在按照 Hello World 示例的步骤,我们首先将文件重命名为 <cite>.pyx</cite> 扩展名,让我们说fib.pyx
,然后我们创建setup.py
文件。使用为 Hello World 示例创建的文件,您需要更改的是 Cython 文件名的名称,以及生成的模块名称,我们这样做:
from distutils.core import setup
from Cython.Build import cythonize
setup(
ext_modules=cythonize("fib.pyx"),
)
使用与 helloworld.pyx 相同的命令构建扩展:
$ python setup.py build_ext --inplace
并使用新的扩展名:
>>> import fib
>>> fib.fib(2000)
1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597
Primes
这是一个小例子,展示了一些可以做的事情。这是查找素数的例程。你告诉它你想要多少素数,并将它们作为 Python 列表返回。
primes.pyx
:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
def primes(int nb_primes):
cdef int n, i, len_p
cdef int p[1000]
if nb_primes > 1000:
nb_primes = 1000
len_p = 0 # The current number of elements in p.
n = 2
while len_p < nb_primes:
# Is n prime?
for i in p[:len_p]:
if n % i == 0:
break
# If no break occurred in the loop, we have a prime.
else:
p[len_p] = n
len_p += 1
n += 1
# Let's return the result in a python list:
result_as_list = [prime for prime in p[:len_p]]
return result_as_list
|
您将看到它的开始就像普通的 Python 函数定义一样,除了参数nb_primes
被声明为int
类型。这意味着传递的对象将被转换为 C 整数(如果不能,则会引发TypeError.
)。
现在,让我们深入研究该函数的核心:
cdef int n, i, len_p
cdef int p[1000]
第 2 行和第 3 行使用cdef
语句定义一些本地 C 变量。结果在处理期间存储在 C 数组p
中,并将在末尾复制到 Python 列表中(第 22 行)。
注意
您不能以这种方式创建非常大的数组,因为它们是在 C 函数调用堆栈上分配的,这是一个相当珍贵和稀缺的资源。要请求更大的数组,甚至是只在运行时知道长度的数组,您可以学习如何有效地使用 C 内存分配 , Python 数组 或 NumPy 阵列 与 Cython。
if nb_primes > 1000:
nb_primes = 1000
与在 C 中一样,声明静态数组需要在编译时知道大小。我们确保用户没有设置大于 1000 的值(或者我们会有一个分段错误,就像在 C 中一样)。
len_p = 0 # The number of elements in p
n = 2
while len_p < nb_primes:
第 7-9 行设置了一个循环,它将测试候选数字的完整性,直到找到所需的素数。
# Is n prime?
for i in p[:len_p]:
if n % i == 0:
break
尝试将候选人除以迄今为止发现的所有素数的第 11-12 行特别令人感兴趣。因为没有引用 Python 对象,所以循环完全转换为 C 代码,因此运行速度非常快。你会注意到我们迭代p
C 数组的方式。
for i in p[:len_p]:
循环被转换为快速 C 循环,就像迭代 Python 列表或 NumPy 数组一样。如果不使用[:len_p]
对 C 数组进行切片,则 Cython 将循环遍历数组的 1000 个元素。
# If no break occurred in the loop
else:
p[len_p] = n
len_p += 1
n += 1
如果没有发生中断,则意味着我们找到了一个素数,并且将执行else
行 16 之后的代码块。我们添加了p
找到的素数。如果你发现在 for 循环奇怪之后有else
,只要知道它是 Python 语言鲜为人知的特性,并且 Cython 会以 C 速度为你执行它。如果 for-else 语法让您感到困惑,请参阅这篇优秀的博客文章。
# Let's put the result in a python list:
result_as_list = [prime for prime in p[:len_p]]
return result_as_list
在第 22 行,在返回结果之前,我们需要将 C 数组复制到 Python 列表中,因为 Python 无法读取 C 数组。 Cython 可以自动将许多 C 类型转换为 Python 类型,如 类型转换 的文档中所述,因此我们可以使用简单的列表解析来复制 C int
值为 Python int
对象的 Python 列表,Cython 在此过程中自动创建。您也可以在 C 数组上手动迭代并使用result_as_list.append(prime)
,结果将是相同的。
您会注意到我们声明的 Python 列表与 Python 中的完全相同。因为变量result_as_list
尚未使用类型显式声明,所以假定它包含一个 Python 对象,并且从赋值中,Cython 也知道确切的类型是 Python 列表。
最后,在第 18 行,普通的 Python return 语句返回结果列表。
使用 Cython 编译器编译 primes.pyx 会生成一个扩展模块,我们可以在交互式解释器中尝试如下:
>>> import primes
>>> primes.primes(10)
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
看,它有效!如果您对 Cython 为您节省了多少工作感到好奇,请查看为此模块生成的 C 代码。
Cython 有一种方法可视化与 Python 对象和 Python 的 C-API 进行交互的位置。为此,将annotate=True
参数传递给cythonize()
。它生成一个 HTML 文件。让我们来看看:
如果一行为白色,则表示生成的代码不与 Python 交互,因此将以与普通 C 代码一样快的速度运行。黄色越深,该行中的 Python 交互越多。这些黄色线通常可以在 Python 对象上运行,引发异常,或执行其他类型的高级操作,而不是可以轻松转换为简单快速的 C 代码。函数声明和返回使用 Python 解释器,因此这些行是黄色的。列表推导也是如此,因为它涉及创建 Python 对象。但行if n % i == 0:
,为什么?我们可以检查生成的 C 代码来理解:
我们可以看到一些检查发生。因为 Cython 默认使用 Python 行为,所以语言将在运行时执行除法检查,就像 Python 一样。您可以使用 编译器指令 取消激活这些检查。
现在让我们看看,即使我们有分区检查,我们也获得了提升速度。让我们编写相同的程序,但是 Python 风格:
def primes_python(nb_primes):
p = []
n = 2
while len(p) < nb_primes:
# Is n prime?
for i in p:
if n % i == 0:
break
# If no break occurred in the loop
else:
p.append(n)
n += 1
return p
也可以采用普通的.py
文件并使用 Cython 进行编译。让我们拿primes_python
,将函数名改为primes_python_compiled
并用 Cython 编译(不改变代码)。我们还将文件名更改为example_py_cy.py
,以区别于其他文件。现在setup.py
看起来像这样:
from distutils.core import setup
from Cython.Build import cythonize
setup(
ext_modules=cythonize(['example.pyx', # Cython code file with primes() function
'example_py_cy.py'], # Python code file with primes_python_compiled() function
annotate=True), # enables generation of the html annotation file
)
现在我们可以确保这两个程序输出相同的值:
>>> primes_python(1000) == primes(1000)
True
>>> primes_python_compiled(1000) == primes(1000)
True
现在可以比较速度:
python -m timeit -s 'from example_py import primes_python' 'primes_python(1000)'
10 loops, best of 3: 23 msec per loop
python -m timeit -s 'from example_py_cy import primes_python_compiled' 'primes_python_compiled(1000)'
100 loops, best of 3: 11.9 msec per loop
python -m timeit -s 'from example import primes' 'primes(1000)'
1000 loops, best of 3: 1.65 msec per loop
primes_python
的 cythonize 版本比 Python 版本快 2 倍,而不需要更改单行代码。 Cython 版本比 Python 版本快 13 倍!有什么可以解释这个?
Multiple things:
- 在这个程序中,每行都进行很少的计算。因此 python 解释器的开销非常重要。如果你要在每一行做很多计算,那将会非常不同。以 NumPy 为例。
- 数据位置。使用 C 时,使用 Python 时可能会有更多的内容适合 CPU 缓存。因为 python 中的所有内容都是一个对象,并且每个对象都是作为字典实现的,所以这不是很容易缓存的。
通常加速比在 2x 到 1000x 之间。这取决于你调用 Python 解释器的程度。与往常一样,请记住在各地添加类型之前进行配置添加类型会降低代码的可读性,因此请谨慎使用它们。
使用 C ++ 语言
使用 Cython,也可以利用 C ++语言,特别是 C ++标准库的一部分可以直接从 Cython 代码导入。
让我们看看当使用 C ++标准库中的向量时我们的primes.pyx
变成了什么。
Note
C ++中的 Vector 是一种数据结构,它基于可调整大小的 C 数组实现列表或堆栈。它类似于array
标准库模块中的 Python array
类型。有一种方法<cite>保留</cite>可用,如果你事先知道你要在矢量中放入多少元素,它将避免复制。有关详细信息,请参阅 cppreference 中的此页面。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
# distutils: language=c++
from libcpp.vector cimport vector
def primes(unsigned int nb_primes):
cdef int n, i
cdef vector[int] p
p.reserve(nb_primes) # allocate memory for 'nb_primes' elements.
n = 2
while p.size() < nb_primes: # size() for vectors is similar to len()
for i in p:
if n % i == 0:
break
else:
p.push_back(n) # push_back is similar to append()
n += 1
# Vectors are automatically converted to Python
# lists when converted to Python objects.
return p
|
第一行是编译器指令。它告诉 Cython 将您的代码编译为 C ++。这将允许使用 C ++语言功能和 C ++标准库。请注意,使用 <cite>pyximport</cite> 无法将 Cython 代码编译为 C ++。您应该使用setup.py
或笔记本来运行此示例。
您可以看到向量的 API 类似于 Python 列表的 API,有时可以用作 Cython 中的替代品。
有关在 Cython 中使用 C ++的更多详细信息,请参阅 在 Cython 中使用 C ++。
语言详细信息
有关 Cython 语言的更多信息,请参阅 语言基础知识 。要在数值计算环境中直接使用 Cython,请参阅 类型内存视图 。
调用 C 库函数
本教程简要介绍了从 Cython 代码调用 C 库函数时需要了解的内容。有关使用外部 C 库,包装它们和处理错误的更长更全面的教程,请参阅 使用 C 库 。
为简单起见,让我们从标准 C 库中的函数开始。这不会为您的代码添加任何依赖项,并且它还具有 Cython 已经为您定义了许多此类函数的额外优势。所以你可以直接使用它们。
例如,假设您需要一种低级方法来解析char*
值中的数字。您可以使用stdlib.h
头文件定义的atoi()
功能。这可以按如下方式完成:
from libc.stdlib cimport atoi
cdef parse_charptr_to_py_int(char* s):
assert s is not NULL, "byte string value is NULL"
return atoi(s) # note: atoi() has no error detection!
您可以在 Cython 的源代码包 Cython / Includes / 中找到这些标准 cimport 文件的完整列表。它们存储在.pxd
文件中,这是提供可以在模块之间共享的可重用 Cython 声明的标准方法(参见 在 Cython 模块之间共享声明 )。
Cython 还为 CPython 的 C-API 提供了一整套声明。例如,要在 C 编译时测试您的代码正在编译的 CPython 版本,您可以这样做:
from cpython.version cimport PY_VERSION_HEX
# Python version >= 3.2 final ?
print(PY_VERSION_HEX >= 0x030200F0)
Cython 还提供 C 数学库的声明:
from libc.math cimport sin
cdef double f(double x):
return sin(x * x)
动态链接
libc 数学库的特殊之处在于它在某些类 Unix 系统(如 Linux)上没有默认链接。除了导入声明之外,还必须将构建系统配置为链接到共享库m
。对于 distutils,将它添加到Extension()
设置的libraries
参数就足够了:
from distutils.core import setup
from distutils.extension import Extension
from Cython.Build import cythonize
ext_modules = [
Extension("demo",
sources=["demo.pyx"],
libraries=["m"] # Unix-like specific
)
]
setup(name="Demos",
ext_modules=cythonize(ext_modules))
外部声明
如果要访问 Cython 未提供即用型声明的 C 代码,则必须自行声明。例如,上面的sin()
函数定义如下:
cdef extern from "math.h":
double sin(double x)
这声明了sin()
函数,使其可用于 Cython 代码并指示 Cython 生成包含math.h
头文件的 C 代码。 C 编译器将在编译时在math.h
中看到原始声明,但 Cython 不解析“math.h”并需要单独的定义。
就像数学库中的sin()
函数一样,只要 Cython 生成的模块与共享库或静态库正确链接,就可以声明并调用任何 C 库。
请注意,通过将其声明为cpdef
,可以轻松地从 Cython 模块导出外部 C 函数。这会为它生成一个 Python 包装器并将其添加到模块 dict 中。这是一个 Cython 模块,可以直接访问 Python 代码的 C sin()
函数:
"""
>>> sin(0)
0.0
"""
cdef extern from "math.h":
cpdef double sin(double x)
当此声明出现在属于 Cython 模块的.pxd
文件中时(即具有相同名称的 在 Cython 模块之间共享声明 ),会得到相同的结果。这允许 C 声明在其他 Cython 模块中重用,同时仍然在此特定模块中提供自动生成的 Python 包装器。
命名参数
C 和 Cython 都支持没有参数名称的签名声明,如下所示:
cdef extern from "string.h":
char* strstr(const char*, const char*)
但是,这会阻止 Cython 代码使用关键字参数调用它。因此,最好像这样编写声明:
cdef extern from "string.h":
char* strstr(const char *haystack, const char *needle)
您现在可以清楚地说明两个参数中的哪一个在您的调用中执行了哪些操作,从而避免了任何歧义并且通常使您的代码更具可读性:
cdef extern from "string.h":
char* strstr(const char *haystack, const char *needle)
cdef char* data = "hfvcakdfagbcffvschvxcdfgccbcfhvgcsnfxjh"
cdef char* pos = strstr(needle='akd', haystack=data)
print(pos is not NULL)
请注意,稍后更改现有参数名称是向后不兼容的 API 修改,就像 Python 代码一样。因此,如果您为外部 C 或 C ++函数提供自己的声明,那么通常需要额外的工作来选择其参数的名称。
使用 C 库
除了编写快速代码之外,Cython 的一个主要用例是从 Python 代码调用外部 C 库。由于 Cython 代码编译为 C 代码本身,因此直接在代码中调用 C 函数实际上是微不足道的。下面给出了在 Cython 代码中使用(和包装)外部 C 库的完整示例,包括适当的错误处理和有关为 Python 和 Cython 代码设计合适 API 的注意事项。
想象一下,您需要一种有效的方法来将整数值存储在 FIFO 队列中。由于内存非常重要,并且值实际上来自 C 代码,因此您无法在列表或双端队列中创建和存储 Python int
对象。所以你要注意 C 中的队列实现。
经过一些网络搜索,你会发现 C 算法库 [CAlg] 并决定使用它的双端队列实现。但是,为了使处理更容易,您决定将其包装在可以封装所有内存管理的 Python 扩展类型中。
| [[CAlg]](#id2) | Simon Howard,C 算法库, [`c-algorithms.sourceforge.net/`](http://c-algorithms.sourceforge.net/) |定义外部声明
你可以在这里下载 CAlg 。
队列实现的 C API,在头文件c-algorithms/src/queue.h
中定义,基本上如下所示:
/* queue.h */
typedef struct _Queue Queue;
typedef void *QueueValue;
Queue *queue_new(void);
void queue_free(Queue *queue);
int queue_push_head(Queue *queue, QueueValue data);
QueueValue queue_pop_head(Queue *queue);
QueueValue queue_peek_head(Queue *queue);
int queue_push_tail(Queue *queue, QueueValue data);
QueueValue queue_pop_tail(Queue *queue);
QueueValue queue_peek_tail(Queue *queue);
int queue_is_empty(Queue *queue);
首先,第一步是在.pxd
文件中重新定义 C API,例如cqueue.pxd
:
# cqueue.pxd
cdef extern from "c-algorithms/src/queue.h":
ctypedef struct Queue:
pass
ctypedef void* QueueValue
Queue* queue_new()
void queue_free(Queue* queue)
int queue_push_head(Queue* queue, QueueValue data)
QueueValue queue_pop_head(Queue* queue)
QueueValue queue_peek_head(Queue* queue)
int queue_push_tail(Queue* queue, QueueValue data)
QueueValue queue_pop_tail(Queue* queue)
QueueValue queue_peek_tail(Queue* queue)
bint queue_is_empty(Queue* queue)
请注意这些声明与头文件声明几乎完全相同,因此您通常可以将它们复制过来。但是,您不需要像上面那样提供 所有 声明,只需要在代码或其他声明中使用那些声明,这样 Cython 就可以看到它们的足够和一致的子集。然后,考虑对它们进行一些调整,以使它们在 Cython 中更舒适。
具体来说,您应该注意为 C 函数选择好的参数名称,因为 Cython 允许您将它们作为关键字参数传递。稍后更改它们是向后不兼容的 API 修改。立即选择好的名称将使这些函数更适合使用 Cython 代码。
我们上面使用的头文件的一个值得注意的差异是第一行中Queue
结构的声明。在这种情况下,Queue
用作 不透明手柄;只有被调用的库才知道里面是什么。由于没有 Cython 代码需要知道结构的内容,我们不需要声明它的内容,所以我们只提供一个空的定义(因为我们不想声明 C 头中引用的_Queue
类型) [1] 。
另一个例外是最后一行。 queue_is_empty()
函数的整数返回值实际上是一个 C 布尔值,即关于它的唯一有趣的事情是它是非零还是零,表明队列是否为空。这最好用 Cython 的bint
类型表示,它在 C 中使用时是普通的int
类型,但在转换为 Python 对象时映射到 Python 的布尔值True
和False
。这种在.pxd
文件中收紧声明的方法通常可以简化使用它们的代码。
最好为您使用的每个库定义一个.pxd
文件,如果 API 很大,有时甚至为每个头文件(或功能组)定义。这简化了它们在其他项目中的重用。有时,您可能需要使用标准 C 库中的 C 函数,或者想直接从 CPython 调用 C-API 函数。对于像这样的常见需求,Cython 附带了一组标准的.pxd
文件,这些文件以易于使用的方式提供这些声明,以适应它们在 Cython 中的使用。主要包装是cpython
,libc
和libcpp
。 NumPy 库还有一个标准的.pxd
文件numpy
,因为它经常在 Cython 代码中使用。有关提供的.pxd
文件的完整列表,请参阅 Cython 的Cython/Includes/
源包。
编写包装类
在声明我们的 C 库的 API 之后,我们可以开始设计应该包装 C 队列的 Queue 类。它将存在于名为queue.pyx
的文件中。 [2]
这是 Queue 类的第一个开始:
# queue.pyx
cimport cqueue
cdef class Queue:
cdef cqueue.Queue* _c_queue
def __cinit__(self):
self._c_queue = cqueue.queue_new()
请注意,它表示__cinit__
而不是__init__
。虽然__init__
也可用,但不保证可以运行(例如,可以创建子类并忘记调用祖先的构造函数)。因为没有初始化 C 指针经常导致 Python 解释器的硬崩溃,所以在 CPython 甚至考虑调用__init__
之前,Cython 提供__cinit__
,始终 在构造时立即被调用,因此它是正确的位置初始化新实例的cdef
字段。但是,在对象构造期间调用__cinit__
时,self
尚未完全构造,并且必须避免对self
执行任何操作,而是分配给cdef
字段。
另请注意,上述方法不带参数,但子类型可能需要接受一些参数。无参数__cinit__()
方法是一种特殊情况,它只是不接收传递给构造函数的任何参数,因此它不会阻止子类添加参数。如果参数在__cinit__()
的签名中使用,则它们必须与用于实例化类型的类层次结构中任何声明的__init__
类方法相匹配。
内存管理
在我们继续实施其他方法之前,重要的是要了解上述实现并不安全。如果在queue_new()
调用中出现任何问题,此代码将简单地吞下错误,因此我们稍后可能会遇到崩溃。根据queue_new()
功能的文档,上述可能失败的唯一原因是内存不足。在这种情况下,它将返回NULL
,而它通常会返回指向新队列的指针。
Python 的方法是提出MemoryError
[3] 。因此我们可以更改 init 函数,如下所示:
# queue.pyx
cimport cqueue
cdef class Queue:
cdef cqueue.Queue* _c_queue
def __cinit__(self):
self._c_queue = cqueue.queue_new()
if self._c_queue is NULL:
raise MemoryError()
| [[3]](#id7) | 在`MemoryError`的特定情况下,为了引发它而创建一个新的异常实例实际上可能会失败,因为我们的内存不足。幸运的是,CPython 提供了一个 C-API 函数`PyErr_NoMemory()`,可以安全地为我们提出正确的例外。只要您编写`raise MemoryError`或`raise MemoryError()`,Cython 就会自动替换此 C-API 调用。如果您使用的是旧版本,则必须从标准软件包`cpython.exc`中导入 C-API 函数并直接调用它。 |
接下来要做的是在不再使用 Queue 实例时清理(即删除了对它的所有引用)。为此,CPython 提供了 Cython 作为特殊方法__dealloc__()
提供的回调。在我们的例子中,我们所要做的就是释放 C Queue,但前提是我们在 init 方法中成功初始化它:
def __dealloc__(self):
if self._c_queue is not NULL:
cqueue.queue_free(self._c_queue)
编译和链接
在这一点上,我们有一个可以测试的工作 Cython 模块。要编译它,我们需要为 distutils 配置一个setup.py
脚本。这是编译 Cython 模块的最基本脚本:
from distutils.core import setup
from distutils.extension import Extension
from Cython.Build import cythonize
setup(
ext_modules = cythonize([Extension("queue", ["queue.pyx"])])
)
要构建针对外部 C 库,我们需要确保 Cython 找到必要的库。存档有两种方法。首先,我们可以告诉 distutils 在哪里找到 c-source 来自动编译queue.c
实现。或者,我们可以构建和安装 C-Alg 作为系统库并动态链接它。如果其他应用也使用 C-Alg,后者是有用的。
静态链接
要自动构建 c 代码,我们需要在 <cite>queue.pyx</cite> 中包含编译器指令:
# distutils: sources = c-algorithms/src/queue.c
# distutils: include_dirs = c-algorithms/src/
cimport cqueue
cdef class Queue:
cdef cqueue.Queue* _c_queue
def __cinit__(self):
self._c_queue = cqueue.queue_new()
if self._c_queue is NULL:
raise MemoryError()
def __dealloc__(self):
if self._c_queue is not NULL:
cqueue.queue_free(self._c_queue)
sources
编译器指令给出了 distutils 将编译和链接(静态)到生成的扩展模块的 C 文件的路径。通常,所有相关的头文件都应该在include_dirs
中找到。现在我们可以使用以下方法构建项目
$ python setup.py build_ext -i
并测试我们的构建是否成功:
$ python -c 'import queue; Q = queue.Queue()'
动态链接
如果我们要打包的库已经安装在系统上,则动态链接很有用。要执行动态链接,我们首先需要构建和安装 c-alg。
要在您的系统上构建 c 算法:
$ cd c-algorithms
$ sh autogen.sh
$ ./configure
$ make
安装 CAlg 运行:
$ make install
之后文件/usr/local/lib/libcalg.so
应该存在。
注意
此路径适用于 Linux 系统,在其他平台上可能有所不同,因此您需要根据系统中libcalg.so
或libcalg.dll
的路径调整本教程的其余部分。
在这种方法中,我们需要告诉安装脚本与外部库链接。为此,我们需要扩展安装脚本以安装更改扩展设置
ext_modules = cythonize([Extension("queue", ["queue.pyx"])])
至
ext_modules = cythonize([
Extension("queue", ["queue.pyx"],
libraries=["calg"])
])
现在我们应该能够使用以下方法构建项目:
$ python setup.py build_ext -i
如果 <cite>libcalg</cite> 未安装在“普通”位置,用户可以通过传递适当的 C 编译器标志在外部提供所需的参数,例如:
CFLAGS="-I/usr/local/otherdir/calg/include" \
LDFLAGS="-L/usr/local/otherdir/calg/lib" \
python setup.py build_ext -i
在运行模块之前,我们还需要确保 <cite>libcalg</cite> 在 <cite>LD_LIBRARY_PATH</cite> 环境变量中,例如通过设置:
$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib
一旦我们第一次编译模块,我们现在可以导入它并实例化一个新的队列:
$ export PYTHONPATH=.
$ python -c 'import queue; Q = queue.Queue()'
但是,这是我们所有 Queue 类到目前为止所能做到的,所以让它更有用。
映射功能
在实现此类的公共接口之前,最好先查看 Python 提供的接口,例如在list
或collections.deque
类中。由于我们只需要 FIFO 队列,因此足以提供方法append()
,peek()
和pop()
,另外还有extend()
方法一次添加多个值。此外,由于我们已经知道所有值都来自 C,因此最好现在只提供cdef
方法,并为它们提供直接的 C 接口。
在 C 中,数据结构通常将数据作为void*
存储到任何数据项类型。由于我们只想存储通常适合指针类型大小的int
值,我们可以通过一个技巧避免额外的内存分配:我们将int
值转换为void*
,反之亦然,并存储值直接作为指针值。
这是append()
方法的简单实现:
cdef append(self, int value):
cqueue.queue_push_tail(self._c_queue, <void*>value)
同样,适用与__cinit__()
方法相同的错误处理注意事项,因此我们最终会使用此实现:
cdef append(self, int value):
if not cqueue.queue_push_tail(self._c_queue,
<void*>value):
raise MemoryError()
现在添加extend()
方法应该是直截了当的:
cdef extend(self, int* values, size_t count):
"""Append all ints to the queue.
"""
cdef int value
for value in values[:count]: # Slicing pointer to limit the iteration boundaries.
self.append(value)
例如,当从 C 数组读取值时,这变得很方便。
到目前为止,我们只能将数据添加到队列中。下一步是编写两个方法来获取第一个元素:peek()
和pop()
,它们分别提供只读和破坏性读访问。为了避免在直接将void*
转换为int
时出现编译器警告,我们使用的中间数据类型足以容纳void*
。在这里,Py_ssize_t
:
cdef int peek(self):
return <Py_ssize_t>cqueue.queue_peek_head(self._c_queue)
cdef int pop(self):
return <Py_ssize_t>cqueue.queue_pop_head(self._c_queue)
通常,在 C 中,当我们将较大的整数类型转换为较小的整数类型而不检查边界时,我们冒着丢失数据的风险,并且Py_ssize_t
可能是比int
更大的类型。但由于我们控制了如何将值添加到队列中,我们已经知道队列中的所有值都适合int
,因此上面的转换从void*
到Py_ssize_t
到int
(返回类型) )设计安全。
处理错误
现在,当队列为空时会发生什么?根据文档,函数返回NULL
指针,该指针通常不是有效值。但由于我们只是简单地向内和外输入,我们无法区分返回值是否为NULL
,因为队列为空或者队列中存储的值为0
。在 Cython 代码中,我们希望第一种情况引发异常,而第二种情况应该只返回0
。为了解决这个问题,我们需要特殊情况下这个值,并检查队列是否真的是空的:
cdef int peek(self) except? -1:
cdef int value = <Py_ssize_t>cqueue.queue_peek_head(self._c_queue)
if value == 0:
# this may mean that the queue is empty, or
# that it happens to contain a 0 value
if cqueue.queue_is_empty(self._c_queue):
raise IndexError("Queue is empty")
return value
请注意我们如何在希望常见的情况下有效地创建了通过该方法的快速路径,返回值不是0
。如果队列为空,则只有该特定情况需要额外检查。
方法签名中的except? -1
声明属于同一类别。如果函数是返回 Python 对象值的 Python 函数,CPython 将在内部返回NULL
而不是 Python 对象来指示异常,该异常将立即由周围的代码传播。问题是返回类型是int
并且任何int
值都是有效的队列项值,因此无法向调用代码显式地发出错误信号。实际上,如果没有这样的声明,Cython 就没有明显的方法可以知道在异常上返回什么以及调用代码甚至知道这个方法 可能 以异常退出。
调用代码可以处理这种情况的唯一方法是在从函数返回时调用PyErr_Occurred()
以检查是否引发了异常,如果是,则传播异常。这显然有性能损失。因此,Cython 允许您声明在异常的情况下它应隐式返回的值,以便周围的代码只需在接收到此精确值时检查异常。
我们选择使用-1
作为异常返回值,因为我们期望它是一个不太可能被放入队列的值。 except? -1
声明中的问号表示返回值不明确(毕竟,可能 可能是队列中的-1
值)并且需要使用PyErr_Occurred()
进行额外的异常检查在调用代码。没有它,调用此方法并接收异常返回值的 Cython 代码将默默地(有时不正确地)假定已引发异常。在任何情况下,所有其他返回值将几乎没有惩罚地通过,因此再次为“正常”值创建快速路径。
既然实现了peek()
方法,pop()
方法也需要适应。但是,由于它从队列中删除了一个值,因此仅在删除后测试队列是否为空 _ 是不够的。相反,我们必须在进入时测试它:_
cdef int pop(self) except? -1:
if cqueue.queue_is_empty(self._c_queue):
raise IndexError("Queue is empty")
return <Py_ssize_t>cqueue.queue_pop_head(self._c_queue)
异常传播的返回值与peek()
完全相同。
最后,我们可以通过实现__bool__()
特殊方法以正常的 Python 方式为 Queue 提供空白指示符(注意 Python 2 调用此方法__nonzero__
,而 Cython 代码可以使用任一名称):
def __bool__(self):
return not cqueue.queue_is_empty(self._c_queue)
请注意,此方法返回True
或False
,因为我们在cqueue.pxd
中将queue_is_empty()
函数的返回类型声明为bint
。
测试结果
既然实现已经完成,您可能需要为它编写一些测试以确保它正常工作。特别是 doctests 非常适合这个目的,因为它们同时提供了一些文档。但是,要启用 doctests,您需要一个可以调用的 Python API。从 Python 代码中看不到 C 方法,因此无法从 doctests 中调用。
为类提供 Python API 的一种快速方法是将方法从cdef
更改为cpdef
。这将让 Cython 生成两个入口点,一个可以使用 Python 调用语义和 Python 对象作为参数从普通 Python 代码调用,另一个可以使用快速 C 语义从 C 代码调用,不需要从 Python 进行中间参数转换。类型。请注意,cpdef
方法确保 Python 方法可以适当地覆盖它们,即使从 Cython 调用它们也是如此。与cdef
方法相比,这增加了很小的开销。
现在我们已经为我们的类提供了 C 接口和 Python 接口,我们应该确保两个接口都是一致的。 Python 用户期望extend()
方法接受任意迭代,而 C 用户希望有一个允许传递 C 数组和 C 内存的方法。两个签名都不兼容。
我们将通过考虑在 C 中,API 也可能想要支持其他输入类型来解决此问题,例如, long
或char
的数组,通常支持不同名称的 C API 函数,如extend_ints()
,extend_longs()
,extend_chars()``等。这允许我们释放方法名extend()
duck typed Python 方法,可以接受任意迭代。
以下清单显示了尽可能使用cpdef
方法的完整实现:
# queue.pyx
cimport cqueue
cdef class Queue:
"""A queue class for C integer values.
>>> q = Queue()
>>> q.append(5)
>>> q.peek()
5
>>> q.pop()
5
"""
cdef cqueue.Queue* _c_queue
def __cinit__(self):
self._c_queue = cqueue.queue_new()
if self._c_queue is NULL:
raise MemoryError()
def __dealloc__(self):
if self._c_queue is not NULL:
cqueue.queue_free(self._c_queue)
cpdef append(self, int value):
if not cqueue.queue_push_tail(self._c_queue,
<void*> <Py_ssize_t> value):
raise MemoryError()
# The `cpdef` feature is obviously not available for the original "extend()"
# method, as the method signature is incompatible with Python argument
# types (Python does not have pointers). However, we can rename
# the C-ish "extend()" method to e.g. "extend_ints()", and write
# a new "extend()" method that provides a suitable Python interface by
# accepting an arbitrary Python iterable.
cpdef extend(self, values):
for value in values:
self.append(value)
cdef extend_ints(self, int* values, size_t count):
cdef int value
for value in values[:count]: # Slicing pointer to limit the iteration boundaries.
self.append(value)
cpdef int peek(self) except? -1:
cdef int value = <Py_ssize_t> cqueue.queue_peek_head(self._c_queue)
if value == 0:
# this may mean that the queue is empty,
# or that it happens to contain a 0 value
if cqueue.queue_is_empty(self._c_queue):
raise IndexError("Queue is empty")
return value
cpdef int pop(self) except? -1:
if cqueue.queue_is_empty(self._c_queue):
raise IndexError("Queue is empty")
return <Py_ssize_t> cqueue.queue_pop_head(self._c_queue)
def __bool__(self):
return not cqueue.queue_is_empty(self._c_queue)
现在我们可以使用 python 脚本测试我们的 Queue 实现,例如这里test_queue.py
:
from __future__ import print_function
import time
import queue
Q = queue.Queue()
Q.append(10)
Q.append(20)
print(Q.peek())
print(Q.pop())
print(Q.pop())
try:
print(Q.pop())
except IndexError as e:
print("Error message:", e) # Prints "Queue is empty"
i = 10000
values = range(i)
start_time = time.time()
Q.extend(values)
end_time = time.time() - start_time
print("Adding {} items took {:1.3f} msecs.".format(i, 1000 * end_time))
for i in range(41):
Q.pop()
Q.pop()
print("The answer is:")
print(Q.pop())
作为在作者的机器上使用 10000 个数字的快速测试表明,使用来自 Cython 代码的 C int
值的这个队列大约是使用 Cython 代码和 Python 对象值的速度的五倍,几乎是使用它的速度的八倍。 Python 循环中的 Python 代码,仍然比使用 Python 整数的 Cython 代码中的 Python 高度优化的collections.deque
类型快两倍。
回调
假设您希望为用户提供一种方法,可以将队列中的值弹出到某个用户定义的事件。为此,您希望允许它们传递一个判断何时停止的谓词函数,例如:
def pop_until(self, predicate):
while not predicate(self.peek()):
self.pop()
现在,让我们假设为了参数,C 队列提供了一个将 C 回调函数作为谓词的函数。 API 可能如下所示:
/* C type of a predicate function that takes a queue value and returns
* -1 for errors
* 0 for reject
* 1 for accept
*/
typedef int (*predicate_func)(void* user_context, QueueValue data);
/* Pop values as long as the predicate evaluates to true for them,
* returns -1 if the predicate failed with an error and 0 otherwise.
*/
int queue_pop_head_until(Queue *queue, predicate_func predicate,
void* user_context);
C 回调函数具有通用void*
参数是正常的,该参数允许通过 C-API 将任何类型的上下文或状态传递到回调函数中。我们将使用它来传递我们的 Python 谓词函数。
首先,我们必须定义一个带有预期签名的回调函数,我们可以将其传递给 C-API 函数:
cdef int evaluate_predicate(void* context, cqueue.QueueValue value):
"Callback function that can be passed as predicate_func"
try:
# recover Python function object from void* argument
func = <object>context
# call function, convert result into 0/1 for True/False
return bool(func(<int>value))
except:
# catch any Python errors and return error indicator
return -1
主要思想是将指针(a.k.a。借用的引用)作为用户上下文参数传递给函数对象。我们将调用 C-API 函数如下:
def pop_until(self, python_predicate_function):
result = cqueue.queue_pop_head_until(
self._c_queue, evaluate_predicate,
<void*>python_predicate_function)
if result == -1:
raise RuntimeError("an error occurred")
通常的模式是首先将 Python 对象引用转换为void*
以将其传递给 C-API 函数,然后将其转换回 C 谓词回调函数中的 Python 对象。对void*
的强制转换创建了借用的引用。在转换为<object>
时,Cython 递增对象的引用计数,从而将借用的引用转换回拥有的引用。在谓词函数结束时,拥有的引用再次超出范围,Cython 丢弃它。
上面代码中的错误处理有点简单。具体而言,谓词函数引发的任何异常将基本上被丢弃,并且只会导致在事实之后引发普通RuntimeError()
。这可以通过将异常存储在通过 context 参数传递的对象中并在 C-API 函数返回-1
以指示错误之后重新引发它来改进。
扩展类型(又名.cdef 类)
原文:
docs.cython.org/en/latest/src/tutorial/cdef_classes.html
为了支持面向对象的编程,Cython 支持编写与 Python 完全相同的普通 Python 类:
class MathFunction(object):
def __init__(self, name, operator):
self.name = name
self.operator = operator
def __call__(self, *operands):
return self.operator(*operands)
然而,基于 Python 所谓的“内置类型”,Cython 支持第二种类:扩展类型,由于用于声明的关键字,有时也称为“cdef 类”。与 Python 类相比,它们受到一定限制,但通常比通用 Python 类更高效,速度更快。主要区别在于它们使用 C 结构来存储它们的字段和方法而不是 Python 字典。这允许他们在他们的字段中存储任意 C 类型,而不需要 Python 包装器,并且可以直接在 C 级访问字段和方法,而无需通过 Python 字典查找。
普通的 Python 类可以从 cdef 类继承,但不能从其他方面继承。 Cython 需要知道完整的继承层次结构,以便布局它们的 C 结构,并将其限制为单继承。另一方面,普通的 Python 类可以从 Cython 代码和纯 Python 代码中继承任意数量的 Python 类和扩展类型。
到目前为止,我们的集成示例并不是非常有用,因为它只集成了一个硬编码函数。为了解决这个问题,我们将使用 cdef 类来表示浮点数上的函数:
cdef class Function:
cpdef double evaluate(self, double x) except *:
return 0
指令 cpdef 提供了两种版本的方法;一个快速使用从 Cython 和一个较慢的使用从 Python。然后:
from libc.math cimport sin
cdef class Function:
cpdef double evaluate(self, double x) except *:
return 0
cdef class SinOfSquareFunction(Function):
cpdef double evaluate(self, double x) except *:
return sin(x ** 2)
这比为 cdef 方法提供 python 包装稍微多一点:与 cdef 方法不同,cpdef 方法可以被 Python 子类中的方法和实例属性完全覆盖。与 cdef 方法相比,它增加了一点调用开销。
为了使类定义对其他模块可见,从而允许在实现它们的模块之外进行有效的 C 级使用和继承,我们在sin_of_square.pxd
文件中定义它们:
cdef class Function:
cpdef double evaluate(self, double x) except *
cdef class SinOfSquareFunction(Function):
cpdef double evaluate(self, double x) except *
使用它,我们现在可以更改我们的集成示例:
from sin_of_square cimport Function, SinOfSquareFunction
def integrate(Function f, double a, double b, int N):
cdef int i
cdef double s, dx
if f is None:
raise ValueError("f cannot be None")
s = 0
dx = (b - a) / N
for i in range(N):
s += f.evaluate(a + i * dx)
return s * dx
print(integrate(SinOfSquareFunction(), 0, 1, 10000))
这几乎与前面的代码一样快,但是由于可以更改集成功能,因此它更加灵活。我们甚至可以传入 Python 空间中定义的新函数:
>>> import integrate
>>> class MyPolynomial(integrate.Function):
... def evaluate(self, x):
... return 2*x*x + 3*x - 10
...
>>> integrate(MyPolynomial(), 0, 1, 10000)
-7.8335833300000077
这比原始的仅使用 Python 的集成代码慢大约 20 倍,但仍然快 10 倍。这显示了当整个循环从 Python 代码移动到 Cython 模块时,加速可以很容易地变大。
关于evaluate
新实施的一些注意事项:
- 此处的快速方法调度仅起作用,因为
Function
中声明了evaluate
。如果在SinOfSquareFunction
中引入evaluate
,代码仍然可以工作,但 Cython 会使用较慢的 Python 方法调度机制。- 以同样的方式,如果参数
f
没有被输入,但只是作为 Python 对象传递,那么将使用较慢的 Python 调度。- 由于参数是打字的,我们需要检查它是否是
None
。在 Python 中,当查找evaluate
方法时,这会导致AttributeError
,但 Cython 会尝试访问None
的(不兼容的)内部结构,就像它是Function
一样,导致崩溃或数据损坏。
有一个 编译器指令 nonecheck
,它会以降低速度为代价启用此检查。以下是编译器指令用于动态打开或关闭nonecheck
的方法:
# cython: nonecheck=True
# ^^^ Turns on nonecheck globally
import cython
cdef class MyClass:
pass
# Turn off nonecheck locally for the function
@cython.nonecheck(False)
def func():
cdef MyClass obj = None
try:
# Turn nonecheck on again for a block
with cython.nonecheck(True):
print(obj.myfunc()) # Raises exception
except AttributeError:
pass
print(obj.myfunc()) # Hope for a crash!
cdef 类中的属性与常规类中的属性的行为不同:
- 所有属性必须在编译时预先声明
- 属性默认只能从用 Cython 访问(类型化访问)
- 属性可以声明为暴露的 Python 空间的动态属性
from sin_of_square cimport Function
cdef class WaveFunction(Function):
# Not available in Python-space:
cdef double offset
# Available in Python-space:
cdef public double freq
# Available in Python-space, but only for reading:
cdef readonly double scale
# Available in Python-space:
@property
def period(self):
return 1.0 / self.freq
@period.setter
def period(self, value):
self.freq = 1.0 / value
pxd 文件
除了.pyx
源文件之外,Cython 还使用.pxd
文件,它们的工作方式类似于 C 头文件 - 它们包含 Cython 声明(有时是代码部分),仅供 Cython 模块使用。使用cimport
关键字将pxd
文件导入pyx
模块。
pxd
文件有很多用例:
它们可用于共享外部 C 声明。
它们可以包含非常适合 C 编译器内联的函数。这些功能应标记为
inline
,例如:cdef inline int int_min(int a, int b): return b if b < a else a
当附带同名的
pyx
文件时,它们为 Cython 模块提供了一个 Cython 接口,以便其他 Cython 模块可以使用比 Python 更高效的协议与之通信。
在我们的集成示例中,我们可能会将其分解为pxd
文件,如下所示:
添加
cmath.pxd
功能,定义 Cmath.h
头文件中可用的 C 功能,如sin
。然后人们只需在integrate.pyx
中做from cmath cimport sin
。添加
integrate.pxd
,以便用 Cython 编写的其他模块可以定义要集成的快速自定义函数。cdef class Function: cpdef evaluate(self, double x) cpdef integrate(Function f, double a, double b, int N)
请注意,如果您的 cdef 类具有属性,则必须在类声明
pxd
文件(如果使用)中声明属性,而不是pyx
文件。编译器会告诉你这个。
Caveats
由于 Cython 混合了 C 语言和 Python 语义,因此有些事情可能会有点令人惊讶或不直观。对于 Python 用户来说,工作总是让 Cython 更自然,因此这个列表将来可能会发生变化。
- 给定两个类型
int
变量a
和b
,a % b
与第二个参数(遵循 Python 语义)具有相同的符号,而不是与第一个符号相同(如在 C)。通过启用 cdivision 指令(Cython 0.12 之前的版本始终遵循 C 语义),可以在某种速度增益下获得 C 行为。- 无条件类型需要小心。
cdef unsigned n = 10; print(range(-n, n))
将打印一个空列表,因为-n
在传递给range
函数之前回绕到一个大的正整数。- Python 的
float
类型实际上包含了 Cdouble
值,而 Python 2.x 中的int
类型包含了 Clong
值。
Profiling
原文:
docs.cython.org/en/latest/src/tutorial/profiling_tutorial.html
这部分描述了 Cython 的性能分析能力。如果您熟悉纯 Python 代码的分析,则只能阅读第一部分( Cython Profiling Basics)。如果您不熟悉 Python 分析,您还应该阅读教程( 分析教程 ),它将逐步引导您完成一个完整的示例。
Cython Profiling Basics
Cython 中的分析由编译器指令控制。它可以通过 Cython 装饰器设置为整个文件或基于每个功能。
启用完整源文件的分析
通过全局指令将完整源文件的分析启用到文件顶部的 Cython 编译器:
# cython: profile=True
请注意,分析会给每个函数调用带来轻微的开销,因此会使程序变慢(或者很多,如果你经常调用一些小函数)。
启用后,从 cProfile 模块调用时,您的 Cython 代码将像 Python 代码一样运行。这意味着您可以使用与仅 Python 代码相同的工具,将您的 Cython 代码与 Python 代码一起分析。
禁用分析功能
如果您的分析因为某些小功能的调用开销而变得混乱,而您希望在您的配置文件中看不到这些功能 - 无论是因为您计划内联它们还是因为您确定不能让它们更快 - 你可以使用一个特殊的装饰器来禁用一个函数的分析(无论它是否全局启用):
cimport cython
@cython.profile(False)
def my_often_called_function():
pass
启用行跟踪
要获得更详细的跟踪信息(对于可以使用它的工具),您可以启用行跟踪:
# cython: linetrace=True
这也将启用分析支持,因此不需要上面的profile=True
选项。例如,覆盖率分析需要线跟踪。
请注意,即使通过编译器指令启用了行跟踪,默认情况下也不会使用它。由于运行时减速可能很大,因此必须由 C 编译器通过设置 C 宏定义CYTHON_TRACE=1
进行编译。要在跟踪中包含 nogil 函数,请设置CYTHON_TRACE_NOGIL=1
(表示CYTHON_TRACE=1
)。可以在setup.py
脚本的扩展定义中定义 C 宏,也可以使用以下文件头注释设置源文件中的相应 distutils 选项(如果cythonize()
用于编译):
# distutils: define_macros=CYTHON_TRACE_NOGIL=1
启用覆盖率分析
从 Cython 0.23 开始,线跟踪(见上文)也支持使用 coverage.py 工具报告覆盖率报告。要使覆盖率分析了解 Cython 模块,还需要在.coveragerc
文件中启用 Cython 的 coverage 插件,如下所示:
[run]
plugins = Cython.Coverage
使用此插件,您的 Cython 源文件应该在 coverage 报告中正常显示。
要将覆盖率报告包含在 Cython 带注释的 HTML 文件中,您需要首先运行 coverage.py 工具以生成 XML 结果文件。将此文件传递到cython
命令,如下所示:
$ cython --annotate-coverage coverage.xml package/mymodule.pyx
这将重新编译 Cython 模块并在其处理的每个 Cython 源文件旁边生成一个 HTML 输出文件,其中包含 coverage 报告中包含的行的颜色标记。
分析教程
这将是一个完整的教程,从头到尾,分析 Python 代码,将其转换为 Cython 代码并保持分析直到它足够快。
作为一个玩具示例,我们想要评估平方倒数的总和,直到某个整数来评估
。我们想要使用的关系已经由欧拉在 1735 年证明并被称为巴塞尔问题。
用于评估截断总和的简单 Python 代码如下所示:
# calc_pi.py
def recip_square(i):
return 1. / i ** 2
def approx_pi(n=10000000):
val = 0.
for k in range(1, n + 1):
val += recip_square(k)
return (6 * val) ** .5
在我的盒子上,这需要大约 4 秒来运行默认 n 的函数。我们选择 n 越高,的近似值越好。经验丰富的 Python 程序员已经看到很多地方可以优化这段代码。但请记住优化的黄金法则:永不优化而不进行分析。让我重复一遍:从不优化而不分析您的代码。您对代码的哪一部分花费太多时间的想法是错误的。至少,我的总是错的。所以让我们编写一个简短的脚本来分析我们的代码:
# profile.py
import pstats, cProfile
import calc_pi
cProfile.runctx("calc_pi.approx_pi()", globals(), locals(), "Profile.prof")
s = pstats.Stats("Profile.prof")
s.strip_dirs().sort_stats("time").print_stats()
在我的盒子上运行它给出以下输出:
Sat Nov 7 17:40:54 2009 Profile.prof
10000004 function calls in 6.211 CPU seconds
Ordered by: internal time
ncalls tottime percall cumtime percall filename:lineno(function)
1 3.243 3.243 6.211 6.211 calc_pi.py:7(approx_pi)
10000000 2.526 0.000 2.526 0.000 calc_pi.py:4(recip_square)
1 0.442 0.442 0.442 0.442 {range}
1 0.000 0.000 6.211 6.211 <string>:1(<module>)
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
它包含代码在 6.2 CPU 秒内运行的信息。请注意,代码慢了 2 秒,因为它在 cProfile 模块中运行。该表包含真正有价值的信息。您可能需要查看 Python 分析文档以获取详细信息。这里最重要的列是 totime(在此函数中花费的总时间而不是计算由此函数调用的函数)和 cumtime(在此函数中花费的总时间也计算所谓的函数通过这个功能)。查看 tottime 列,我们看到大约一半的时间花在 approx_pi 上,另一半花在 recip_square 上。还有半秒钟在范围内度过…当然我们应该使用 xrange 进行如此大的迭代。实际上,只需将范围更改为 xrange 就可以在 5.8 秒内运行代码。
我们可以在纯 Python 版本中进行优化,但由于我们对 Cython 感兴趣,让我们继续前进并将此模块带到 Cython。我们无论如何都会这样做,以使循环运行得更快。这是我们的第一个 Cython 版本:
# cython: profile=True
# calc_pi.pyx
def recip_square(int i):
return 1. / i ** 2
def approx_pi(int n=10000000):
cdef double val = 0.
cdef int k
for k in range(1, n + 1):
val += recip_square(k)
return (6 * val) ** .5
注意第一行:我们必须告诉 Cython 应该启用性能分析。这使得 Cython 代码稍慢,但如果没有这个,我们将无法从 cProfile 模块获得有意义的输出。其余代码大部分都没有改变,我只输入了一些可能会加快速度的变量。
我们还需要修改我们的分析脚本以直接导入 Cython 模块。这是添加 Pyximport 模块导入的完整版本:
# profile.py
import pstats, cProfile
import pyximport
pyximport.install()
import calc_pi
cProfile.runctx("calc_pi.approx_pi()", globals(), locals(), "Profile.prof")
s = pstats.Stats("Profile.prof")
s.strip_dirs().sort_stats("time").print_stats()
我们只添加了两行,其余的完全相同。或者,我们也可以手动将代码编译成扩展名;我们根本不需要更改配置文件脚本。该脚本现在输出以下内容:
Sat Nov 7 18:02:33 2009 Profile.prof
10000004 function calls in 4.406 CPU seconds
Ordered by: internal time
ncalls tottime percall cumtime percall filename:lineno(function)
1 3.305 3.305 4.406 4.406 calc_pi.pyx:7(approx_pi)
10000000 1.101 0.000 1.101 0.000 calc_pi.pyx:4(recip_square)
1 0.000 0.000 4.406 4.406 {calc_pi.approx_pi}
1 0.000 0.000 4.406 4.406 <string>:1(<module>)
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
我们增加了 1.8 秒。不是太寒酸。将输出与前一个进行比较,我们看到 recip_square 函数变得更快,而 approx_pi 函数没有发生很大变化。让我们更专注于 recip_square 函数。首先请注意,不要从我们模块之外的代码调用此函数;所以将它变成 cdef 以减少呼叫开销是明智的。我们也应该摆脱幂运算符:它变成了 Cython 的 pow(i,2)函数调用,但我们可以改为编写 i * i,这可能更快。整个功能也是内联的良好候选者。让我们看看这些想法的必要变化:
# cython: profile=True
# calc_pi.pyx
cdef inline double recip_square(int i):
return 1. / (i * i)
def approx_pi(int n=10000000):
cdef double val = 0.
cdef int k
for k in range(1, n + 1):
val += recip_square(k)
return (6 * val) ** .5
现在运行配置文件脚本会产生:
Sat Nov 7 18:10:11 2009 Profile.prof
10000004 function calls in 2.622 CPU seconds
Ordered by: internal time
ncalls tottime percall cumtime percall filename:lineno(function)
1 1.782 1.782 2.622 2.622 calc_pi.pyx:7(approx_pi)
10000000 0.840 0.000 0.840 0.000 calc_pi.pyx:4(recip_square)
1 0.000 0.000 2.622 2.622 {calc_pi.approx_pi}
1 0.000 0.000 2.622 2.622 <string>:1(<module>)
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
那又给我们买了 1.8 秒。不是我们可以期待的戏剧性变化。为什么 recip_square 仍然在这个表中;应该是内联的,不是吗?这样做的原因是,即使消除了函数调用,Cython 仍会生成分析代码。让我们告诉它不再介绍 recip_square;无论如何,我们无法使功能更快:
# cython: profile=True
# calc_pi.pyx
cimport cython
@cython.profile(False)
cdef inline double recip_square(int i):
return 1. / (i * i)
def approx_pi(int n=10000000):
cdef double val = 0.
cdef int k
for k in range(1, n + 1):
val += recip_square(k)
return (6 * val) ** .5
运行它显示了一个有趣的结果:
Sat Nov 7 18:15:02 2009 Profile.prof
4 function calls in 0.089 CPU seconds
Ordered by: internal time
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.089 0.089 0.089 0.089 calc_pi.pyx:10(approx_pi)
1 0.000 0.000 0.089 0.089 {calc_pi.approx_pi}
1 0.000 0.000 0.089 0.089 <string>:1(<module>)
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
首先要注意的是速度的巨大提升:这个版本只占我们第一个 Cython 版本的 1/50。另请注意,recip_square 已经从我们想要的表中消失了。但最奇特和重要的变化是,about_pi 也变得更快。这是所有分析的问题:在配置文件运行中调用函数会给函数调用增加一定的开销。这个开销是而不是添加到被调用函数所花费的时间,而是加到调用函数所花费的时间。在这个例子中,在最后一次运行中,approx_pi 不需要 2.622 秒;但它调用了 recip_square 10000000 次,每次都需要稍微设置一下它的分析。这相当于大约 2.6 秒的大量时间损失。禁用经常调用的函数的分析现在揭示了 approx_pi 的实际时间;如果需要,我们现在可以继续优化它。
这个分析教程到此结束。此代码仍有一些改进空间。我们可以尝试用 C stdlib 中的 sqrt 调用替换 approx_pi 中的幂运算符;但这并不一定比调用 pow(x,0.5)更快。
即便如此,我们在这里取得的成果非常令人满意:我们提出了一个比原始 Python 版本快得多的解决方案,同时保留了功能和可读性。
Unicode 和传递字符串
与 Python 3 中的字符串语义类似,Cython 严格区分字节字符串和 unicode 字符串。最重要的是,这意味着默认情况下,字节字符串和 unicode 字符串之间没有自动转换(Python 2 在字符串操作中的作用除外)。所有编码和解码必须通过显式编码/解码步骤。为了在简单的情况下简化 Python 和 C 字符串之间的转换,模块级c_string_type
和c_string_encoding
指令可用于隐式插入这些编码/解码步骤。
Cython 代码中的 Python 字符串类型
Cython 支持四种 Python 字符串类型: bytes
, str
,unicode
和basestring
。 bytes
和unicode
类型是普通 Python 2.x 中已知的特定类型(在 Python 3 中命名为 bytes
和 str
)。此外,Cython 还支持 bytearray
类型,其行为类似于 bytes
类型,除了它是可变的。
str
类型的特殊之处在于它是 Python 2 中的字节字符串和 Python 3 中的 Unicode 字符串(用于使用语言级别 2 编译的 Cython 代码,即默认值)。意思是,它总是与 Python 运行时自身调用的类型 str
完全对应。因此,在 Python 2 中, bytes
和 str
都代表字节串类型,而在 Python 3 中, str
和unicode
]表示 Python Unicode 字符串类型。切换是在 C 编译时进行的,用于运行 Cython 的 Python 版本是不相关的。
在使用语言级别 3 编译 Cython 代码时, str
类型在 Cython 编译时使用完全符合 Unicode 字符串类型进行标识,即在运行时无法识别 bytes
在 Python 2 中。
请注意, str
类型与 Python 2 中的unicode
类型不兼容,即您无法将 Unicode 字符串分配给键入的变量或参数 str
。该尝试将在运行时导致编译时错误(如果可检测)或 TypeError
异常。因此,在必须与 Python 2 兼容的代码中静态键入字符串变量时应该小心,因为此 Python 版本允许混合字节字符串和数据的 unicode 字符串,用户通常希望代码能够同时使用这两者。仅针对 Python 3 的代码可以安全地将变量和参数键入为 bytes
或unicode
。
basestring
类型表示 str
和unicode
类型,即 Python 2 和 Python 3 中的所有 Python 文本字符串类型。这可用于键入通常包含 Unicode 文本的文本变量(至少在 Python 3)中,但为了向后兼容的原因,必须另外接受 Python 2 中的 str
类型。它与 bytes
类型不兼容。它的使用在普通的 Cython 代码中应该是罕见的,因为通用 object
类型(即无类型代码)通常足够好并且具有支持字符串子类型的分配的额外优点。在 Cython 0.20 中添加了对basestring
类型的支持。
字符串文字
Cython 了解所有 Python 字符串类型前缀:
- 字节串的
b'bytes'
- Unicode 字符串的
u'text'
f'formatted {value}'
用于格式化的 Unicode 字符串文字,由 PEP 498 定义(在 Cython 0.24 中添加)
当使用语言级别 2 和unicode
对象(即 Python 3 str
)进行语言级别 3 编译时,未加前缀的字符串文字将成为 str
对象。
关于 C 字符串的一般说明
在许多用例中,C 字符串(即字符串指针)速度慢且繁琐。首先,它们通常需要以某种方式进行手动内存管理,这使得它更有可能在代码中引入错误。
然后,Python 字符串对象缓存它们的长度,因此请求它(例如,验证索引访问的边界或将两个字符串连接成一个)是一种有效的恒定时间操作。相反,调用strlen()
从 C 字符串获取此信息需要线性时间,这使得对 C 字符串的许多操作相当昂贵。
关于文本处理,Python 内置了对 Unicode 的支持,C 完全缺乏。如果您正在处理 Unicode 文本,那么使用 Python Unicode 字符串对象通常比尝试使用 C 字符串中的编码数据更好。 Cython 使这非常简单有效。
一般来说:除非您知道自己在做什么,否则请尽量避免使用 C 字符串,而应使用 Python 字符串对象。对此明显的例外是从外部 C 代码来回传递它们。此外,C ++字符串也记住它们的长度,因此它们可以在某些情况下提供 Python 字节对象的合适替代,例如,在明确定义的上下文中不需要引用计数时。
传递字节串
我们在名为c_func.pyx
的文件中声明了虚拟 C 函数,我们将在本教程中重复使用它们:
from libc.stdlib cimport malloc
from libc.string cimport strcpy, strlen
cdef char* hello_world = 'hello world'
cdef Py_ssize_t n = strlen(hello_world)
cdef char* c_call_returning_a_c_string():
cdef char* c_string = <char *> malloc((n + 1) * sizeof(char))
if not c_string:
raise MemoryError()
strcpy(c_string, hello_world)
return c_string
cdef void get_a_c_string(char** c_string_ptr, Py_ssize_t *length):
c_string_ptr[0] = <char *> malloc((n + 1) * sizeof(char))
if not c_string_ptr[0]:
raise MemoryError()
strcpy(c_string_ptr[0], hello_world)
length[0] = n
我们制作了相应的c_func.pxd
以便能够实现这些功能:
cdef char* c_call_returning_a_c_string()
cdef void get_a_c_string(char** c_string, Py_ssize_t *length)
在 C 代码和 Python 之间传递字节字符串非常容易。从 C 库接收字节字符串时,只需将其转换为 Python 变量,即可让 Cython 将其转换为 Python 字节字符串:
from c_func cimport c_call_returning_a_c_string
cdef char* c_string = c_call_returning_a_c_string()
cdef bytes py_string = c_string
转换为 object
或 bytes
的类型将执行相同的操作:
py_string = <bytes> c_string
这将创建一个 Python 字节字符串对象,该对象包含原始 C 字符串的副本。它可以安全地在 Python 代码中传递,并在最后一次引用超出范围时进行垃圾收集。重要的是要记住,字符串中的空字节充当终止符,如 C 中通常所知。因此,上述内容仅适用于不包含空字节的 C 字符串。
除了不使用空字节之外,对于长字符串,上述也是非常低效的,因为 Cython 必须首先在 C 字符串上调用strlen()
以通过计算字节直到终止空字节来找出长度。在许多情况下,用户代码已经知道长度,例如因为 C 函数返回了它。在这种情况下,通过切片 C 字符串告诉 Cython 确切的字节数会更有效。这是一个例子:
from libc.stdlib cimport free
from c_func cimport get_a_c_string
def main():
cdef char* c_string = NULL
cdef Py_ssize_t length = 0
# get pointer and length from a C function
get_a_c_string(&c_string, &length)
try:
py_bytes_string = c_string[:length] # Performs a copy of the data
finally:
free(c_string)
这里,不需要额外的字节计数,c_string
中的length
字节将被复制到 Python 字节对象中,包括任何空字节。请记住,在这种情况下,切片索引被认为是准确的,并且没有进行边界检查,因此不正确的切片索引将导致数据损坏和崩溃。
请注意,Python 字节字符串的创建可能会因异常而失败,例如由于记忆力不足。如果转换后需要free()
字符串,则应将赋值包装在 try-finally 结构中:
from libc.stdlib cimport free
from c_func cimport c_call_returning_a_c_string
cdef bytes py_string
cdef char* c_string = c_call_returning_a_c_string()
try:
py_string = c_string
finally:
free(c_string)
要将字节字符串转换回 C char*
,请使用相反的赋值:
cdef char* other_c_string = py_string # other_c_string is a 0-terminated string.
这是一个非常快速的操作,之后other_c_string
指向 Python 字符串本身的字节字符串缓冲区。它与 Python 字符串的生命周期有关。当 Python 字符串被垃圾收集时,指针变为无效。因此,只要char*
正在使用,就必须保持对 Python 字符串的引用。通常,这只会调用一个接收指针作为参数的 C 函数。但是,当 C 函数存储指针供以后使用时,必须特别小心。除了保持对字符串对象的 Python 引用外,不需要手动内存管理。
从 Cython 0.20 开始,支持 bytearray
类型并以与 bytes
类型相同的方式强制执行。但是,在 C 上下文中使用它时,在将对象缓冲区转换为 C 字符串指针后,必须特别注意不要增大或缩小对象缓冲区。这些修改可以更改内部缓冲区地址,这将使指针无效。
接受 Python 代码中的字符串
另一方面,从 Python 代码接收输入,初看起来可能看起来很简单,因为它只处理对象。但是,在不使 API 太窄或太不安全的情况下做到这一点可能并不完全明显。
在 API 仅处理字节字符串(即二进制数据或编码文本)的情况下,最好不要将输入参数键入 bytes
,因为这会将允许的输入限制为准确该类型并排除子类型和其他类型的字节容器,例如 bytearray
对象或内存视图。
根据数据的处理方式(以及在何处),最好接收一维存储器视图,例如,
def process_byte_data(unsigned char[:] data):
length = data.shape[0]
first_byte = data[0]
slice_view = data[1:-1]
# ...
Cython 的内存视图在 Typed Memoryviews 中有更详细的描述,但上面的例子已经显示了 1 维字节视图的大部分相关功能。它们允许有效地处理数组并接受任何可以将其自身解压缩到字节缓冲区中的内容,而无需中间复制。处理后的内容最终可以在内存视图本身(或其中的一部分)中返回,但通常最好将数据复制回平坦且简单的 bytes
或 bytearray
对象,特别是当只返回一个小切片时。由于内存视图不会复制数据,因此它们会保持整个原始缓冲区的活动状态。这里的一般想法是通过接受任何类型的字节缓冲区来自由输入,但通过返回一个简单,适应良好的对象来严格控制输出。这可以简单地完成如下:
def process_byte_data(unsigned char[:] data):
# ... process the data, here, dummy processing.
cdef bint return_all = (data[0] == 108)
if return_all:
return bytes(data)
else:
# example for returning a slice
return bytes(data[5:7])
如果字节输入实际上是编码文本,并且进一步处理应该在 Unicode 级别进行,那么正确的做法是直接解码输入。这几乎只是 Python 2.x 中的一个问题,Python 代码期望它可以将带有编码文本的字节串( str
)传递到文本 API 中。由于这通常发生在模块 API 的多个位置,因此辅助函数几乎总是可行的,因为它允许以后轻松调整输入规范化过程。
这种输入规范化功能通常类似于以下内容:
# to_unicode.pyx
from cpython.version cimport PY_MAJOR_VERSION
cdef unicode _text(s):
if type(s) is unicode:
# Fast path for most common case(s).
return <unicode>s
elif PY_MAJOR_VERSION < 3 and isinstance(s, bytes):
# Only accept byte strings as text input in Python 2.x, not in Py3.
return (<bytes>s).decode('ascii')
elif isinstance(s, unicode):
# We know from the fast path above that 's' can only be a subtype here.
# An evil cast to <unicode> might still work in some(!) cases,
# depending on what the further processing does. To be safe,
# we can always create a copy instead.
return unicode(s)
else:
raise TypeError("Could not convert to unicode.")
然后应该像这样使用:
from to_unicode cimport _text
def api_func(s):
text_input = _text(s)
# ...
类似地,如果进一步处理发生在字节级别,但是应该接受 Unicode 字符串输入,那么如果您使用内存视图,则以下可能有效:
# define a global name for whatever char type is used in the module
ctypedef unsigned char char_type
cdef char_type[:] _chars(s):
if isinstance(s, unicode):
# encode to the specific encoding used inside of the module
s = (<unicode>s).encode('utf8')
return s
在这种情况下,您可能希望另外确保字节串输入确实使用正确的编码,例如如果需要纯 ASCII 输入数据,可以在循环中运行缓冲区并检查每个字节的最高位。这也应该在输入规范化函数中完成。
处理“const”
许多 C 库在其 API 中使用const
修饰符来声明它们不会修改字符串,或者要求用户不得修改它们返回的字符串,例如:
typedef const char specialChar;
int process_string(const char* s);
const unsigned char* look_up_cached_string(const unsigned char* key);
Cython 支持该语言中的const
修饰符,因此您可以直接声明上述函数,如下所示:
cdef extern from "someheader.h":
ctypedef const char specialChar
int process_string(const char* s)
const unsigned char* look_up_cached_string(const unsigned char* key)
将字节解码为文本
如果您的代码只处理字符串中的二进制数据,那么最初提供的传递和接收 C 字符串的方法就足够了。但是,当我们处理编码文本时,最好在接收时将 C 字节字符串解码为 Python Unicode 字符串,并在出路时将 Python Unicode 字符串编码为 C 字节字符串。
使用 Python 字节字符串对象,通常只需调用bytes.decode()
方法将其解码为 Unicode 字符串:
ustring = byte_string.decode('UTF-8')
Cython 允许您对 C 字符串执行相同操作,只要它不包含空字节:
from c_func cimport c_call_returning_a_c_string
cdef char* some_c_string = c_call_returning_a_c_string()
ustring = some_c_string.decode('UTF-8')
并且,对于已知长度的字符串,更有效:
from c_func cimport get_a_c_string
cdef char* c_string = NULL
cdef Py_ssize_t length = 0
# get pointer and length from a C function
get_a_c_string(&c_string, &length)
ustring = c_string[:length].decode('UTF-8')
当字符串包含空字节时,应该使用相同的字符串,例如当它使用像 UCS-4 这样的编码时,每个字符以四个字节编码,其中大多数字符往往是 0。
同样,如果提供切片索引,则不会进行边界检查,因此不正确的索引会导致数据损坏和崩溃。但是,使用负索引是可能的,并将调用strlen()
以确定字符串长度。显然,这仅适用于没有内部空字节的 0 终止字符串。以 UTF-8 编码的文本或 ISO-8859 编码之一通常是一个很好的候选者。如果有疑问,最好传递“明显”正确的索引,而不是依赖于数据是否符合预期。
通常的做法是在专用函数中包装字符串转换(以及一般的非平凡类型转换),因为每当从 C 接收文本时,都需要以完全相同的方式完成。这可能如下所示:
from libc.stdlib cimport free
cdef unicode tounicode(char* s):
return s.decode('UTF-8', 'strict')
cdef unicode tounicode_with_length(
char* s, size_t length):
return s[:length].decode('UTF-8', 'strict')
cdef unicode tounicode_with_length_and_free(
char* s, size_t length):
try:
return s[:length].decode('UTF-8', 'strict')
finally:
free(s)
最有可能的是,根据要处理的字符串类型,您会更喜欢代码中较短的函数名称。不同类型的内容通常意味着在接收时处理它们的不同方式。为了使代码更具可读性并预测未来的更改,最好对不同类型的字符串使用单独的转换函数。
将文本编码为字节
反过来,将 Python unicode 字符串转换为 C char*
本身非常有效,假设您实际需要的是内存管理字节字符串:
py_byte_string = py_unicode_string.encode('UTF-8')
cdef char* c_string = py_byte_string
如前所述,这将指针指向 Python 字节字符串的字节缓冲区。尝试在不保留对 Python 字节字符串的引用的情况下执行相同操作将失败并出现编译错误:
# this will not compile !
cdef char* c_string = py_unicode_string.encode('UTF-8')
在这里,Cython 编译器注意到代码采用指向临时字符串结果的指针,该结果将在赋值后进行垃圾回收。稍后访问无效指针将读取无效内存,并可能导致段错误。因此,Cython 将拒绝编译此代码。
C ++字符串
包装 C ++库时,字符串通常以std::string
类的形式出现。与 C 字符串一样,Python 字节字符串自动强制转换为 C ++字符串:
# distutils: language = c++
from libcpp.string cimport string
def get_bytes():
py_bytes_object = b'hello world'
cdef string s = py_bytes_object
s.append('abc')
py_bytes_object = s
return py_bytes_object
内存管理情况与 C 中的情况不同,因为创建 C ++字符串会生成字符串对象随后拥有的字符串缓冲区的独立副本。因此,可以将临时创建的 Python 对象直接转换为 C ++字符串。使用此方法的常用方法是将 Python unicode 字符串编码为 C ++字符串:
cdef string cpp_string = py_unicode_string.encode('UTF-8')
请注意,这涉及一些开销,因为它首先将 Unicode 字符串编码为临时创建的 Python 字节对象,然后将其缓冲区复制到新的 C ++字符串中。
另一方面,Cython 0.17 及更高版本提供了高效的解码支持:
# distutils: language = c++
from libcpp.string cimport string
def get_ustrings():
cdef string s = string(b'abcdefg')
ustring1 = s.decode('UTF-8')
ustring2 = s[2:-2].decode('UTF-8')
return ustring1, ustring2
对于 C ++字符串,解码片将始终考虑字符串的适当长度并应用 Python 切片语义(例如,为越界索引返回空字符串)。
自动编码和解码
Cython 0.19 附带两个新指令:c_string_type
和c_string_encoding
。它们可用于更改 C / C ++字符串强制执行的 Python 字符串类型。默认情况下,它们仅强制执行字节类型,并且必须明确地进行编码或解码,如上所述。
有两种用例不方便。首先,如果正在处理的所有 C 字符串(或大多数)包含文本,则从 Python unicode 对象自动编码和解码可以减少代码开销。在这种情况下,您可以将模块中的c_string_type
指令设置为unicode
,将c_string_encoding
指定为 C 代码使用的编码,例如:
# cython: c_string_type=unicode, c_string_encoding=utf8
cdef char* c_string = 'abcdefg'
# implicit decoding:
cdef object py_unicode_object = c_string
# explicit conversion to Python bytes:
py_bytes_object = <bytes>c_string
第二个用例是当所有正在处理的 C 字符串只包含 ASCII 可编码字符(例如数字)时,您希望代码在 Python 2 中使用本机遗留字符串类型,而不是始终使用 Unicode。在这种情况下,您可以将字符串类型设置为 str
:
# cython: c_string_type=str, c_string_encoding=ascii
cdef char* c_string = 'abcdefg'
# implicit decoding in Py3, bytes conversion in Py2:
cdef object py_str_object = c_string
# explicit conversion to Python bytes:
py_bytes_object = <bytes>c_string
# explicit conversion to Python unicode:
py_bytes_object = <unicode>c_string
另一个方向,即自动编码为 C 字符串,仅支持 ASCII 和“默认编码”,它通常是 Python 3 中的 UTF-8,通常是 Python 2 中的 ASCII。在这种情况下,CPython 通过保持一个来处理内存管理字符串的编码副本与原始 unicode 字符串一起存活。否则,将无法以任何合理的方式限制编码字符串的生命周期,从而使得从其中提取 C 字符串指针的任何尝试都是危险的尝试。以下安全地将 Unicode 字符串转换为 ASCII(将c_string_encoding
更改为default
以使用默认编码):
# cython: c_string_type=unicode, c_string_encoding=ascii
def func():
ustring = u'abc'
cdef char* s = ustring
return s[0] # returns u'a'
(此示例使用函数上下文来安全地控制 Unicode 字符串的生命周期。可以从外部修改全局 Python 变量,这使得依赖其值的生命周期变得很危险。)
源代码编码
当字符串文字出现在代码中时,源代码编码很重要。它确定 Cython 将在字节文字的 C 代码中存储的字节序列,以及在解析字节编码的源文件时 Cython 为 unicode 文字构建的 Unicode 代码点。在 PEP 263 之后,Cython 支持源文件编码的显式声明。例如,将以下注释放在ISO-8859-15
(Latin-9)编码源文件的顶部(进入第一行或第二行)需要在解析器中启用ISO-8859-15
解码:
# -*- coding: ISO-8859-15 -*-
当没有提供明确的编码声明时,源代码被解析为 UTF-8 编码的文本,如 PEP 3120 所指定的。 UTF-8 是一种非常常见的编码,可以表示整个 Unicode 字符集,并且与有效编码的纯 ASCII 编码文本兼容。这使得它成为通常主要由 ASCII 字符组成的源代码文件的非常好的选择。
例如,将以下行放入 UTF-8 编码的源文件中将打印5
,因为 UTF-8 对双字节序列'\xc3\xb6'
中的字母'ö'
进行编码:
print( len(b'abcö') )
而以下ISO-8859-15
编码的源文件将打印4
,因为编码仅使用此字母的 1 个字节:
# -*- coding: ISO-8859-15 -*-
print( len(b'abcö') )
请注意,unicode 文字u'abcö'
在两种情况下都是正确解码的四字符 Unicode 字符串,而未加前缀的 Python str
文字'abcö'
将成为 Python 2 中的字节字符串(因此长度为 4)或者在上面的示例中为 5),以及 Python 3 中的 4 个字符的 Unicode 字符串。如果您不熟悉编码,则在首次阅读时可能看起来不太明显。有关详细信息,请参阅 CEP 108 。
根据经验,最好避免使用未加前缀的非 ASCII str
文字,并对所有文本使用 unicode 字符串文字。 Cython 还支持__future__
import unicode_literals
,它指示解析器将源文件中所有未加前缀的 str
文字读取为 unicode 字符串文字,就像 Python 3 一样。
单字节和字符
Python C-API 使用普通的 C char
类型来表示字节值,但它有两个特殊的整数类型用于 Unicode 代码点值,即单个 Unicode 字符: Py_UNICODE
和 Py_UCS4
。 Cython 支持第一个本地,支持 Py_UCS4
是 Cython 0.15 的新功能。 Py_UNICODE
定义为无符号 2 字节或 4 字节整数,或定义为wchar_t
,具体取决于平台。确切类型是 CPython 解释器构建中的编译时选项,扩展模块在 C 编译时继承此定义。 Py_UCS4
的优点在于,无论平台如何,它都可以保证足够大,以适应任何 Unicode 代码点值。它被定义为 32 位无符号整数或长整数。
在 Cython 中,char
类型在强制转换为 Python 对象时与 Py_UNICODE
和 Py_UCS4
类型的行为不同。类似于 Python 3 中字节类型的行为,char
类型默认强制为 Python 整数值,因此以下打印 65 而不是A
:
# -*- coding: ASCII -*-
cdef char char_val = 'A'
assert char_val == 65 # ASCII encoded byte value of 'A'
print( char_val )
如果你想要一个 Python 字节字符串,你必须明确地请求它,以下将打印A
(或 Python 3 中的b'A'
):
print( <bytes>char_val )
显式强制适用于任何 C 整数类型。超出char
或unsigned char
范围的值将在运行时升高 OverflowError
。在分配类型变量时,强制也会自动发生,例如:
cdef bytes py_byte_string
py_byte_string = char_val
另一方面, Py_UNICODE
和 Py_UCS4
类型很少在 Python unicode 字符串的上下文之外使用,因此它们的默认行为是强制使用 Python unicode 宾语。因此,以下将打印字符A
,与 Py_UNICODE
类型的代码相同:
cdef Py_UCS4 uchar_val = u'A'
assert uchar_val == 65 # character point value of u'A'
print( uchar_val )
同样,显式转换将允许用户覆盖此行为。以下将打印 65:
cdef Py_UCS4 uchar_val = u'A'
print( <long>uchar_val )
请注意,转换为 C long
(或unsigned long
)将正常工作,因为 Unicode 字符可以具有的最大代码点值是 1114111(0x10FFFF
)。在 32 位或更高的平台上,int
同样出色。
窄 Unicode 构建
在版本 3.3 之前的 CPython 的窄版本构建中,即在sys.maxunicode
为 65535 的情况下构建(例如所有 Windows 构建,而不是宽版本中的 1114111),仍然可以使用不适合的字符串代码点。 16 位宽 Py_UNICODE
类型。例如,这样的 CPython 构建将接受 unicode 文字u'\U00012345'
。但是,在这种情况下,底层系统级编码泄漏到 Python 空间中,因此该文字的长度变为 2 而不是 1.这也显示在迭代它或索引到它时。在这个例子中,可见的子串是u'\uD808'
和u'\uDF45'
。它们形成了代表上述特征的所谓代理对。
有关该主题的更多信息,请阅读 Wikipedia 关于 UTF-16 编码的文章。
相同的属性适用于为窄 CPython 运行时环境编译的 Cython 代码。在大多数情况下,例如在搜索子字符串时,可以忽略此差异,因为文本和子字符串都将包含代理项。因此,大多数 Unicode 处理代码也可以在窄版本上正常工作。编码,解码和打印将按预期工作,因此上述文字在窄和宽 Unicode 平台上变成完全相同的字节序列。
但是,程序员应该知道单个 Py_UNICODE
值(或 CPython 中的单个’字符’unicode 字符串)可能不足以在窄平台上表示完整的 Unicode 字符。例如,如果在 unicode 字符串中对u'\uD808'
和u'\uDF45'
的独立搜索成功,则这并不一定意味着字符u'\U00012345
是该字符串的一部分。很可能是字符串中的两个不同的字符碰巧与所讨论的字符的代理对共享代码单元。寻找子串正常工作是因为代理对中的两个代码单元使用不同的值范围,因此该对在代码点序列中始终是可识别的。
从版本 0.15 开始,Cython 扩展了对代理对的支持,因此即使在狭窄的平台上,您也可以安全地使用in
测试从完整的 Py_UCS4
范围中搜索字符值:
cdef Py_UCS4 uchar = 0x12345
print( uchar in some_unicode_string )
类似地,它可以在窄和宽 Unicode 平台上将具有高 Unicode 代码点值的一个字符串强制转换为 Py_UCS4 值:
cdef Py_UCS4 uchar = u'\U00012345'
assert uchar == 0x12345
在 CPython 3.3 及更高版本中, Py_UNICODE
类型是系统特定wchar_t
类型的别名,不再与 Unicode 字符串的内部表示相关联。相反,任何 Unicode 字符都可以在所有平台上表示,而无需使用代理项对。这意味着无论 Py_UNICODE
的大小如何,该版本都不再存在窄版本。有关详细信息,请参阅 PEP 393 。
只要将类型推断应用于无类型变量或者在源代码中明确使用可移植 Py_UCS4
类型,Cython 0.16 及更高版本内部处理此更改并对单个字符值执行正确的操作平台特异性 Py_UNICODE
型。 Cython 应用于 Python unicode 类型的优化将在 C 编译时自动适应 PEP 393 ,像往常一样。
迭代
只要循环变量被适当地键入,Cython 0.13 就支持对char*
,字节和 unicode 字符串的有效迭代。所以以下将生成预期的 C 代码:
cdef char* c_string = "Hello to A C-string's world"
cdef char c
for c in c_string[:11]:
if c == 'A':
print("Found the letter A")
这同样适用于字节对象:
cdef bytes bytes_string = b"hello to A bytes' world"
cdef char c
for c in bytes_string:
if c == 'A':
print("Found the letter A")
对于 unicode 对象,Cython 会自动将循环变量的类型推断为 Py_UCS4
:
cdef unicode ustring = u'Hello world'
# NOTE: no typing required for 'uchar' !
for uchar in ustring:
if uchar == u'A':
print("Found the letter A")
自动类型推断通常会在这里产生更高效的代码。但是,请注意,某些 unicode 操作仍然需要将值作为 Python 对象,因此 Cython 最终可能会为循环内部的循环变量值生成冗余转换代码。如果这导致特定代码段的性能下降,您可以显式地将循环变量键入为 Python 对象,或者将其值赋值给循环内部的 Python 类型变量,以在运行 Python 之前强制执行一次性强制对它的操作。
in
测试也有优化,因此以下代码将在纯 C 代码中运行(实际上使用 switch 语句):
cpdef void is_in(Py_UCS4 uchar_val):
if uchar_val in u'abcABCxY':
print("The character is in the string.")
else:
print("The character is not in the string")
结合上面的循环优化,这可以产生非常有效的字符切换代码,例如,在 unicode 解析器中。
Windows 和宽字符 API
Windows 系统 API 本身以零终止 UTF-16 编码wchar_t*
字符串的形式支持 Unicode,因此称为“宽字符串”。
默认情况下,Windows 版本的 CPython 定义 Py_UNICODE
作为wchar_t
的同义词。这使得内部unicode
表示与 UTF-16 兼容,并允许有效的零拷贝转换。这也意味着 Windows 构建总是窄 Unicode 构建与所有警告。
为了帮助与 Windows API 互操作,Cython 0.19 支持宽字符串(以Py_UNICODE*
的形式)并隐式地将它们转换为unicode
字符串对象。这些转换的行为与char*
和 bytes
相同,如传递字节串中所述。
除自动转换外,C 语境中出现的 unicode 文字成为 C 级宽字符串文字, len()
内置函数专门用于计算零终止Py_UNICODE*
字符串或数组的长度。
以下是如何在 Windows 上调用 Unicode API 的示例:
cdef extern from "Windows.h":
ctypedef Py_UNICODE WCHAR
ctypedef const WCHAR* LPCWSTR
ctypedef void* HWND
int MessageBoxW(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, int uType)
title = u"Windows Interop Demo - Python %d.%d.%d" % sys.version_info[:3]
MessageBoxW(NULL, u"Hello Cython \u263a", title, 0)
警告
强烈建议不要在 Windows 之外使用Py_UNICODE*
字符串。 Py_UNICODE
本身在不同平台和 Python 版本之间不可移植。
CPython 3.3 已经转移到 unicode 字符串( PEP 393 )的灵活内部表示,使得所有 Py_UNICODE
相关 API 被弃用,效率低下。
har c
for c in c_string[:11]:
if c == ‘A’:
print(“Found the letter A”)
这同样适用于字节对象:
```py
cdef bytes bytes_string = b"hello to A bytes' world"
cdef char c
for c in bytes_string:
if c == 'A':
print("Found the letter A")
对于 unicode 对象,Cython 会自动将循环变量的类型推断为 Py_UCS4
:
cdef unicode ustring = u'Hello world'
# NOTE: no typing required for 'uchar' !
for uchar in ustring:
if uchar == u'A':
print("Found the letter A")
自动类型推断通常会在这里产生更高效的代码。但是,请注意,某些 unicode 操作仍然需要将值作为 Python 对象,因此 Cython 最终可能会为循环内部的循环变量值生成冗余转换代码。如果这导致特定代码段的性能下降,您可以显式地将循环变量键入为 Python 对象,或者将其值赋值给循环内部的 Python 类型变量,以在运行 Python 之前强制执行一次性强制对它的操作。
in
测试也有优化,因此以下代码将在纯 C 代码中运行(实际上使用 switch 语句):
cpdef void is_in(Py_UCS4 uchar_val):
if uchar_val in u'abcABCxY':
print("The character is in the string.")
else:
print("The character is not in the string")
结合上面的循环优化,这可以产生非常有效的字符切换代码,例如,在 unicode 解析器中。
Windows 和宽字符 API
Windows 系统 API 本身以零终止 UTF-16 编码wchar_t*
字符串的形式支持 Unicode,因此称为“宽字符串”。
默认情况下,Windows 版本的 CPython 定义 Py_UNICODE
作为wchar_t
的同义词。这使得内部unicode
表示与 UTF-16 兼容,并允许有效的零拷贝转换。这也意味着 Windows 构建总是窄 Unicode 构建与所有警告。
为了帮助与 Windows API 互操作,Cython 0.19 支持宽字符串(以Py_UNICODE*
的形式)并隐式地将它们转换为unicode
字符串对象。这些转换的行为与char*
和 bytes
相同,如传递字节串中所述。
除自动转换外,C 语境中出现的 unicode 文字成为 C 级宽字符串文字, len()
内置函数专门用于计算零终止Py_UNICODE*
字符串或数组的长度。
以下是如何在 Windows 上调用 Unicode API 的示例:
cdef extern from "Windows.h":
ctypedef Py_UNICODE WCHAR
ctypedef const WCHAR* LPCWSTR
ctypedef void* HWND
int MessageBoxW(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, int uType)
title = u"Windows Interop Demo - Python %d.%d.%d" % sys.version_info[:3]
MessageBoxW(NULL, u"Hello Cython \u263a", title, 0)
警告
强烈建议不要在 Windows 之外使用Py_UNICODE*
字符串。 Py_UNICODE
本身在不同平台和 Python 版本之间不可移植。
CPython 3.3 已经转移到 unicode 字符串( PEP 393 )的灵活内部表示,使得所有 Py_UNICODE
相关 API 被弃用,效率低下。
CPython 3.3 更改的一个结果是unicode
字符串的 len()
总是在 代码点(“字符”)中测量,而 Windows API 期望 UTF-16 的数量 代码单元(每个代理单独计算)。要始终获得代码单元的数量,请直接调用 PyUnicode_GetSize()
。