美文网首页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的函数与执行原理

    我们从动态变量和静态变量中学到的许多知识也适用于函数。Python和C函数具有一些共同的属性:它们(通常)都具有名...

  • JS async 函数深究

    实现原理 async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。 执行顺序问...

  • ios 程序的启动原理

    ios 程序启动原理:1.执行mian函数,在main函数中会执行UIApplicationMain函数,UIAp...

  • iOS 程序启动原理和UIApplication

    程序启动原理: 1.执行main函数,进而执行main里面的UIApplicationMain函数。 2.通过系统...

  • Golang 函数

    匿名函数 回调函数 闭包 defer 延迟执行函数 多个defer的时候 先延迟的最后执行 先进后出栈原理...

  • Cython系列教程:二. 构建Cython代码

    1 Cython的代码的构建过程 Cython的代码是需要编译的,与C/C++类似,用Cython写的.pyx文件...

  • dajngo中间件和装饰器的概念

    一 原理 1. 装饰器,是利用闭包的原理去更改一个函数的功能,可以理解为让一个函数执行之前,去另外一个函数里面执行...

  • JS异步之宏队列与微队列

    原理图 js单线程执行,首先执行主线程stack里面所有的代码,执行完成之后执行队列里面的函数。队列里面的函数优先...

  • 函数防抖和节流

    一、什么是函数节流 函数节流:是确保函数特定的时间内至多执行一次。 1.函数节流的原理 函数节流的原理挺简单的,当...

  • 应用程序启动原理

    程序启动原理及步骤: 1,执行main函数 -->UIApplicationMain UIApplicationM...

网友评论

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

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