美文网首页IT狗工作室
第3篇:Cython的函数与执行原理

第3篇:Cython的函数与执行原理

作者: 铁甲万能狗 | 来源:发表于2020-04-16 22:16 被阅读0次

    我们从动态变量和静态变量中学到的许多知识也适用于函数。Python和C函数具有一些共同的属性:它们(通常)都具有名称,采用零个或多个参数,并且在调用时可以返回新值或对象。 但是Python函数更加灵活和强大。 Python函数是一种特殊的对象,这意味着它们是具有状态和行为。 这种抽象非常有用

    我们来回顾一下Python的函数的一些特性

    • 在导入时和在运行时动态创建;
    • 用lambda关键字匿名创建;
    • 在另一个函数(或其他嵌套范围)内定义;
    • 从其他函数返回;
    • 作为参数传递给其他函数;
    • 用位置或关键字参数调用;
    • 使用默认值定义

    Cython支持的函数分类

    现在我们在概念做一些约定,我们知道Cython支持三种函数

    • 由def关键字定义的函数,我们称为原生的Python函数
    • 由cdef关键字定义的函数,我们成为C函数Cython函数
    • 由cpdef关键字定义的函数,我们成为混合函数
    • 由def关键子定义的函数,函数体内出现关键字定义的C类型的参数或局部变量,这样的函数是混合函数的特殊形式

    C函数具有最低的调用开销,并且比Python函数快好几个数量级,但它具有一些特点局限性

    • 可以作为参数传递给其他函数

    C函数的限制

    • 不能在另一个函数中定义
    • 具有不可修改的静态分配名称
    • 仅接受位置参数
    • 不支持参数的默认值

    Python函数的所有功能和灵活性都需要付出一定的代价:Python函数比C函数要慢几个数量级,甚至是不带参数的函数。Cython支持Python和C函数,并允许它们以自然和直接的方式相互调用,所有这些都在同一源文件中。

    Cython中带有def关键字的Python函数

    Cython支持使用def关键字定义的常规Python函数,并且它们可以像我们期望的那样工作。 例如,考虑一个sieve_of_ethen函数,该函数返回传入整数n之前的所有质数组成的一个列表,我们定义一个这样的函数,并保存到一个叫primers.pyx的文件中

    def sieve_of_ethen(n):
        pr = [True for i in range(n + 1)]
        p = 2
        res=list()
        
        while (p * p <= n):
            if (pr[p] == True):
                for i in range(p * p, n + 1, p):
                    pr[i] = False
                #end-for
            #end-if
            p += 1
        #end-while
        
        for p in range(2,n):
            if pr[p]:
                res.append(p)
            #end-if
        #end-for
        return res
    #end-def
    

    这个简单的Python函数是有效的Cython代码。在Cython中,n参数是一个动态Python变量,并且在调用时必须将其传递给Python对象。sieve_of_ethen的使用方式相同,无论它是在纯Python中定义还是在Cython中定义并从扩展模块导入。

    我们通过?来查看模块的方法名称,显示类型信息为builtin_function_or_method,表示Cython编译器已经将Python函数编译为C函数了


    我们尝试导入纯Python版本的py_primer模块,如下图

    再次查看模块中的函数类型,类型信息仅显示为function,Cython编译器没有对.py文件中的函数进行编译

    不难发现Cython编译器的行为特征:Cython编译仅会对pyx文件中的任何类型的函数尝试进行编译

    此时,我们可以比较好奇,究竟原生的Python函数(仅被Python解释器执行)和被Cython编译器处理过的Python函数,它们两者之间的性能差异有多大?
    我们可以通过Jupyter NoteBook的魔术方法%timeit进行比较

    对于该系统上较小的输入值,尽管Cython的cy_primer.sieve_of_ethen()函数的运行速度取决于许多因素,但cy_primer.sieve_of_ethen()函数的运行速度大约比py_primer.sieve_of_ethen()快42.32%。 加速的根本原因在于消除了Cython中的解释开销和减少的函数调用开销

    就用法而言,py_primer模块和cy_primer模块中的函数是相同的。 在实现方面,这两个函数有一些重要的区别。

    • Python版本具有类型是Function,而Cython版本具有Builtin_function_or_method类型。
    • Python版本具有几个可修改的属性(例如name)而Cython版本不可修改。
    • 当被调用时,Python版本使用Python解释器执行字节码,而Cython版本运行已编译的C代码,这些代码调用Python / CAPI,完全绕开了字节码解释。
    字节码解析过程是非常低效率的

    Cython中的任意类型函数的参数类型静态化

    在这里,我们静态类型n。因为n是一个函数参数,所以我们省略了cdef关键字。当我们从Python调用sieve_of_ethen时,Cython会将Python对象参数转换为C的long类型,如果不能,则引发一个适当的异常(TypeError或OverflowError),这里我们定义下面的函数签名为sieve_of_ethen_v2(long n)

    #cython:language_level=3
    def sieve_of_ethen_v2(long n):
        """返回给定小于整数N的所有质数"""
        pr = [True for i in range(n + 1)]
        p = 2
        res=list()
        
        while (p * p <= n):
            if (pr[p] == True):
                for i in range(p * p, n + 1, p):
                    pr[i] = False
                #end-for
            #end-if
            p += 1
        #end-while
        
        for k in range(2,n):
            if pr[k]:
                res.append(k)
            #end-if
        #end-for
        return res
    #end-def
    

    在Cython中定义任何函数时,我们可能会混合使用动态类型的Python对象参数和静态类型的参数。 Cython允许静态类型的参数具有默认值,并且静态类型的参数可以按位置或通过关键字传递。,我们来再次运行一下修改后的py_primer.sieve_of_ethen_v2最新版本,此时我们尝试运行后被上一次的测试快了一些,也是不错的改进。


    ss8.png

    Cython中的C函数

    当用于定义函数时,cdef关键字创建具有C调用语义的函数。cdef函数的参数和返回类型通常是静态类型的,它们可以处理C指针对象、struct和其他不能自动强制为Python类型的C类型。将cdef函数看作用Cython类似Python的语法定义的C函数是很巧妙的想法

    #cython:language_level=3
    cdef long sieve_of_ethen_v3(long n):
        """返回给定小于整数N的所有质数"""
        pr = [True for i in range(n + 1)]
        p = 2
        res=list()
        
        while (p * p <= n):
            if (pr[p] == True):
                for i in range(p * p, n + 1, p):
                    pr[i] = False
                #end-for
            #end-if
            p += 1
        #end-while
        
        for k in range(2,n):
            if pr[k]:
                res.append(k)
            #end-if
        #end-for
        return res
    #end-def
    

    仔细检查前面的示例中的c_fact可以发现,参数类型和返回类型是静态声明的,并且不使用任何Python对象。因此,无需从Python类型转换为C类型。 调用c_fact函数与调用纯C函数一样有效,因此该函数的调用开销很小。没有什么可以阻止我们在cdef函数中声明和使用Python对象和动态变量,或者将它们作为参数接受。但是,当我们想要尽可能接近C而又不直接编写C代码时,通常会使用cdef函数.

    Cython允许在同一Cython源文件中将cdef函数与Python版本的def函数一起定义。 cdef函数的可选返回类型可以是我们看到的任何静态类型,包括指针,结构体,C的数组和静态Python类型(例如list或dict)。 我们还可以有一个void的返回类型。如果省略了返回类型,则默认为对象。

    Cython对C函数的封装行

    当我们从外部Python代码调用Cython中的C函数sieve_of_ether_v3,会出现AttributeError错误。因为Cython的C函数在编译后对外部的Python代码调用是不可见的。

    经常看到一些Python读物谈论Python对代码实现如何做到封装,事实上,Python写的任何函数和类中的方法或属性没封装可言,Python不存在像C++/JAVA有类似private/protect/public等关键字的访问控制,Python在模块内的函数,类方法和属性,对外部调用它的代码都是公开的,这种公开包括:

    • 所有函数名以及函数名的具体实现
    • 所有类的属性和方法名称,以及类方法的具体实现

    但对于Cython程序来说,由于Cython集成了C和大部份C++的主要特性,因此Cython程序编写的函数,具有真正意义上的封装性。

    • 在同一Cython源文件中的任何其他函数(def或cdef)都可以调用用cdef声明的函数(了解如何放松此约束)。
    • Cython不允许从外部Python代码调用cdef函数,由于此限制,cdef函数通常用作快速辅助函数,以帮助def函数完成其工作。
    • Cython允许外部Python代码调用Cython中的Python函数,但Cython中的Python函数具体实现是编译后的C函数。

    基于上面的分析,我们在同一个pyx文件中定义一个Python函数primers_by_py,并且通过它调用C函数,因为Python函数对于外部Python代码调用是可见的

    #cython:language_level=3
    
    def primers_by_py(long n):
        """返回给定小于整数N的所有质数"""
        return sieve_of_ethen_v3(n)
    
    cdef list sieve_of_ethen_v3(long n):
        """返回给定小于整数N的所有质数"""
        pr = [True for i in range(n + 1)]
        p = 2
        res=list()
        while (p * p <= n):
            if (pr[p] == True):
                for i in range(p * p, n + 1, p):
                    pr[i] = False
                #end-for
            #end-if
            p += 1
        #end-while
        
        for k in range(2,n):
            if pr[k]:
                res.append(k)
            #end-if
        #end-for
        return res
    #end-def
    

    Ok,我们再次运行可以看到修改,速度上比之前Cython编译后的Python函数sieve_of_ethen_v2稍微慢了0.04秒,对于测试的数据规模10,000,000,我认为还可以接受。通常Python代码对Cython中的C函数间接调用会比Cython中的Python函数会快很多,本文是一个特例,因为这会跟算法本身有关,由于

    Cython中混合函数

    还有第三种函数,用cpdef关键字声明,它是def和cdef的混合。cpdef函数结合了其他两种函数的特性,并解决了它们的许多局限性。在上一节中,我们通过编写一个def包装函数primers_by_py使cdef函数sieve_of_ethen_v3对Python可用,该函数只需将其参数转发到primers_by_py并返回其结果。一个cpdef函数会自动为我们提供这两个函数:一个是该函数的C版本,另一个是它的Python包装器,两个都有相同的名称。当我们从Cython调用函数时,我们调用该函数的C版本;当我们从Python调用函数时,该函数的包装器被调用。这样,cpdef函数将def函数的可访问性与cdef函数的性能结合起来。

    #cython:language_level=3
    cpdef list sieve_of_ethen_v4(long n):
        """返回给定小于整数N的所有质数"""
        pr = [True for i in range(n + 1)]
        p = 2
        res=list()
        while (p * p <= n):
            if (pr[p] == True):
                for i in range(p * p, n + 1, p):
                    pr[i] = False
                #end-for
            #end-if
            p += 1
        #end-while
        
        for k in range(2,n):
            if pr[k]:
                res.append(k)
            #end-if
        #end-for
        return res
    #end-def
    

    测试还如下图


    ss8.png

    待更新.....

    相关文章

      网友评论

        本文标题:第3篇:Cython的函数与执行原理

        本文链接:https://www.haomeiwen.com/subject/wzdcvhtx.html