Cython官方文档中文翻译:初级教程

  • 说明

    尝试翻译Cython Documentation以助学习。

    水平有限,乐迎指正;文档首页:《Cython官方文档中文翻译

  • Cython基础

    Cython的根本特性可以总结如下:Cython是带有C数据类型的Python

    Cython就是PythonPython的几乎所有代码都是有效的Cython代码。(虽然也有一些局限( Limitations),但就目前而言还是非常接近的)Cython编译器将代码转为C代码,从而实现对Python/C API的平等调用。

    因为变量和参数可声明为C数据类型,故Cython能量远不止此。控制Python值C值的代码可以自由融合,当需要时则自动转换。Python的引用计数维护和错误检查以及全部错误处理功能也都是自动的,其中就包括try-excepttry-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.sounix系统)或helloworld.pyxwindows系统)文件。现在来使用这个文件,打开一个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 array p中,最后拷贝到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 listNumpy进行迭代,实际被转换成速度更快的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 arraysCython可以在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作为一种与PythonC同等级的语言,其内面对两种类型: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_pythonPython版本相比,代码无需做任何改动,速度就快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的可以实现liststack的数据结构。在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++标准库。当然,并不能用pyximportCython代码编译成C++。只能采用setup.pynotebook来运行此例/

    vectorAPIPython listAPI类似,有时可在Cython中用作替代。

    Cython使用C++的更多详情,参见 Using C++ in Cython

  • 语言细节

    关于Cython这门语言的更多内容,参见 Language Basics

    在数值计算上下文中熟练使用Cython,参见 Typed Memoryviews

  • References

  1. 原文链接
  2. Cython使用案例之:输出Hello World
  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值