-
说明
尝试翻译Cython Documentation以助学习。
水平有限,乐迎指正;文档首页:《Cython官方文档中文翻译》
-
Cython基础
Cython
的根本特性可以总结如下:Cython
是带有C
数据类型的Python
Cython
就是Python
:Python
的几乎所有代码都是有效的Cython代码。(虽然也有一些局限( Limitations),但就目前而言还是非常接近的)Cython
编译器将代码转为C代码
,从而实现对Python/C API
的平等调用。因为变量和参数可声明为
C数据类型
,故Cython
能量远不止此。控制Python值
和C值
的代码可以自由融合,当需要时则自动转换。Python
的引用计数维护和错误检查以及全部错误处理功能也都是自动的,其中就包括try-except
、try-finally
语句(即便是在处理C数据的过程中) -
Cython实现Hello World
鉴于
Cython
可以识别任意有效的python源文件,开始时的困难之一就是搞懂如何编译你的扩展。让我们从标准的python
hello world
开始:print("Hello World")
将上述代码保存在一个
helloworld.pyx
文件内,然后创建setup.py
文件,这是一个Makefile(更详细了解参见 Source Files and Compilation),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
上述命令会在本地文件夹内创建一个
helloworld.so
(unix系统)或helloworld.pyx
(windows系统)文件。现在来使用这个文件,打开一个Python 解释器(在命令行中cmd > python
),之后类似引入常规模块一样import
这个文件:>>> import helloworld Hello world
恭喜你!你现在就知道如何构建一个Cython扩展了。但是,这个例子并没有体现出使用Cython的必要性,下面让我们来看一个更实用的例子。
译者注:详细实现过程及可能出现问题,参见《Cython使用案例之:输出Hello World》
-
pyximport:面向开发人员的Cython编译
如果你的模块无需其他C库或特殊的构建设置(build setup),那么你可以采用
pyximport
模块,最初由Paul Prescod开发,用import直接导入*.pyx文件,这样就避免每次更新代码后都单独重新执行运行setup.py的动作。与Cython*绑定安装,实用方法如下:>>> import pyximport; pyximport.install() >>> import helloworld
译者注:上述代码在cmd > python解释器中运行,会直接实现编译过程并导入,文件夹中并不生成
.pyx
、.c
文件。Pyximport模块还对普通Python模块提供了实验性编程支持,这允许你在每一个
Python
导入(import)的*.pyx和.py模块上运行Cython
,包括标准库和后续安装的包。当然,还有很多模块是Cython
编译不了的,在这种情况下,import机制将返回去加载Python
源模块。.py*的导入机制如下:>>> pyximport.install(pyimport=True)
不建议在最终用户端实用*Pyximport构建代码,因为其与import*系统挂钩。适合最终用户端的方式是提供以轮包(wheel packaging format)形式存在的预构建(pre-built)的二进制包。
-
斐波那契(Fibonacci)的乐趣
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例子的步骤,先创建一个*.pyx扩展,比如fib.pyx*,然后创建一个setup.py,直接实用Hello World例中的setup.py,只需要更改
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
译者注:
同样,可以采用pyximport来实现上述编译过程:
# 第一步:命令行进入python $ py # 本机需要用py表示python3 # 第二步:在python中采用pyximport编译 >>> import pyximport;pyximport.install() >>> fib # 第三步:在python内调用fib模块的fib函数 >>> fib.fib(2000) 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597
-
质数
这里是个小例子,展示Cython功能。也是一个常规功能:找质数(素数)的个数。
你告诉他你需要多少个质数,它给你返回一个包含所需质数的list:
prime.pyx文件
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 # p中当前元素数量 n = 2 while len_p < nb_primes: # 判断n是否质数 for i in p[:len_p]: if n % i == 0: break # 如果上述循环未中断程序,那我们就得到了一个质数 else: p[len_p] = n len_p += 1 n += 1 # 以Python list形式返回结果 result_as_list = [prime for prime in p[:len_p]] return result_as_list
乍看类似常规Python函数定义,不同在于指定参数
nb_primes
类型为int,意味着传入对象会转变为C integer(如果不能转换则报错TypeError)让我们深挖函数的核心:
cdef int n, i, len_p cdef int p[1000]
上述两行用
cdef
声明定义一些本地C变量。计算结果在过程中暂存在C arrayp
中,最后拷贝到Python list中。你不能以此方式创建过大的arrays,因为他们是在C函数调用堆栈(call stack)上调用的,这是稀有资源。如需更大的arrays或是只有运行时才能知道arrays长度,你需要学习一下Cython如何有效利用C memory allocation, Python arrays or NumPy arrays 。
if nb_primes > 1000: nb_primes = 1000
如C语言中,声明一个静态数组(static array)需要提前知晓编译时其大小。
len_p = 0 # p中元素数量 n = 2 while len_p < nb_primes:
上述代码设置了一个循环,用于测试候选数目直到找到所需的质数个数。
# 判断n是否时质数? for i in p[:len_p]: if n % i == 0: break
上述前两行,用于区分候选n与已有素数,这一步很特别,因为没有涉及任何Python对象,整个循环完全转成C代码,也因此运行极快,我们用
p
C array遍历:for i in p[:len_p]:
上述循环看起来时对Python list或Numpy进行迭代,实际被转换成速度更快的C循环,如果不用*[:len_p]对C数组进行切片,则Cython会直接对数组的1000*个元素进行遍历。
# 如果没有从循环中退出 else: p[len_p] = n len_p += 1 n += 1
如果***break***未运行,说明我们找到了一个质数,之后就运行上述代码块。将找到的质数加到
p
中。如果你觉得在**for循环后用else挺奇怪,只需要知道这是Python语言少有人了解的特征就好了,Cython会以C的速度执行它。如果还是对for-else语法感到困惑,参见这个牛逼的博客( blog post)。# 将结果导入python list result_as_list = [prime for prime in p[:len_p]] return result_as_list
如上,在返回结果之前,我们像将其从C array复制到Python list中,因为Python无法读取C arrays。Cython可以在C类型与Python类型之间自动转换(详情参见 type conversion),因此这里我们可以用一个简单的列表理解来将C int值复制到Cython自动创建的Python int 对象的Python list中。你也可以手动遍历C array,并采用*result_as_list.append(prime)*方式,结果是一样的。
你应该留意到了我们采用与
Python
相同的方式声明了一个Python list。因为变量result_as_list尚未明确声明类型,只是假设其承载一个Python对象,从赋值过程中,Cython
也知晓了其确切类型是Python list。译者注:
第一句原文是,
You’ll notice we declare a Python list exactly the same way it would be in Python.
直译是我们用Python
声明了一个Python list,乍听有点迷惑,我的理解是,Cython
作为一种与Python
、C
同等级的语言,其内面对两种类型:C的数据类型和Python的数据类型,此处是说用Cython语言
声明了一个Python list对象,用法与在Python语言
中声明Python list对象一样。最后,一个常规的
Python
return声明返回了return list。用Cython编译器将primes.pyx编译生成一个扩展模块,我们可以在交互式解释器中使用此模块:
>>> import primes >>> primes.primes(10) [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
成功运行如上,如果你想了解Cython到底节省多大程度的工作量,看一眼为此模块生成的C代码。
Cython
有一种可视化方法,可以了解与Python对象、Python’s C-API的交互位置。如要使用,给cythonize()传入annotate=True即可,它会生成一个HTML文件:白色的行,意思是生成的代码无需与Python交互,因此运行速度与C代码一样快。
黄色越深的行,意味着与Python的交互越多。
这些黄色的行通常是对Python对象做操作、报错、或与直接转成C代码相比而言更高层级的操作。函数声明和返回,使用Python解释器,因此这些行会变成黄色。对于列表理解(list comprehension)也是一样,因为其包含了Python对象的创建。
那么
if n % i == 0:
呢?我们可以检查生成的C代码来理解:我们可以看到其中有检查环节,因为Cython默认为Python行为,语言在运行时会执行除法检查,就行Python一样。你可以通过 compiler directives来作废这些检查。
下面我们来看一下
if
,即便我们用了除法检查,我们还是得到了速度上的提升。让我们用Python
复现同等功能的代码:def primes_python(nb_primes): p = [] n = 2 while len(p) < nb_primes: for i in p: if m % i == 0: break else: p.append(n) n += 1 return p
用
Cython
直接编译.py
文件也是可行的。现在,我们将上述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
cythonize之后的primes_python与
Python版本
相比,代码无需做任何改动,速度就快2倍;Cython版本
则是Python版本
的13倍;原因何在?众多:
- 这段代码中,每行的计算量不大。因此,Python解释器的开销很重要。如果每行的计算量很大的,结果会完全不同
- 数据局部性(data locality)。与使用
Python
相比使用C
时可以更多使用CPU缓存。因为Python
中万物皆对象,而每个对象被实施为一个字典,这对缓存不太友好。
通常,提速在2-1000倍之间,取决于你调用Python解释器的程度。一如既往,谨记,无论何处,添加类型前先profile。添加类型会降低代码可读性,因此要恰当使用。
-
用C++计算质数
Cython
还可以充分利用C++
,特别是,Cython代码可直接引入部分C++标准库。先看看当使用C++标准库中的向量( vector )时,我们的primes.pyx变成了什么?
C++中的Vector是一种基于可变大小C array的可以实现list或stack的数据结构。在array标准库模块中类似Python array类型。如果你提前知晓需要放入vector多少元素,可以采用reverse方法避免复制。更多详情参见 this page from cppreference
# 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++标准库
。当然,并不能用pyximport
将Cython代码编译成C++。只能采用setup.py或notebook来运行此例/vector的API与Python list的API类似,有时可在
Cython
中用作替代。Cython
使用C++
的更多详情,参见 Using C++ in Cython。 -
语言细节
关于
Cython
这门语言的更多内容,参见 Language Basics。在数值计算上下文中熟练使用
Cython
,参见 Typed Memoryviews。 -
References
Cython官方文档中文翻译:初级教程
最新推荐文章于 2024-06-29 11:31:26 发布