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