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

我们从动态变量和静态变量中学到的许多知识也适用于函数。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

cpdef函数有一个限制,因为它兼有Python和C函数的双重功能:它的参数和返回类型必须与Python和C类型兼容。任何Python对象都可以在C级别表示(例如,通过使用动态类型的参数,或通过静态类化内置类型),但并非所有C类型都可以在Python中表示。因此,我们不能随意使用void、C指针或C数组作为cpdef函数的参数类型或返回类型。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,084评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,623评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,450评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,322评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,370评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,274评论 1 300
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,126评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,980评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,414评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,599评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,773评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,470评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,080评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,713评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,852评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,865评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,689评论 2 354

推荐阅读更多精彩内容