《Cython系列》4. Cython中的扩展类
Posted traditional
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《Cython系列》4. Cython中的扩展类相关的知识,希望对你有一定的参考价值。
楔子
在之前的博客中,我们介绍了Cython给Python添加的一些基础特性,以及它们所提供的功能和用法。但是Cython还可以增强Python的类,不过在学习之前我们需要了解一下Python的类和扩展类之前的区别。
Python类和扩展类之间的差异
首先Python中"一切皆对象",怎么理解呢?首先在最基本的层次上,一个对象有三样东西:地址、值、类型,我们通过id函数可以获取地址并将每一个对象都区分开来,使用type获取类型。Python中对象有很多属性,这些属性都放在自身的属性字典里面,这个字典可以通过__dict__
获取。我们调用对象的某一个属性的时候,可以通过.
的方式来调用。Python也允许我们通过class关键字自定义一个类。
在这一节,我们将会学习如何使用Cython操纵Python中的类。
首先Python中内置了很多的类,tuple、dict等等,这些类在C一级通过Python/C api直接包含在了Python运行时中。但就使用而言,它和我们自己使用class定义的类是没有什么区别的,如果非要说区别的话,那就是内置的类的一些属性、以及内置的类里面的方法的属性是没法修改的。当然我们删除一个属性、添加一个属性也是不可以的。
class A:
pass
print(A.__name__) # A
A.__name__ = "B"
print(A.__name__) # B
try:
int.__name__ = "INT"
except Exception as e:
print(e) # can‘t set attributes of built-in/extension type ‘int‘
正如之前说的那样,我们除了在Python中定义类,还可以直接使用Python/C api在C级别创建自己的类型,这样的类型称之为扩展类、或者扩展类型(说白了在C中实现的类就叫做扩展类)
。
Python解释器本来就是C写的,所以我们可以在C的层面上面实现Python的任何对象,类也是如此。Python中自定义的类和内置的类在C一级的结构是一致的,所以我们只需要按照Python/C api提供的标准来编写即可。但还是那句话,使用C来编写会比较麻烦,因为本质上就是写C语言。
当我们操作扩展类的时候,我们操作的是编译好的静态代码,因此在访问内部属性的时候,可以实现快速的C一级的访问,这种访问可以显著的提高性能。但是在扩展类的实现、以及处理相应的实例对象和在纯Python中定义类是完全不同的,需要有专业的Python/C api的知识,不适合新手。
这也是Cython出现在此的原因:Cython使得我们创建和操作扩展类就像操作Python中的类一样。在Cython中定义一个扩展类通过cdef class的形式,和Python中的常规类保持了高度的相似性。
尽管在语法上有着相似之处,但是cdef class定义的类对所有方法和数据都有快速的C级别的访问,这也是和扩展类和Python中的普通类之间的一个最显著的区别。
Cython中的扩展类
举一个Python中类
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def get_area(self):
return self.width * self.height
这个类是使用Python在解释器的级别定义的,可以被CPython编译的。我们定义了矩形的宽和高,并且提供了一个方法,计算面积。这个类是可以动态修改的,我们可以指定任意的属性。
如果我们只是将这个Python类编译为C时,那么得到的类依旧是一个纯Python类,而不是扩展类。所有的操作,仍然是通过动态调度通用的Python对象来实现的。只不过由于解释器的开销省去了,因此效率上会提升一点点,但是它无法从静态类型上获益,因为此时的Cython代码仍然需要在运行时动态调度来解析类型。
改成扩展类的话,我们需要这么做。
cdef class Rectangle:
cdef long width, height
def __init__(self, w, h):
self.width = w
self.height = h
def get_area(self):
return self.width * self.height
此时的关键字我们使用的是cdef class,意思就是表示这个类不是一个普通的Python类,而是一个扩展类。内部代码,我们多了一个cdef long width, height
,这个是名称和self的属性是同名的,表示self中的width、height都必须是一个long,或者说可以转为C中的long的Python对象。另外对于cdef来说,定义的类是可以被外部访问的,虽然函数不行、但类可以。
>>> import cython_test
>>> rect = cython_test.Rectangle(3, 4)
>>> rect.get_area()
12
>>>
>>> rect = cython_test.Rectangle("3", "4")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "1.pyx", line 6, in cython_test.Rectangle.__init__
self.width = w
TypeError: an integer is required
>>> # 我们传递了一个字符串,告诉我们需要一个整型
注意:我们在__init__
中实例化的属性,都必须在类中使用cdef声明,举个栗子。
cdef class Rectangle:
# 这里我们只声明了width,没有声明height,那么是不是意味着这个height可以接收任意对象呢?
cdef long width
def __init__(self, w, h):
self.width = w
self.height = h
def get_area(self):
return self.width * self.height
>>> import cython_test
>>> rect = cython_test.Rectangle(3, 4)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "1.pyx", line 7, in cython_test.Rectangle.__init__
self.height = h
AttributeError: ‘cython_test.Rectangle‘ object has no attribute ‘height‘
>>>
凡是在没有在cdef中声明的,都不可以赋值给self,可能有人发现了这不是访问,而是添加呀。我添加一个属性咋啦,没咋,无论是获取还是赋值,self中的属性必须使用cdef在类中声明。我们举一个Python内置类型的例子吧
a = 1
try:
a.xx = 123
except Exception as e:
print(e) # ‘int‘ object has no attribute ‘xx‘
一样等价,我们的扩展类和内建的类是同级别的,一个属性如果想通过self.
的方式来调用,那么一定要在类里面通过cdef声明。
cdef class Rectangle:
cdef long width, height
def __init__(self, w, h):
self.width = w
self.height = h
def get_area(self):
return self.width * self.height
>>> import cython_test
>>> rect = cython_test.Rectangle(3, 4)
>>> rect.get_area()
12
>>>
>>> rect.a = "xx"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: ‘cython_test.Rectangle‘ object has no attribute ‘a‘
>>> # 如果想动态修改、添加类型,那么需要解释器在解释的时候来动态操作
>>> # 但扩展类和内置的类是等价的,直接指向了C一级的结构,不需要解释器解释这一步,因此也失去了动态修改的能力
>>> # 也正因为如此,也能提高效率。因为很多时候,我们不需要动态修改。
>>>
>>> # 当一个类实例化之后,会给实例对象一个属性字典,通过__dict__获取,它的所有属性以及相关的值都会存储在这里
>>> # 其实获取一个实例对象的属性,本质上是从属性字典里面获取,instance.attr等价于instance.__dict__["attr"],同理修改、创建也是。
>>> # 但是注意:这只是针对普通的Python类而言,但扩展类内部是没有__dict__的。
>>> rect.__dict__
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: ‘cython_test.Rectangle‘ object has no attribute ‘__dict__‘
>>>
>>>
>>> # 不光没有__dict__,你连self本身的属性都无法访问
>>> rect.width
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: ‘cython_test.Rectangle‘ object has no attribute ‘width‘
>>> # 提示我们self没有width属性,所以我们实例化之后再想修改是不行的,连获取都获取不到
>>> # 只能调用它的一些方法罢了。
对于内建的类,其实例对象也是没有属性字典的。
a = 123
try:
print(a.__dict__)
except Exception as e:
print(e) # ‘int‘ object has no attribute ‘__dict__‘
# 但是int这个类本身是有属性字典的,只是没有办法赋值
# 而我们自定义的Python类是可以这么做的,等于给类添加了一些函数
try:
int.__dict__["xx"] = "xx"
except Exception as e:
print(e) # ‘mappingproxy‘ object does not support item assignment
# 还是那句话,动态设置、修改、获取、删除属性,这些都是在解释器解释字节码的时候动态操作的,在解释的时候是允许你做一些这样的骚操作的
# 但是内置的类和扩展类是不需要解释这一步的,它们是彪悍的人生,直接指向了C一级的数据结构,因此也就丧失了这种动态的能力
同理对于扩展类,也是相同的结果。
>>> cython_test.Rectangle.__dict__["xx"] = 123
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: ‘mappingproxy‘ object does not support item assignment
>>>
所以扩展类和内置的类的表现是相同的。
但是扩展类毕竟是我们自己指定的,如果我们就是想修改self的一些属性呢?答案是将其暴露给外界即可。
cdef class Rectangle:
# 通过cdef public的方式进行声明即可
# 这样的话就会暴露给外界了
cdef public long width, height
def __init__(self, w, h):
self.width = w
self.height = h
def get_area(self):
return self.width * self.height
>>> import cython_test
>>> rect = cython_test.Rectangle(3, 4)
>>> rect.width
3
>>> rect.get_area()
12
>>> rect.width = 123
>>> rect.get_area()
492
>>>
>>> rect.__dict__
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: ‘cython_test.Rectangle‘ object has no attribute ‘__dict__‘
>>> # 但属性字典依旧是没有的
通过cdef public声明的属性,是可以被外界获取并修改的,除了cdef public之外还有cdef readonly,同样会将属性暴露给外界,但是只能访问不能修改。
>>> rect.width
3
>>> rect.width = 123
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: attribute ‘width‘ of ‘cython_test.Rectangle‘ objects is not writable
cdef readonly 类型 变量名:实例属性可以被访问,但是不可以被修改
cdef public 类型 变量名:实例属性可以被访问,也可以被修改
cdef 类型 变量名:实例属性既不可以被访问,更不可以被修改
当然即便使用cdef public和cdef readonly定义变量,Cython也一样可以实行快速访问,因为扩展类的方法基本上忽略了readonly和public的声明,它们存在的目的只是为了控制来自Python的访问。
C一级的构造函数和析构函数
每一个实例对象都指向了一个C结构体,也就是Python调用__init__
函数里面的self参数。当__init__
参数被调用时,会初始化self参数上的属性,而且__init__
参数是自动调用的。但是我们知道在__init__
参数调用之前,会先调用__new__
方法,__new__
方法的作用就是为创建的实例对象开辟一份内存,然后返回其指针然后交给self。在C级别就是,在调用__init__
之前,实例对象指向的结构体必须已经分配好内存,并且所有结构字段都处于可以接收初始值的有效状态。
Cython扩充了一个名为__cinit__
的特殊方法,用于执行C级别的内存分配和初始化。不过对于之前定义的Rectangle类的__init__
方法,也是可以充当此角色的,因为内部的字段接收的值是两个double,不需要C级别的内存分配。但如果需要C级别的内存分配,那么就不可以使用__init__
了,而是需要使用__cinit__
。
# 导入相关函数,malloc,free
# 如果不熟悉的话,建议去了解一下C语言
from libc.stdlib cimport malloc, free
cdef class A:
cdef:
unsigned int n
double *array # 一个数组,存储了double类型的变量
def __cinit__(self, n):
self.n = n
# 在C一级进行动态分配内存
self.array = <double *>malloc(n * sizeof(double))
if self.array == NULL:
raise MemoryError()
def __dealloc__(self):
"""如果进行了动态内存分配,那么必须要定义__dealloc__
否则在编译的时候会抛出异常:Storing unsafe C derivative of temporary Python reference
然后我们释放掉指针指向的内存
"""
if self.array != NULL:
free(self.array)
def set_value(self):
cdef long i
for i in range(self.n):
self.array[i] = (i + 1) * 2
def get_value(self):
cdef long i
for i in range(self.n):
print(self.array[i])
>>> import cython_test
>>> a = cython_test.A(5)
>>> a.set_value()
>>> a.get_value()
2.0
4.0
6.0
8.0
10.0
>>>
所以__cinit__
是用来进行C一级内存的动态分配的,另外我们说如果在__cinit__
通过malloc进行了内存分配,那么必须要定义__dealloc__
函数将指针指向的内存释放掉。当然即使我们不释放也没关系,只不过可能发生内存泄露(雾)
,但是__dealloc__
这个函数是必须要被定义,它会在实例对象回收时被调用。
这个时候可能有人好奇了,那么__cinit__
和__init__
函数有什么区别呢?区别还是蛮多的,我们细细道来。
首先它们只能通过def来定义,另外在不涉及malloc动态分配内存的时候,__cinit__
和__init__
是等价的。然而一旦涉及到malloc,那么动态分配内存只能在__cinit__
中进行,如果这个过程写在了__init__
函数中,比如将我们上面例子的__cinit__
改为__init__
的话,你会发现self的所有变量都没有设置进去、或者说设置失败,并且其它的方法若是引用了self.array,那么还会导致丑陋的段错误。
还有一点就是,__cinit__
函数会在__init__
函数之前调用,我们实例化一个扩展类的时候,参数会先传递给__cinit__
,然后__cinit__
再将接收到的参数原封不动的传递给__init__
。
cdef class A:
cdef public:
unsigned int a, b
def __cinit__(self, a, b):
print("__cinit__")
self.a = a
self.b = b
print(self.a, self.b)
def __init__(self, c, d):
"""__cinit__中接收两个参数
然后会将参数原封不动的传递到这里,所以这里也要接收两个参数
参数名可以不一致,但是个数要匹配
"""
print("__init__")
print(c, d)
>>> import cython_test
>>> a = cython_test.A(111, 222)
__cinit__
(111, 222)
__init__
(111, 222)
>>> a.a
111
>>> a.b
222
>>>
注意:__cinit__
只有在涉及C级别内存分配的时候才会出现,如果没有涉及那么使用__init__
就可以,虽然在不涉及malloc的时候这两者是等价的,但是__cinit__
会比__init__
的开销要大一些。而如果涉及C级别内存分配,那么建议__cinit__
只负责,内存的动态分配,__init__
负责属性的创建。
from libc.stdlib cimport malloc, free
cdef class A:
cdef public:
unsigned int a, b, c
# 这里的array不可以使用public或者readonly
# 原因很简单,因为一旦指定了public和readonly,就意味着这些属性是可以被Python访问的
# 所以需要其能够转化为Python中的对象,而我们说C中的指针,除了char *是不能转化为Python对象的
# 因此这里的array一定不能暴露给外界,否则编译出错,提示我们:double *无法转为Python对象
cdef double *array
def __cinit__(self, *args, **kwargs):
"""这里面只做内存分配,设置属性交给__init__,所以参数一般都写*args, **kwargs
但是如果分配的内存如果需要通过参数来指定的话,那么还是不建议使用*args和**kwargs的
具体情况具体分析
"""
self.array = <int *>malloc(3 * sizeof(int))
def __init__(self, a, b, c):
self.a = a
self.b = b
self.c = c
def __dealloc__(self):
free(self.array)
cdef和cpdef方法
我们之前使用了cdef和cpdef,我们说:cdef可以定义变量,但是不能被Python直接访问;可以定义函数,不能直接被外界访问;可以定义一个类,能直接被外界访问。而cpdef专门用于定义函数,cpdef定义的函数既可以在Cython内部访问,也可以被外界访问,因为它定义了两个版本的函数:一个是高性能的纯C版本(此时等价于cdef,至于为什么高效,因为它是C一级的,直接指向了具体数据结构,当然还有其它原因,我们之前都说过的)
,另一个是Python包装器(相当于我们手动定义的Python函数)
,所以我们还要求使用cpdef定义的函数的参数和返回值类型必须是Python可以表示的,像char *之外的指针就不行。
那么同理它们也可以作用于方法,当然方法也是类在获取函数的时候进行封装得到的,所以一样的道理。但是注意:cdef和cpdef修饰的cdef class定义的静态类里面的方法,如果是class定义的纯Python类,那么内部是不可以出现cdef或者cpdef的。
cdef class A:
cdef public:
long a, b
def __init__(self, a, b):
self.a = a
self.b = b
cdef long f1(self):
return self.a * self.b
cpdef long f2(self):
return self.a * self.b
>>> import cython_test
>>> a = cython_test.A(11, 22)
>>> a.f2()
242
>>> a.f1()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: ‘cython_test.A‘ object has no attribute ‘f1‘
>>>
cdef和cpdef之间在函数上的差异,在方法中得到了同样的体现。
此外,这个类的实例也可以作为函数的参数,这个是肯定的。
cdef class A:
cdef public:
long a, b
def __init__(self, a, b):
self.a = a
self.b = b
cpdef long f2(self):
return self.a * self.b
def func(self_lst):
s = 0
for self in self_lst:
s += self.f2()
return s
>>> import cython_test
>>> a1 = cython_test.A(1, 2)
>>> a2 = cython_test.A(2, 4)
>>> a3 = cython_test.A(2, 3)
>>>
>>> cython_test.func([a1, a2, a3])
16
>>>
这是Python的特性,一切都是对象,尽管没有指明self_lst是什么类型,但只要它可以被for循环即可;尽管没有指明self_lst里面的元素是什么类型,只要它有f2方法即可。并且这里的func可以在Cython中定义,同样可以在Python中定义,这两者是没有差别的,因为都是Python中的函数。另外在遍历的时候仍然需要确定这个列表里面的元素是什么,意味着列表里面的元素仍然是PyObject *,它需要获取类型、转化、属性查找,因为Cython不知道类型是什么、导致其无法优化。但如果我们规定了类型,那么再调用f2的时候,那么会直接指向C一级的数据结构,因此不需要那些无用的检测。
cdef class A:
cdef public:
long a, b
def __init__(self, a, b):
self.a = a
self.b = b
cpdef long f2(self):
return self.a * self.b
# 规定这是一个list,参数也变成静态变量的话会更快,总之静态定义越多速度会越快
def func(list self_lst):
# 声明long类型的s,A类型的self
# 我们下面使用的是 s = s + self.f2(), 所以这里的s要赋一个初始值0
cdef long s = 0
cdef A self
for self in self_lst:
s += self.f2()
return s
调用得到的结果是一样的,可以自己尝试一下。这样的话速度会变快很多,因为我们实例对象静态类的实例对象,f2方法也是cpdef方法,所以在执行的时候不涉及Python对象。并且求和的时候,也是一个指使用C的操作,因为s是一个double。
这个版本的速度比之前快了10倍,这表名类型化比非类型化要快了10倍。如果我们删除了cdef A self,也就是不规定其类型,而还是按照Python的语义来调用,那么速度仍然和之前一样,即便使用cpdef定义。所以重点在于指定类型为静态类型,只要规定好类型,那么就可以提升速度;而Cython是为Python服务的,肯定要经常使用Python的类型,那么提前规定好、让其指向C一级的数据结构,速度会提升很多。如果是int和float,那么就使用C中的long和double,这样速度就更加快速了,当然即便用Python的int和float依旧可以起到加速的效果,只不过没有C明显。因此重点是一定要静态定义类型,只要类型明确那么就能进行大量的优化。
Python慢有很多原因,其中一个原因就是它无法对类型进行优化,以及对象分配在堆上。无法基于类型进行优化,就意味着每次都要进行大量的检测,当然这些我们前面已经说过了,如果规定好类型,那么就不用兜那么大圈子了;而对象分配在堆上这是无法避免的,只要你用Python的对象,都是分配在堆上,所以对于整型和浮点型,我们通过定义为C的类型使其分配在栈上,能够更加的提升速度。总之记住一句话:Cython加速的关键就在于,类型一定要静态声明,并且对整数和浮点使用C中long和double。
当然,虽说如此,但是该使用Python中对象就使用Python的对象,我们基于类型优化其实是可以获得相当可观的速度的。至于要不要通过C的类型
(比如使用结构体、共同体这种复杂类型)
进行更深一步的优化,就看你对Cython的掌握程度了。
在上面的基础上,如果将def改成cpdef那么效率是没有差别的,但是一旦改成cdef那么效率会再次提升,原因很简单,因为def和cpdef都是支持外部Python访问的;而cdef只支持内部Cython访问,那么它就只指向了一个C级的数据结构,但是def和cpdef都涉及到Python函数,而我们说Python函数比C函数开销要大的。当然cdef的缺点就是外部无法访问,而且函数调用需要的开销基本可以忽略不计的。
方法中给参数指定类型
无论是def、cdef、cpdef,都可以给参数规定类型,如果类型传递的不对就会报错。比如:上面的func函数如果是普通的Python函数,那么对于Python而言只要能够被for循环即可,所以它可以是列表、元组、集合。但是我们上面的func规定了类型,尽管它还是def定义的,但是参数只能传递list对象或者其子类的实例对象,如果传递tuple对象就会报错。
当然,对于__cinit__
和__init__
也是可以的,另外我们说这两位老铁只能用def定义。
cdef class A:
cdef public:
long a, b
def __init__(self, float a, float b):
self.a = a
self.b = b
这里我们规定了类型,但是有没有发现什么问题呢?这里我们的参数a和b必须是一个float,如果传递的是其它类型会报错,但是赋值的时候self.a和self.b又需要接收一个long,所以这是一个自相矛盾的死结,在编译的时候就会报错。所以给__init__
参数传递的值的类型要和类中cdef声明的类型保持一致。
然后为了更好地解释Cython带来的性能改进,我们需要了解关于继承、子类化、和扩展类型的多态性的基础知识。
继承和子类化
扩展类型只能继承单个基类,并且继承的基类必须是直接指向C实现的类型(可以是使用cdef class定义的扩展类型,也可以是内置类型,因为内置类型也是直接指向C一级的结构)
。如果基类是常规的Python类(需要在运行时经过解释器动态解释才能指向C一级的结构)
,或者继承了多个基类,那么Cython在编译时会抛出异常。
cdef class Girl:
cdef public:
str name
long age
def __init__(self, name, age):
self.name = name
self.age = age
cpdef str get_info(self):
# 在Cython中我们是可以使用了f-string的
# 但是返回值类型我们除了使用str,还可以要使用unicode,这个我们后面还会说
return f"name: {self.name}, age: {self.age}"
cdef class CGirl(Girl):
cdef public str where
def __init__(self, name, age, where):
self.where = where
super().__init__(name, age)
class PyGirl(Girl):
def __init__(self, name, age, where):
self.where = where
super().__init__(name, age)
我们定义了一个扩展类(Girl)
,然后让另一个扩展类(CGirl)
和普通的Python类(PyGirl)
都去继承它。我们说扩展类不可以继承Python类,但Python类是可以继承扩展类的。
>>> import cython_test
>>>
>>> c_girl = cython_test.CGirl("古明地觉", 17, "东方地灵殿")
>>> c_girl.get_info()
‘name: 古明地觉, age: 17‘
>>>
>>> py_girl = cython_test.PyGirl("古明地觉", 17, "东方地灵殿")
>>> py_girl.get_info()
‘name: 古明地觉, age: 17‘
>>>
>>> c_girl.where
‘东方地灵殿‘
>>> py_girl.where
‘东方地灵殿‘
>>>
我们看到,对于扩展类和普通的Python类,它们都是可以继承扩展类的。这里我们的name和where没有使用char *而是使用的str,我个人建议不使用char *,原因有两个:
- 1. 传递的时候需要手动编码成bytes
def func(char *name):
print(name)
>>> import cython_test
>>> cython_test.func("古明地觉")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "1.pyx", line 1, in cython_test.func
def func(char *name):
TypeError: expected bytes, str found
>>>
>>> cython_test.func("古明地觉".encode("utf-8"))
b‘xe5x8fxa4xe6x98x8exe5x9cxb0xe8xa7x89‘
>>>
提示我们:需要一个bytes,但是却传递了一个str。问题是我们之前说一个单独的字符串可以被解释成C中的字符串啊,那只是在Cython中声明一个普通变量的时候(比如:cdef char *name="古明地觉")
,可以当成普通的字符串,如果是作为参数那么是不行的,必须手动编码成字节。
- 2. 在类中会出现意想不到的结果
cpdef char *func1():
# 直接返回一个bytes
return b"xxx"
cpdef char *func2():
# 赋值给char *,返回这个char *变量
cdef char *res = "yyy"
return res
cpdef char *func3(char *name):
# 返回name本身
return name
cpdef char *func4(char *name):
# 先将name解码成Python的字符串,进行f-strings,然后编码成字节返回
return f"name: {name.decode(‘utf-8‘)}".encode("utf-8")
>>> import cython_test
>>>
>>> cython_test.func1()
b‘xxx‘
>>>
>>> cython_test.func2()
b‘yyy‘
>>> cython_test.func3("古明地觉".encode("utf-8"))
b‘xe5x8fxa4xe6x98x8exe5x9cxb0xe8xa7x89‘
>>>
>>> cython_test.func3("古明地觉".encode("utf-8")).decode("utf-8")
‘古明地觉‘
>>>
>>> cython_test.func4("古明地觉".encode("utf-8")).decode("utf-8")
‘name: 古明地觉‘
我们看到对于函数来说,是没有任何问题的,将cpdef换成def也是一样的, 但是换成类呢?
cdef class Girl:
cdef public char *name1
def __init__(self, name1):
self.name1 = name1
# 这里使用普通的Python函数
def foo1(self, char* name2):
print(self.name1.decode("utf-8"))
print(name2.decode("utf-8"))
def foo2(self):
print(self.name1.decode("utf-8"))
def foo3(self):
cdef bytes tmp = self.name1
print(tmp)
print(tmp.decode("utf-8"))
>>> import cython_test
>>> g = cython_test.Girl("古明地觉".encode("utf-8"))
>>> g.foo1("椎名真白".encode("utf-8"))
椎名真白
椎名真白
>>> g.foo2()
>>> g.foo3()
b‘‘
>>>
惊了,我们看到在foo1中self.name1和name2在decode之后得到的都是name2的结果;在foo2中,self.name1根本打印不出来;在foo3中,即便我们赋值给了一个变量,还是得不到正确的结果。
显然我们看到方法foo1中传递的参数是可以正常打印的,不能正常打印的是self的char *属性,但是关于原因,为什么在函数中可以正常使用,但到了类的实例对象这里就不行了,这个我目前也无法解释。书中使用的是数值,我没有完全按照它的举的例子,因此这个结果也是我偶然发现的。将字符串声明为char *的时候,发现了这个问题。显然这已经超出了我可以调试的范畴,这个在不了解Cython底层是如何加速类的情况下,想调试得到答案对我而言是一件不可能完成的任务。
因此我个人建议,像创建变量、if、for、函数、类等等,在这些Python的逻辑中,C的类型只使用long和double即可,其它的就还使用Python的类型,无论是参数还是返回值都是如此。至于像结构体、共同体等C的数据结构中,是否使用char *我们后续再讨论。
除此之外还有一个重点,那就是返回值的问题,我们看到上面的CGirl这个类,我们说的get_info函数除了使用cdef?str
,还可以使用cdef?unicode
,这在Python3中是没有区别的。但如果你在编译的时候没有指定language_level = 3,那么str会默认使用Python2中的str,那么你使用Python3的时候就必须传递一个字节串了。因为Python2中的str在Python3中代表bytes,如果使用Python2的语义编译并且还想传递字符串的话,那么需要通过unicode,因为Python2的unicode相当于Python3的str。所以我们才说,在编译的时候要显式地指定language_level=3,否则很容易出现这种错误。
继承的话,会有什么样的结果呢?我们说cdef定义的方法和函数一样,无法被外部的Python访问,那么内部的Python类在继承的时候可不可以访问呢?以及私有属性呢?
我们先来看看Python中关于私有属性的例子。
class A:
def __init__(self):
self.__name = "xxx"
def __foo(self):
return self.__name
class B(A):
def test(self):
try:
self.__name
except Exception as e:
print(e)
try:
self.__foo()
except Exception as e:
print(e)
B().test()
"""
‘B‘ object has no attribute ‘_B__name‘
‘B‘ object has no attribute ‘_B__foo‘
"""
我们说定义的私有属性只能在当前类里面使用,一旦出去了就不能够再访问了。其实私有属性本质上只是Python给你改了个名字,在原来的名字前面加上一个_类名
,所以__name
和__foo
其实相当于是_A__name
和_A__foo
,但是当我们在外部用实例属性去获取__name
和__foo
的时候,获取的就是__name
和__foo
,而显然A里面没有这两个属性,因此报错。解决的办法就是通过调用_A__name
和_A__foo
,但是不建议这么做,因为这是私有变量,如果非要访问的话,那就不要定义成私有的。如果是在A这个类里面调用的话,那么Python解释器也会自动为我们加上_类名
这个前缀,我们在类里面调用self.__name
的时候,实际上调用的也是self._A__name
私有属性,但是在外部就不会了。
如果是继承的话,通过报错信息我们也知道原因。B也是一个类,那么在B里面调用私有属性,同样会加上_类名
这个前缀,但是这个类名显然是B的类名,不是A的类名,因此找不到_B__name
和_B__foo
,当然我们强制通过_A__name
和_A__foo
也是可以访问的,只是不建议这么做。
因此Python中不存在绝对的私有,只不过是解释器内部偷梁换柱将你的私有属性换了个名字罢了,但是我们可以认为它是私有的,因为按照原本的逻辑没有办法访问。同理继承的子类,有没有办法使用父类的私有属性。
但是在Cython中是不是这样子呢?
cdef class Person:
cdef public:
long __age
str __name
long length
def __init__(self, name, age, length):
self.__age = age
self.__name = name
self.length = length
cdef str __get_info(self):
return f"name: {self.__name}, age: {self.__age}, length: {self.length}"
cdef str get_info(self):
return f"name: {self.__name}, age: {self.__age}, length: {self.length}"
cdef class CGirl(Person):
cpdef test1(self):
print(self.__name, self.__age, self.length)
cpdef test2(self):
print(self.__get_info())
class PyGirl(Person):
def test1(self):
print(self.length)
print(self.__name, self.__age)
def test2(self):
print(self.__get_info())
def test3(self):
print(self.get_info())
>>> import cython_test
>>> c_g = cython_test.CGirl("古明地觉", 17, 156)
>>> c_g.test1()
古明地觉 17 156
>>> c_g.test2()
name: 古明地觉, age: 17, length: 156
>>>
>>> py_g = cython_test.PyGirl("古明地觉", 17, 156)
>>> py_g.test1()
156
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "1.pyx", line 31, in cython_test.PyGirl.test1
print(self.__name, self.__age)
AttributeError: ‘PyGirl‘ object has no attribute ‘_PyGirl__name‘
>>> py_g.test2()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "1.pyx", line 34, in cython_test.PyGirl.test2
print(self.__get_info())
AttributeError: ‘PyGirl‘ object has no attribute ‘_PyGirl__get_info‘
>>> py_g.test3()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "1.pyx", line 37, in cython_test.PyGirl.test3
print(self.get_info())
AttributeError: ‘PyGirl‘ object has no attribute ‘get_info‘
>>>
我们看到对于Cython定义的C一级的类而言,不仅是cdef定义的方法,连私有属性也可以一并使用。但是对于纯Python类就不行了,私有属性无法访问就算了,就连父类使用cdef定义的非私有方法也无法继承下来,原因就是PyGirl是一个Python类,不是使用cdef class定义的静态类。如果把父类的cdef get_info改成def或者cpdef,那么Python子类是可以访问的。
我们说cdef定义的是C一级的方法,不是Python的方法、也不是cpdef定义的时候自带Python包装器,因此它无法被Python子类继承,因此它并没有跨越语言的边界。当然如果你不熟悉Cython中的继承、并且有很想使用继承,那么就不要使用cdef,使用def或者cpdef定义吧。虽说cdef定义的C一级的函数调用比Python快,但是说实话那一点点快几乎没啥意义。Cython加速的核心在于类型上的优化,如果我们能使用静态的方式声明,那么速度就会有明显的提升,不要为了加速反倒畏手畏脚地这不敢用那不敢用。
总之Cython加速记住两个原则:1. 能使用静态声明的方式使用静态声明,不仅是变量,还有参数、返回值;2. 关于int和float,使用C中的long和double。关于优化我们后面还会继续说,总之原则就是上面这两点做到了,我们的目的就达成了。至于cdef比def、cpdef少的那一点点函数调用的开销可以说是沧海一粟,更何况你要想被Python访问,光一个cdef也办不到,肯定需要依赖Python的包装器的。
类型转化
我们知道Python中类在继承扩展类的时候,无法继承其内部的cdef方法,但如果我们知道这个类是继承扩展类的,那么其实例对象可不可以转化为扩展类的类型呢?
cdef class A:
cdef funcA(self):
return 123
class B(A):
# 显然func1内部无法访问扩展类A的funcA
def func1(self):
return self.funcA()
# 但是我们在使用的时候将其类型转化一下
def func2(self):
return (<A> self).funcA()
>>> import cython_test
>>> b = cython_test.B()
>>> b.func1()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "1.pyx", line 10, in cython_test.B.func1
return self.funcA()
AttributeError: ‘B‘ object has no attribute ‘funcA‘
>>> b.func2()
123
>>>
我们看到b.func2是可以调用成功的,但我们知道对于Python中类型使用<>这种方式如果转化不成功,那么也不会有任何影响,会保留原来值(C中的整型和浮点除外)
,这可能会有点危险。因此我们可以通过(<A?> self)
,这样self必须是A或者其子类的实例对象,否则报错。
扩展类型对象和None
看一个简单的函数
cdef class Girl:
cdef public:
str name
long age
def __init__(self, name, age):
self.name = name
self.age = age
def dispatch(Girl g):
print(g.name, g.age)
>>> import cython_test
>>> cython_test.dispatch(cython_test.Girl("古明地觉", 17))
古明地觉 17
>>> cython_test.dispatch(cython_test.Girl("椎名真白", 16))
椎名真白 16
>>> class B(cython_test.Girl):
... pass
...
>>> cython_test.dispatch(B("mashiro", 16))
mashiro 16
>>>
>>> cython_test.dispatch(object())
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Argument ‘g‘ has incorrect type (expected cython_test.Girl, got object)
>>>
我们传递一个Girl或者其子类的实例对象的话是没有问题的,但是传递一个其它的则不行。
但是在Cython中None是一个例外,即使它不是Girl的实例对象,但也是可以传递的,除了C规定的类型之外,只要是Python的类型,不管什么,传递一个None都是可以的。这就类似于C中的空指针,任何指针都可以传递给空指针,但是没有办法做什么操作。
所以这里可以传递一个None,但是执行逻辑的时候显然会报错。
>>> import cython_test
>>> cython_test.dispatch(None)
Segmentation fault
[root@iz2ze3ik2oh85c6hanp0hmz ~]#
然而报错还是轻的,这里发生段错误,解释器直接异常退出了。原因就在于不安全地访问了Girl实例对象的成员属性,属性和方法都是C接口的一部分,而Python中None本质上没有C接口,因此访问属性或者调用方法都是无效的。为了确保这些操作的安全,最好加上一层检测。
def dispatch(Girl g):
if g is None:
raise TypeError("...")
print(g.name, g.age)
但是除了上面那种做法,Cython还提供了一种特殊的语法。
def dispatch(Girl g not None):
print(g.name, g.age)
此时如果我们传递了None,那么就会报错。不过这个版本由于要预先进行类型检查,判断是否为None,从而会牺牲一些效率。不过虽说如此,但是传递None所造成的段错误是非常致命的,因此我们是非常有必要防范这一点的。当然还是那句话,虽然效率会牺牲一点点,但还是那句话,与Cython带来的效率提升相比,这点牺牲是非常小的,况且这也是必要的。
>>> import cython_test
>>> cython_test.dispatch(None)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Argument ‘g‘ has incorrect type (expected cython_test.Girl, got NoneType)
>>>
此时对None也是一视同仁的,传递一个None也是不符合类型的。这里我们传递了一个None,但是除了None还能传递别的吗?答案是不行的,只能传递None,因为Cython只有对None不会进行检测。
def dispatch(Girl g not 123):
^
------------------------------------------------------------
1.pyx:11:24: Expected ‘None‘
许多人认为需要not None字句是不方便的,这个特性经常被争论,但幸运的是,在函数的参数声明中使用not None是非常方便的。
个人觉得Cython的语法设计的真酷,笔者本人非常喜欢。
为了更高的性能,Cython还提供了一个默认的nonecheck编译器指令,可以对整个扩展模块不进行检查。通过在文件的开头加上一个注释:# cython: nonecheck=True
,但是个人建议不要这么干。
Cython中扩展类的property
Python中的property非常的易用且强大,可以让我们精确地控制某个属性的修改,而Cython也是支持property描述符的,但是方式有些不一样。不过在介绍Cython的property之前,我们先来看看Python中的property。
class Girl:
def __init__(self):
self.name = None
@property
def x(self):
# 不需要我们对x进行调用,直接通过self.x即可获取返回值
# 让函数像属性一样直接获取
return self.name
@x.setter
def x(self, value):
# 当我们self.x = "古明地觉"的时候,会调用这个函数
# "古明地觉"就会传递给这里的value
self.name = value
@x.deleter
def x(self):
# 执行del self.x的时候,就会调用这个函数
print("被调用了")
del self.name
girl = Girl()
print(girl.x) # None
girl.x = "古明地觉"
print(girl.x) # 古明地觉
del girl.x # 被调用了
这里是通过装饰器的方式实现的,三个函数都是一样的名字,除了使用装饰器,我们还可以这么做。
class Girl:
def __init__(self):
self.name = None
def fget(self):
return self.name
def fset(self, value):
self.name = value
def fdel(self):
print("被调用了")
del self.name
# 传递三个函数即可,除此之外还有一个doc属性
x = property(fget, fset, fdel, doc="这是property")
girl = Girl()
print(girl.x) # None
girl.x = "古明地觉"
print(girl.x) # 古明地觉
del girl.x # 被调用了
所以property就是让我们像访问属性一样访问函数,那么它内部是怎么做到的呢?不用想,肯定是通过描述符。
class MyProperty: # 模仿类property,实现与其一样的功能
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
self.doc = doc
def __get__(self, instance, owner):
return self.fget(instance)
def __set__(self, instance, value):
return self.fset(instance, value)
def __delete__(self, instance):
return self.fdel(instance)
def setter(self, func):
return type(self)(self.fget, func, self.fdel, self.doc)
def deleter(self, func):
return type(self)(self.fget, self.fset, func, self.doc)
class Girl1:
def __init__(self):
self.name = None
@MyProperty
def x(self):
return self.name
@x.setter
def x(self, value):
self.name = value
@x.deleter
def x(self):
print("被调用了")
del self.name
class Girl2:
def __init__(self):
self.name = None
def fget(self):
return self.name
def fset(self, value):
self.name = value
def fdel(self):
print("被调用了")
del self.name
x = MyProperty(fget, fset, fdel)
girl1 = Girl1()
print(girl1.x) # None
girl1.x = "古明地觉"
print(girl1.x) # 古明地觉
del girl1.x # 被调用了
girl2 = Girl2()
print(girl2.x) # None
girl2.x = "古明地觉"
print(girl2.x) # 古明地觉
del girl2.x # 被调用了
我们通过描述符的方式手动实现了一个property的功能,描述符事实上在Python解释器的层面也用的非常多,我们说实例调用方法的时候,第一个参数self会自动传递也是通过描述符实现的。所以描述符不光我们在Python的层面用,在解释器的层面上也大量使用描述符。同理字典也是如此,我们定义的类的实例对象的属性都是存在一个字典里面的,我们称之为属性字典,所以字典在Python中是经过高度优化的,原因就是不仅我们在用,底层也在大量使用。
下面来看看Cython中的property
针对扩展类的property,Cython有着不同的语法,但是实现了相同的结果。
cdef class Girl:
cdef str name
def __init__(self):
self.name = None
property x:
def __get__(self):
return self.name
def __set__(self, value):
self.name = value
>>> import cython_test
>>> g = cython_test.Girl()
>>>
>>> g.x
>>> print(g.x)
None
>>> g.x = "古明地觉"
>>> g.x
‘古明地觉‘
我们看到Cython是将property和描述符结合在一起了,但是实现起来感觉更方便了。
不过最重要的还是魔法方法,魔法方法算是Python中非常强大的一个特性,Python将每一个操作符都抽象成了对应的魔法方法,也正因为如此numpy也得以很好的实现。那么在Cython中,魔法方法是如何体现的呢?
魔法方法在Cython中更加魔法
通过魔法方法可以对运算符进行重载,魔法方法的特点就是它的函数名以双下划线开头、并以双下划线结尾。我们之前讨论了__cinit__
、__init__
、__dealloc__
,并了解了它们分别用于C一级的初始化、Python一级的初始化、对象的释放(特指C中的指针)
。除了那三个,Cython中也支持其它的魔法方法,但是注意:Cython不支持__del__
,__del__
由__dealloc__
负责实现。
算术魔法方法
假设在Python中定义了一个类class A,那么如果希望A的实例对象可以使用+,那么内部需要定义def __add__(self, other)
方法。然后a + 123的时候,就会转化成A.__add__(a, 123)
,其中a是A的实例对象。如果是123 + a的话,那么会先调用123的__add__
方法,如果出现类型错误,那么会去检测a是否有__radd__
,有的话会去调用,没有的话就会报错,同理对于其它的算术操作也是类似的。
class A:
def __add__(self, other):
print("__add__ is called")
return 1 + other
def __radd__(self, other):
print("__radd__ is called")
return 1 + other
a = A()
print(a + 123)
"""
__add__ is called
124
"""
print(123 + a) # 先调用123的__add__,没有的话调用a的__radd__
"""
__radd__ is called
124
"""
# 当然的例子中,两个魔法方法中的self都是A的实例对象,有人会觉得这不是废话吗
# 之所以要提这一点,是为了给后面的Cython做铺垫
除了类似于__add__
这种实例对象放在左边、__radd__
这种实例对象放在右边,还有__iadd__
,它是用于+=这种形式。
class A:
def __iadd__(self, other):
print("__iadd__ is called")
return 1 + other
a = A()
a += 123
print(a)
"""
__iadd__ is called
124
"""
# 如果没定义__iadd__,也是可以使用这种形式,会转化成a = a + 123,所以会调用__add__方法
所以Python真的是把每一个操作都抽象成了一个魔方方法。
但是对于Cython中的扩展类来说,不使用类似于__radd__
这种实现方式。我们只需要定义一个__add__
即可同时实现__add__
和__radd__
。对于Cython中的扩展类型A,a是A的实例对象,如果是a + 123,那么会调用__add__
方法,然后第一个参数是a、第二个参数是123;但如果是123 + a,那么依旧会调用__add__
,不过此时__add__
的第一个参数是123、第二个参数才是a。所以不像Python中的魔法方法,第一个参数self永远是实例本身,第一个参数是谁取决于谁在前面。所以将第一个参数叫做self容易产生误解,官方也不建议将第一个参数使用self作为参数名。
cdef class Girl:
def __add__(x, y):
return x, y
>>> import cython_test
>>> g = cython_test.Girl()
>>>
>>> g + 123
(<cython_test.Girl object at 0x7f554d2ad120>, 123)
>>> 123 + g
(123, <cython_test.Girl object at 0x7f554d2ad120>)
>>>
我们看到,__add__
中的参数确实是由位置决定的,那么再来看一个例子。
cdef class Girl:
cdef long a
def __init__(self, a):
self.a = a
def __add__(x, y):
if isinstance(x, Girl):
# 这里为什么需要转化呢?直接x.a + y不行吗?
# 答案是不行的,因为这个x是我们外部传过来的Girl对象
# 但是我们这里的a不是一个public或者readonly,直接访问是得不到的,所以需要转化一下才可以访问
return (<Girl> x).a + y
return (<Girl> y).a + x
>>> import cython_test
>>> g = cython_test.Girl(3)
>>> g + 2
5
>>> 2 + g
5
>>> # 和浮点运算也是可以的
>>> g + 2.1
5.1
>>> 2.1 + g
5.1
>>> g += 4
>>> g
7
>>>
除了__add__
,Cython也是支持__iadd__
的,此时的第一个参数是self,因为+=这种形式,第一种参数永远是实例对象。
另外我们这里说的
__add__
和__iadd__
只是举例,其它的算术操作也是可以的。
富比较
Cython的扩展类可以使用__eq
、__ne__
等等,和Python一致的富比较魔法方法
cdef class A:
# 这里比较操作符两边的值的位置依旧会影响这里的x、y
# 但是对于Python中的比较来说则不会,self永远是实例对象
def __eq__(self, other):
print(self, other)
return "=="
>>> import cython_test
>>> a = cython_test.A()
>>> a == 3
<cython_test.A object at 0x7f87b034a120> 3
‘==‘
>>> 3 == a
<cython_test.A object at 0x7f87b034a120> 3
‘==‘
>>>
a == 3,那么会调用a的__eq__
,3 == a会先调用3的__eq__
,如果抛出个类型错误,那么会改用a的__eq__
,但是和算术魔法方法不一样,比较操作没有__req__
或者__ieq__
,并且比较的时候第一个参数永远是实例对象。
cdef class A:
def __eq__(self, other):
print(self, other)
return "A =="
class B:
def __eq__(self, other):
print(self, other)
return "B =="
>>> import cython_test
>>>
>>> a = cython_test.A()
>>> b = cython_test.B()
>>>
>>> a == 123 # 调用a的__eq__
<cython_test.A object at 0x7f8e1f684120> 123
‘A ==‘
>>> b == 123 # 调用b的__eq__
<cython_test.B object at 0x7f8e177e3080> 123
‘B ==‘
>>>
>>> 123 == a # 调用a的__eq__, 第一个参数还是a
<cython_test.A object at 0x7f8e1f684120> 123
‘A ==‘
>>> 123 == b # 调用b的__eq__, 第一个参数还是b
<cython_test.B object at 0x7f8e177e3080> 123
‘B ==‘
>>>
>>> a == b # 调用a的__eq__, 第一个参数是a, 第二个参数是b
<cython_test.A object at 0x7f8e1f684120> <cython_test.B object at 0x7f8e177e3080>
‘A ==‘
>>> b == a # 调用b的__eq__, 第一个参数是b, 第二个参数是a
<cython_test.B object at 0x7f8e177e3080> <cython_test.A object at 0x7f8e1f684120>
‘B ==‘
>>>
链式比较也是可以的,比如:a == b == 123等价于a == b and b == 123。
>>> a == b == 123
<cython_test.A object at 0x7f8e1f684120> <cython_test.B object at 0x7f8e177e3080>
<cython_test.B object at 0x7f8e177e3080> 123
‘B ==‘
先执行a == b返回"A ==",再执行b == 3返回"B ==",然后"A =="和"B =="进行and,前面为真,所以返回后面的"B =="。
然后Python类和扩展类之间的最后一个差别就是对迭代器的支持。
迭代器支持
Cython中的扩展类也是支持迭代器协议的,而且定义的方法和纯Python之间是一样的。
cdef class A:
cdef public:
list values
long __index
def __init__(self, values):
self.values = values
self.__index = 0
def __iter__(self):
return self
def __next__(self):
try:
ret = self.values[self.__index]
self.__index += 1
return ret
except IndexError:
raise StopIteration
>>> import cython_test
>>> a = cython_test.A([‘椎名真白‘, ‘古明地觉‘, ‘雾雨魔理沙‘])
>>> for _ in a:
... _
...
‘椎名真白‘
‘古明地觉‘
‘雾雨魔理沙‘
>>>
我们知道在Python中,for循环会去寻找__iter__
,但如果找不到会退而求其次去找__getitem__
,那么在Cython中是不是也是如此呢。
cdef class A:
cdef public:
list values
long __index
def __init__(self, values):
self.values = values
self.__index = 0
def __getitem__(self, item):
return self.values[item]
>>> import cython_test
>>> a = cython_test.A([‘椎名真白‘, ‘古明地觉‘, ‘雾雨魔理沙‘])
>>> for _ in a:
... _
...
‘椎名真白‘
‘古明地觉‘
‘雾雨魔理沙‘
>>>
我们看到,也是一样的。
当然上面只是介绍了魔法方法的一部分,Python中的魔法方法(比如__getattr__、__call__、__hash__等等等等)
在Cython中基本上都支持,并且Cython还提供了一些Python所没有的魔法方法。当然这些我们就不说了,如果你熟悉Python的话,那么在Cython中也是按照相同的方式进行使用即可。总之,用久了就孰能生巧了。
注意:魔法方法只能用def定义,不可以使用cdef或者cpdef。
对了,还有上下文管理器,在Cython中也是一样的用法。Python中基本上所有的魔法方法在Cython都可以直接用。
cdef class A:
def __enter__(self):
print("__enter__")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print("__exit__")
>>> import cython_test
>>> a = cython_test.A()
>>> with a:
... pass
...
__enter__
__exit__
>>>
这一次我们说了一下Cython中的扩展类,它和Python中内置类是等价的,都是直接指向了C一级的数据结构,不需要字节码的翻译过程。也正因为如此,它失去一些动态特性,但同时也获得了效率,因为这两者本来就是不可兼得的。
Cython的类有点复杂,还是需要多使用,不过它毕竟在各方面都和Python保持接近,因此学习来也不是那么费劲的。
虽然创建扩展类的最简单的方式是通过Cython,但是通过Python/C api直接在C中实现的话,则是最有用的练习,但还是那句话,它需要我们对Python/C api有一个很深的了解,而这是一个非常难得的事情,因此使用Cython就变成了我们最佳的选择。
我们老说Python/C api,可能有人觉得这到底是个什么玩意,别急,最后我们就来演示一下吧。我们不拿类来举例,就拿函数举例吧。因为使用C实现Python的函数就已经够复杂了,我们要实现的函数是:接收两个整型,然后返回两个整型之和。
#include "Python.h"
//函数的具体实现
static PyObject *
my_func1(PyObject *self, PyObject *args)
{
int a, b;
if (!PyArg_ParseTuple(args, "ii", &a, &b)){
return NULL;
}
return PyLong_FromLong(a + b);
}
static PyMethodDef module_functions[] = {
{
"my_func1",
(PyCFunction)my_func1,
METH_VARARGS,
"this is a function named my_func1",
},
{NULL, NULL}
};
static PyModuleDef HANSER = {
PyModuleDef_HEAD_INIT,
"hanser",
"this is a module named hanser",
-1,
module_functions,
NULL,
NULL,
NULL,
NULL
};
PyMODINIT_FUNC
PyInit_hanser(void)
{
return PyModule_Create(&HANSER);
}
这里面涉及很多知识,比如:参数解析、参数类型、定义模块,当然还有如何导入一个模块、调用模块的属性和方法、定义一个类的时候使用类的魔方方法等等等等,我这里就不写了,总之使用Python/C api编写扩展模块是一件非常累的事情。
总结
如果你能够掌握Python/C api的话,那么你一定是一个了不起的人。总之一句话:如果你用C编写扩展模块的时候,能够像写Python一样轻松,或者说Python语言的高级用法,比如魔方方法、描述符、元类、装饰器等等等等,你可以迅速用Python实现的话。那么我可以负责任的告诉你,要是只论Python的话(当然相信你的C水平也是极高的)
,你可以轻松地进入任何一家公司。
但正因为Python/C api写起来是一件异常痛苦的事情,所以Cython的出现极大的解放了程序猿的双手。
总之扩展类型是Cython将C的性能和Python的外观相结合的一个体现,Cython定义的扩展类型具有如下特性:
允许轻松高效地访问实例的C级数据和方法;
效率高;
允许控制属性的可见性;
可以被Python类继承
等价于内置类型,我们说扩展类不能继承Python类,但是它可以继承内置类型,因为扩展类和内置类是同一级别的。
在后面的系列中我们将更加自由的使用扩展类型,以及使用扩展类型来包装C的结构体、函数、以及C++中的类,从而给外部库提供一个优秀的面向对象的接口。
以上是关于《Cython系列》4. Cython中的扩展类的主要内容,如果未能解决你的问题,请参考以下文章