1、Python类和扩展类之间的差异 首先 Python 中 “一切皆对象”,怎么理解呢?首先在最基本的层次上,一个对象有三样东西:地址、值、类型,通过 id 函数可以获取地址并将每一个对象都区分开来,使用 type 获取类型。Python 中对象有很多属性,这些属性都放在自身的属性字典里面,这个字典可以通过 __dict__
获取。调用对象的某一个属性的时候,可以通过 .
的方式来调用,Python 也允许我们通过 class 关键字自定义一个类。
1 2 3 4 5 6 7 8 9 10 11 12 class A : pass print (A.__name__) A.__name__ = "B" print (A.__name__) try : int .__name__ = "INT" except Exception as e: print (e)
除了在 Python 中定义类,还可以直接使用 Python/C API 在 C 级别创建自己的类型,这样的类型称之为扩展类、或者扩展类型(说白了在 C 中实现的类就叫做扩展类)。
Python 解释器本来就是 C 写的,所以可以在 C 的层面上面实现 Python 的任何对象,类也是如此。Python 中自定义的类和内置的类在 C 一级的结构是一致的,所以只需要按照 Python/C API 提供的标准来编写即可。但还是那句话,使用 C 来编写会比较麻烦,因为本质上就是写 C 语言。
当操作扩展类的时候,操作的是编译好的静态代码,因此在访问内部属性的时候,可以实现快速的 C 一级的访问,这种访问可以显著的提高性能。但是在扩展类的实现、以及处理相应的实例对象和在纯 Python 中定义类是完全不同的,需要有专业的 Python/C API 的知识,不适合新手。
这也是 Cython 要增强 Python 类的原因:Cython 使得创建和操作扩展类就像操作 Python 中的类一样。在Cython中定义一个扩展类通过 cdef class 的形式,和 Python 中的常规类保持了高度的相似性。
尽管在语法上有着相似之处,但是 cdef class 定义的类对所有方法和数据都有快速的 C 级别的访问,这也是和扩展类和 Python 中的普通类之间的一个最显著的区别。而且扩展类和 int、str、list 等内置的类都属于静态类,它们的属性是不可修改的。
2、Cython中的扩展类 写一个 Python 中的类
1 2 3 4 5 6 7 8 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 类编译的话,那么得到的类依旧是一个纯 Python 类,而不是扩展类。所有的操作,仍然是通过动态调度通用的 Python 对象来实现的。只不过由于解释器的开销省去了,因此效率上会提升一点点,但是它无法从静态类型上获益,因为此时的 Cython 代码仍然需要在运行时动态调度来解析类型。
改成扩展类的话,需要这么做。
1 2 3 4 5 6 7 8 9 10 11 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 来说,定义的类是可以被外部访问的,虽然函数不行、但类可以 。
1 2 3 4 5 6 7 8 9 10 11 import pyximportpyximport.install(language_level=3 ) import cython_testrect = cython_test.Rectangle(3 , 4 ) print (rect.get_area()) try : rect = cython_test.Rectangle("3" , "4" ) except TypeError as e: print (e)
注意:在 __init__
中实例化的属性,都必须在类中使用 cdef 声明,举个栗子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 cdef class Rectangle : cdef long width def __init__ (self, w, h ): self.width = w self.height = h def get_area (self ): return self.width * self.height import pyximport pyximport.install(language_level=3 ) import cython_testrect = cython_test.Rectangle(3 , 4 ) """ File "cython_test.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 内置类型的例子吧:
1 2 3 4 5 a = 1 try : a.xx = 123 except Exception as e: print (e)
一样等价,扩展类和内建的类是同级别的,一个属性如果想通过 self.
的方式来调用,那么一定要在类里面通过 cdef 声明。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 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 pyximportpyximport.install(language_level=3 ) import cython_testrect = cython_test.Rectangle(3 , 4 ) try : rect.a = "xx" except AttributeError as e: print (e) """ 如果想动态修改、添加类型,那么需要解释器在解释的时候来动态操作 但扩展类和内置的类是等价的,直接指向了C一级的结构,不需要解释器解释这一步,因此也失去了动态修改的能力 也正因为如此,也能提高效率。因为很多时候,我们不需要动态修改。 当一个类实例化之后,会给实例对象一个属性字典,通过__dict__获取,它的所有属性以及相关的值都会存储在这里 其实获取一个实例对象的属性,本质上是从属性字典里面获取,instance.attr 等价于instance.__dict__["attr"],同理修改、创建也是。 但是注意:这只是针对普通的 Python 类而言,但扩展类的实例对象内部是没有 __dict__ 的。 """ try : rect.__dict__ except AttributeError as e: print (e) try : rect.width except AttributeError as e: print (e)
所以内建的类和扩展类是完全类似的,其实例对象都没有属性字典,至于类本身是有属性字典的,但是这个字典不可修改。因为虽然叫属性字典,但它的类型实际上是一个 mappingproxy。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import pyximportpyximport.install(language_level=3 ) import cython_testtry : int .__dict__["a" ] = 123 except TypeError as e: print (e) try : cython_test.Rectangle.__dict__["a" ] = 123 except TypeError as e: print (e)
还是那句话,动态设置、修改、获取、删除属性,这些都是在解释器解释字节码的时候动态操作的,在解释的时候是允许你做一些这样的骚操作的。但是内置的类和扩展类是不需要解释这一步的,它们是彪悍的人生,直接指向了 C 一级的数据结构,因此也就丧失了这种动态的能力。
但是扩展类毕竟是自己指定的,如果想修改 self 的一些属性呢?答案是将其暴露给外界即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 cdef class Rectangle : 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 pyximportpyximport.install(language_level=3 ) import cython_testrect = cython_test.Rectangle(3 , 4 ) print (rect.width) print (rect.get_area()) rect.width = 123 print (rect.get_area()) try : rect.__dict__ except AttributeError as e: print (e)
通过 cdef public
声明的属性,是可以被外界获取并修改的,除了cdef public
之外还有 cdef readonly
,同样会将属性暴露给外界,但是只能访问不能修改。
1 2 3 4 5 6 7 8 9 10 11 12 import pyximportpyximport.install(language_level=3 ) import cython_testrect = cython_test.Rectangle(3 , 4 ) print (rect.width) try : rect.width = 123 except AttributeError as e: print (e)
cdef readonly 类型 变量名:实例属性可以被访问,但是不可以被修改
cdef public 类型 变量名:实例属性可以被访问,也可以被修改
cdef 类型 变量名:实例属性既不可以被访问,更不可以被修改
当然定义变量无论是使用 cdef public
还是 cdef readonly
,Cython 内部的方法都可以实行快速访问,因为扩展类的方法基本上忽略了 readonly 和 public 的声明,它们存在的目的只是为了控制来自外界的访问。
3、C一级的构造函数和析构函数 每一个实例对象都对应了一个 C 结构体,其指针就是 Python 调用__init__
函数里面的 self 参数。当__init__
参数被调用时,会初始化 self 参数上的属性,而且__init__
参数是自动调用的。但是在 __init__
参数调用之前,会先调用__new__
方法, __new__
方法的作用就是为创建的实例对象开辟一份内存,然后返回其指针并交给 self。在 C 级别就是,在调用__init__
之前,实例对象指向的结构体必须已经分配好内存,并且所有结构字段都处于可以接收初始值的有效状态。
Cython 扩充了一个名为__cinit__
的特殊方法,用于执行 C 级别的内存分配和初始化。不过对于之前定义的 Rectangle 类的 __init__
方法,因为内部的字段接收的值是两个 double,不需要 C 级别的内存分配。但如果需要 C 级别的内存分配,那么就不可以使用 __init__
了,而是需要使用 __cinit__
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 from libc.stdlib cimport malloc, freecdef class A : cdef: unsigned int n double *array def __cinit__ (self, n ): self.n = n self.array = <double *>malloc(n * sizeof(double)) if self.array == NULL: raise MemoryError() def __dealloc__ (self ): """如果进行了动态内存分配,也就是定义了 __cinit__,那么必须要定义 __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 pyximportpyximport.install(language_level=3 ) import cython_testa = 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__
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 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 pyximportpyximport.install(language_level=3 ) import cython_testa = cython_test.A(111 , 222 ) """ __cinit__ 111 222 __init__ 111 222 """ print (a.a) print (a.b)
注意:__cinit__
只有在涉及 C 级别内存分配的时候才会出现,如果没有涉及那么使用 __init__
就可以,虽然在不涉及 malloc 的时候这两者是等价的,但是 __cinit__
会比 __init__
的开销要大一些。而如果涉及 C 级别内存分配,那么建议 __cinit__
只负责内存的动态分配,__init__
负责属性的创建。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from libc.stdlib cimport malloc, freecdef class A : cdef public: unsigned int a, b, c cdef double *array def __cinit__ (self, *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)
4、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 的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 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 pyximportpyximport.install(language_level=3 ) import cython_testa = cython_test.A(11 , 22 ) print (a.f2()) a.f1() """ a.f1() AttributeError: 'cython_test.A' object has no attribute 'f1' """
cdef 和 cpdef 之间在函数上的差异,在方法中得到了同样的体现。
此外,这个类的实例也可以作为函数的参数,这个是肯定的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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 pyximportpyximport.install(language_level=3 ) import cython_testa1 = cython_test.A(1 , 2 ) a2 = cython_test.A(2 , 4 ) a3 = cython_test.A(2 , 3 ) print (cython_test.func([a1, a2, a3]))
这是 Python 的特性,一切都是对象,尽管没有指明 self_lst 是什么类型,但只要它可以被 for 循环即可;尽管没有指明 self_lst 里面的元素是什么类型,只要它有 f2 方法即可。并且这里的 func 可以在 Cython 中定义,同样可以在 Python 中定义,这两者是没有差别的,因为都是 Python 中的函数。另外在遍历的时候仍然需要确定这个列表里面的元素是什么,意味着列表里面的元素仍然是 PyObject *,它需要获取类型、转化、属性查找,因为 Cython 不知道类型是什么、导致其无法优化。但如果规定了类型,那么再调用 f2 的时候,那么会直接指向 C 一级的数据结构,因此不需要那些无用的检测。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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 cpdef long func(list self_lst): cdef long s = 0 cdef A self for self in self_lst: s += self.f2() return s
调用得到的结果是一样的。这样的话速度会变快很多,因为在循环的时候,规定了变量类型,并且求和也是一个只使用 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 的掌握程度了。
在上面的基础上,如果将 cpdef 改成 cdef 那么效率会再次提升,原因很简单,因为 def 和 cpdef 都是支持外部 Python 访问的;而 cdef 只支持内部 Cython 访问,那么它就只指向了一个 C 级的数据结构,但是 def 和 cpdef 都涉及到 Python 函数,而我们说 Python 函数比 C 函数开销要大的。当然 cdef 的缺点就是外部无法访问,而且函数调用需要的开销基本可以忽略不计的。
4.1 方法中给参数指定类型 无论是 def、cdef、cpdef,都可以给参数规定类型,如果类型传递的不对就会报错。比如:上面的 func 函数如果是普通的 Python 函数,那么内部的参数对于 Python 而言只要能够被 for 循环即可,所以它可以是列表、元组、集合。但是上面的 func 规定了类型,参数只能传递 list 对象或者其子类的实例对象,如果传递 tuple 对象就会报错。
然后我们来看看__init__
。
1 2 3 4 5 6 7 8 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 带来的性能改进,需要了解关于继承、子类化、和扩展类型的多态性的基础知识。
5、继承和子类化 扩展类型只能继承单个基类,并且继承的基类必须是直接指向 C 实现的类型 (可以是使用 cdef class 定义的扩展类型,也可以是内置类型,因为内置类型也是直接指向 C 一级的结构)。如果基类是常规的 Python 类(需要在运行时经过解释器动态解释才能指向 C 一级的结构),或者继承了多个基类,那么 Cython 在编译时会抛出异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 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): 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 类是可以继承扩展类的 。
1 2 3 4 5 6 7 8 9 10 11 12 13 import pyximportpyximport.install(language_level=3 ) import cython_testc_girl = cython_test.CGirl("cython" , 17 , "python" ) print (c_girl.get_info()) py_girl = cython_test.PyGirl("cython" , 17 , "python" ) print (py_girl .get_info()) print (c_girl.where) print (py_girl.where)
对于扩展类和普通的 Python 类,它们都是可以继承扩展类的。
继承的话,会有什么样的结果呢?cdef定义的方法和函数一样,无法被外部的Python访问,那么内部的Python类在继承的时候可不可以访问呢?以及私有属性呢?
先来看看 Python 中关于私有属性的例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 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
私有属性,但是在外部就不会了。
1 2 3 4 5 6 7 8 9 10 _A__name = "cython" class A : def __init__ (self ): self.name = __name print (A().name)
如果是继承的话,通过报错信息也知道原因。B也是一个类,那么在 B 里面调用私有属性,同样会加上 _类名
这个前缀,但是这个类名显然是 B 的类名,不是 A 的类名,因此找不到 _B__name
和 _B__foo
,当然我们强制通过_A__name
和_A__foo
也是可以访问的,只是不建议这么做。
因此 Python 中不存在绝对的私有,只不过是解释器内部偷梁换柱将你的私有属性换了个名字罢了,但是可以认为它是私有的,因为按照原本的逻辑没有办法访问。同理继承的子类,也没有办法使用父类的私有属性。
但是在 Cython 中是不是这样子呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 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 "cython_test.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 "cython_test.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 "cython_test.pyx" , line 37 , in cython_test.PyGirl.test3 print (self.get_info()) AttributeError: 'PyGirl' object has no attribute 'get_info' >>>
看到对于 Cython 定义的 C 一级的类而言,私有属性、私有方法可以一并使用 。但是对于纯 Python 类就不行了,私有属性、私有方法 无法访问就算了,就连父类使用 cdef 定义的非私有方法也无法继承下来,原因就是 PyGirl 是一个 Python 类,不是使用 cdef class 定义的静态类。如果把父类的 cdef get_info 改成 def 或者 cpdef,那么Python子类是可以直接访问的。
我们说 cdef 定义的是 C 一级的方法,不是 Python 的方法、也不是 cpdef 定义的时候自带 Python 包装器,因此它无法被 Python 子类继承,因此它并没有跨越语言的边界。当然如果不熟悉 Cython 中的继承、并且有很想使用继承,那么就不要使用 cdef,直接使用 cpdef 定义吧(或者使用 def,只不过此时无法指定返回值类型)。虽说 cdef 只定义的 C 一级的函数调用比自带 Python 包装器的 cpdef 快,但是说实话那一点点快几乎没啥意义。Cython 加速的核心在于类型上的优化,如果我们能使用静态的方式声明,那么速度就会有明显的提升,不要为了加速反倒畏手畏脚地这不敢用那不敢用。
总之 Cython 加速记住两个原则:
能使用静态声明的方式使用静态声明,不仅是变量,还有参数、返回值;
这是 Cython 默认的行为,int 和 float 使用的是 C 中 的int 和 float,但是为了支持更大的数字,我们直接使用 long 和 double 即可。
但是问题来了,如果希望自定义的扩展类不可以被其它类继承的话该怎么做呢?
1 2 3 4 5 6 cimport cython @cython.final cdef class NotInheritable : pass
通过 cython.final,那么被装饰的类就是一个不可继承类,不光是外界普通的 Python 类,内部的扩展类也是不可继承的。
1 2 3 4 5 6 7 8 9 10 import pyximportpyximport.install(language_level=3 ) import cython_testclass A (cython_test.NotInheritable): pass """ TypeError: type 'cython_test.NotInheritable' is not an acceptable base type """
告诉我们 NotInheritable 不是一个合法的基类。
6、类型转换 Python 中类在继承扩展类的时候,无法继承其内部的 cdef 方法,但如果这个类是继承扩展类的,那么其实例对象可不可以转化为扩展类的类型呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 cdef class A : cdef funcA(self): return 123 class B (A ): 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 "cython_test.pyx" , line 10 , in cython_test.B.func1 return self.funcA() AttributeError: 'B' object has no attribute 'funcA' >>> b.func2()123 >>>
看到 b.func2 是可以调用成功的,但知道使用 <>
这种方式如果转化不成功,那么也不会有任何影响,会保留原来值(C中的整型和浮点除外),这可能会有点危险。因此可以通过 (<A?> self)
,这样 self 必须是 A 或者其子类的实例对象 ,否则报错。
7、扩展类型对象和None 看一个简单的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 cdef class Girl : cdef public: str name long age def __init__ (self, name, age ): self.name = name self.age = age cpdef tuple dispatch(Girl g): return g.name, g.age import pyximportpyximport.install(language_level=3 ) import cython_testprint (cython_test.dispatch(cython_test.Girl("python" , 17 ))) print (cython_test.dispatch(cython_test.Girl("cython" , 16 ))) class B (cython_test.Girl): pass print (cython_test.dispatch(B("mashiro" , 16 ))) cython_test.dispatch(object ()) """ cython_test.dispatch(object()) TypeError: Argument 'g' has incorrect type (expected cython_test.Girl, got object) """
传递一个 Girl 或者其子类的实例对象的话是没有问题的,但是传递一个其它的则不行。
但是在 Cython 中 None 是一个例外,即使它不是 Girl 的实例对象,但也是可以传递的,除了 C 规定的类型之外,只要是 Python 的类型,不管什么,传递一个 None 都是可以的。这就类似于 C 中的空指针,任何指针都可以传递给空指针,但是没有办法做什么操作。
所以这里可以传递一个 None,但是执行逻辑的时候显然会报错。
1 2 3 4 5 import pyximportpyximport.install(language_level=3 ) import cython_testcython_test.dispatch(None )
然而报错还是轻的,上面代码执行的时候会发生段错误,解释器直接异常退出了。原因就在于不安全地访问了 Girl 实例对象的成员属性,属性和方法都是 C 接口的一部分,而 Python 中 None 本质上没有 C 接口,因此访问属性或者调用方法都是无效的。为了确保这些操作的安全,最好加上一层检测。
1 2 3 4 cpdef tuple dispatch(Girl g): if g is None : raise TypeError("..." ) return g.name, g.age
但是除了上面那种做法,Cython 还提供了一种特殊的语法。
1 2 def dispatch (Girl g not None ): return g.name, g.age
此时如果我们传递了 None,那么就会报错。不过这个版本由于要预先进行类型检查,判断是否为 None,从而会牺牲一些效率。不过虽说如此,但是传递 None 所造成的段错误是非常致命的,因此非常有必要防范这一点的。当然还是那句话,虽然效率会牺牲一点点,但还是那句话,与 Cython 带来的效率提升相比,这点牺牲是非常小的,况且这也是必要的。但是注意:not None 只能出现在 def 定义的函数中,cdef 和 cpdef 是不合法的 。
1 2 3 4 5 6 7 8 9 import pyximportpyximport.install(language_level=3 ) import cython_testcython_test.dispatch(None ) """ cython_test.dispatch(None) TypeError: Argument 'g' has incorrect type (expected cython_test.Girl, got NoneType) """
此时对 None 也是一视同仁的,传递一个 None 也是不符合类型的。这里我们设置的是 not None,但是除了 None 还能设置别的吗?答案是不行的,只能设置 None,因为 Cython 只有对 None 不会进行检测。
1 2 3 4 5 cpdef tuple dispatch(Girl g not 123 ): ^ ------------------------------------------------------------ cython_test.pyx:11 :24 : Expected 'None'
许多人认为需要 not None 字句是不方便的,这个特性经常被争论,但幸运的是,在函数的参数声明中使用 not None 是非常方便的。
为了更高的性能,Cython 还提供了一个默认的 nonecheck 编译器指令,可以对整个扩展模块不进行检查,通过在文件的开头加上一个注释:# cython: nonecheck=True
。
8、Cython中扩展类的property Python 中的 property 非常的易用且强大,可以精确地控制某个属性的修改,而 Cython 也是支持 property 描述符的,但是方式有些不一样。不过在介绍 Cython 的 property 之前,来看看 Python 中的 property。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class Girl : def __init__ (self ): self.name = None @property def x (self ): return self.name @x.setter def x (self, value ): self.name = value @x.deleter def x (self ): print ("被调用了" ) del self.name girl = Girl() print (girl.x) girl.x = "cython" print (girl.x) del girl.x
这里是通过装饰器的方式实现的,三个函数都是一样的名字,除了使用装饰器,还可以这么做。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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 x = property (fget, fset, fdel, doc="这是property" ) girl = Girl() print (girl.x) girl.x = "cython" print (girl.x) del girl.x
所以 property 就是像访问属性一样访问函数,那么它内部是怎么做到的呢?不用想,肯定是通过描述符。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 class MyProperty : 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) girl1.x = "cython" print (girl1.x) del girl1.x girl2 = Girl2() print (girl2.x) girl2.x = "cython" print (girl2.x) del girl2.x
通过描述符的方式手动实现了一个 property 的功能,描述符事实上在 Python 解释器的层面也用的非常多,实例调用方法的时候,第一个参数 self 会自动传递也是通过描述符实现的。所以描述符不光在 Python 的层面用,在解释器的层面上也大量使用描述符。同理字典也是如此,定义的类的实例对象的属性都是存在一个字典里面的,称之为属性字典,所以字典在 Python 中是经过高度优化的,原因就是不仅我们在用,底层也在大量使用。
下面来看看Cython中的property
针对扩展类的 property,Cython 有着不同的语法,但是实现了相同的结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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 pyximportpyximport.install(language_level=3 ) import cython_testg = cython_test.Girl() print (g.x) g.x = "cython" print (g.x)
看到 Cython 是将 property 和描述符结合在一起了,但是实现起来感觉更方便了。
不过最重要的还是魔法方法,魔法方法算是 Python 中非常强大的一个特性, Python 将每一个操作符都抽象成了对应的魔法方法,也正因为如此 numpy 也得以很好的实现。那么在 Cython 中,魔法方法是如何体现的呢?
9、魔法方法在Cython中使用 通过魔法方法可以对运算符进行重载,魔法方法的特点就是它的函数名以双下划线开头、并以双下划线结尾 。之前讨论了__cinit__
、__init__
、__dealloc__
,并了解了它们分别用于 C 一级的初始化、Python 一级的初始化、对象的释放(特指 C 中的指针)。除了那三个,Cython 中也支持其它的魔法方法,但是注意:Cython 不支持 __del__
,__del__
由 __dealloc__
负责实现。
9.1 算术魔法方法 假设在 Python 中定义了一个类 class A,如果希望 A 的实例对象可以进行加法运算,那么内部需要定义__add__
或__radd__
方法。关于__add__
和__radd__
的区别就在于该实例对象是在加号的左边还是右边。以 A() + B()
为例,A 和 B 是我们自定义的类:
首先尝试寻找 A 的 __add__ 方法, 如果有直接调用
如果 A 中不存在 __add__ 方法, 那么会去寻找 B 的 __radd__ 方法
但如果是一个整数和自定义的类的实例对象相加呢?
123 + A(): 先寻找 A 的 __radd__
A() + 123: 先寻找 A 的 __add__
代码演示一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class A : def __add__ (self, other ): return "A add" def __radd__ (self, other ): return "A radd" class B : def __add__ (self, other ): return "B add" def __radd__ (self, other ): return "B radd" print (A() + B()) print (B() + A()) print (123 + B()) print (A() + 123 )
除了类似于 __add__
这种实例对象放在左边、__radd__
这种实例对象放在右边,还有__iadd__
,它用于 += 这种形式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class A : def __iadd__ (self, other ): print ("__iadd__ is called" ) return 1 + other a = A() a += 123 print (a)""" __iadd__ is called 124 """
当然这都比较简单,其它的算数魔法方法也是类似的。并且里面的 self 就是对应类的实例对象,有人会觉得这不是废话吗?之所以要提这一点,是为了给下面的Cython做铺垫。
对于 Cython 中的扩展类来说,不使用类似于 __radd__
这种实现方式,只需要定义一个 __add__
即可同时实现 __add__
和 __radd__
。对于 Cython 中的扩展类型 A,a 是 A 的实例对象,如果是 a + 123,那么会调用 __add__
方法,然后第一个参数是 a、第二个参数是123;但如果是 123 + a,那么依旧会调用__add__
,不过此时 __add__
的第一个参数是 123、第二个参数才是 a。所以不像 Python 中的魔法方法,第一个参数 self 永远是实例本身,第一个参数是谁取决于谁在前面。所以将第一个参数叫做 self 容易产生误解,官方也不建议将第一个参数使用 self 作为参数名。
1 2 3 4 5 6 7 8 9 10 11 12 13 cdef class Girl : def __add__ (x, y ): return x, y import pyximportpyximport.install(language_level=3 ) import cython_testg = cython_test.Girl() print (g + 123 ) print (123 + g)
我们看到,__add__
中的参数确实是由位置决定的,那么再来看一个例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 cdef class Girl : cdef long a def __init__ (self, a ): self.a = a def __add__ (x, y ): if isinstance (x, Girl): return (<Girl> x).a + y return (<Girl> y).a + x import pyximportpyximport.install(language_level=3 ) import cython_testg = cython_test.Girl(3 ) print (g + 2 ) print (2 + g) print (g + 2.1 ) print (2.1 + g) g += 4 print (g)
除了 __add__
,Cython 也是支持__iadd__
的,此时的第一个参数是 self,因为 += 这种形式,第一种参数永远是实例对象。
9.2 富比较 Cython 的扩展类可以使用__eq
、__ne__
等等,和 Python 一致的富比较魔法方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 cdef class A : def __eq__ (self, other ): print (self, other) return "==" import pyximportpyximport.install(language_level=3 ) import cython_testa = cython_test.A() print (a == 3 )""" <cython_test.A object at 0x0000015D792C7940> 3 == """ print (3 == a)""" <cython_test.A object at 0x0000015D792C7940> 3 == """
和算术魔法方法不一样,比较操作没有__req__
或者__ieq
__,并且比较的时候第一个参数永远是实例对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 cdef class A : def __eq__ (self, other ): print (self, other) return "A ==" class B : def __eq__ (self, other ): print (self, other) return "B ==" import pyximportpyximport.install(language_level=3 ) import cython_testa = cython_test.A() b = cython_test.B() print (a == 123 )""" <cython_test.A object at 0x00000223641631C0> 123 A == """ print (b == 123 )""" <cython_test.B object at 0x00000223641E71F0> 123 B == """ print (a == 123 )""" <cython_test.A object at 0x00000223641631C0> 123 A == """ print (b == 123 )""" <cython_test.B object at 0x00000223641E71F0> 123 B == """ print (a == b)""" <cython_test.A object at 0x00000223641631C0> <cython_test.B object at 0x00000223641E71F0> A == """ print (b == a)""" <cython_test.B object at 0x00000223641E71F0> <cython_test.A object at 0x00000223641631C0> B == """
链式比较也是可以的,比如:a == b == 123 等价于 a == b and b == 123。
1 2 3 4 5 6 7 8 9 10 11 12 13 import pyximportpyximport.install(language_level=3 ) import cython_testa = cython_test.A() b = cython_test.B() print (a == b == 123 )""" <cython_test.A object at 0x000001817F1D31C0> <cython_test.B object at 0x000001817630E0D0> <cython_test.B object at 0x000001817630E0D0> 123 B == """
先执行 a == b 返回 “A ==”,再执行 b == 3 返回 “B ==”,然后 “A ==” 和 “B ==” 进行 and,前面为真,所以返回后面的 “B ==”。
9.3 迭代器支持 Cython 中的扩展类也是支持迭代器协议的,而且定义的方法和纯 Python 之间是一样的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 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 pyximport pyximport.install(language_level=3 ) import cython_testa = cython_test.A(['椎名真白' , '古明地觉' , '雾雨魔理沙' ]) for _ in a: print (_) """ 椎名真白 古明地觉 雾雨魔理沙 """
知道在 Python 中,for 循环会先去寻找__iter__
,但如果找不到会退而求其次去找 __getitem__
,那么在 Cython 中是不是也是如此呢。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 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 pyximportpyximport.install(language_level=3 ) import cython_testa = cython_test.A(['椎名真白' , '古明地觉' , '雾雨魔理沙' ]) for _ in a: print (_) """ 椎名真白 古明地觉 雾雨魔理沙 """
我们看到,也是一样的。
当然上面只是介绍了魔法方法的一部分,Python 中的魔法方法(比如__getattr__
、__call__
、__hash__
等等)在 Cython 中基本上都支持,并且 Cython 还提供了一些 Python 所没有的魔法方法。当然这些我们就不说了,如果你熟悉 Python 的话,那么在 Cython 中也是按照相同的方式进行使用即可。总之,用久了就孰能生巧了。
注意 :魔法方法只能用def定义,不可以使用cdef或者cpdef。
还有上下文管理器,在Cython中也是一样的用法。Python中基本上所有的魔法方法在Cython都可以直接用。
1 2 3 4 5 6 7 8 9 10 11 12 mport pyximport pyximport.install(language_level=3 ) import cython_testa = cython_test.A() with a: pass """ __enter__ __exit__ """
Cython 中的扩展类,它和 Python 中内置类是等价的,都是直接指向了 C 一级的数据结构,不需要字节码的翻译过程。也正因为如此,它失去一些动态特性,但同时也获得了效率,因为这两者本来就是不可兼得的。
Cython 的类有点复杂,还是需要多使用,不过它毕竟在各方面都和 Python 保持接近,因此学习来也不是那么费劲。
虽然创建扩展类的最简单的方式是通过 Cython,但是通过 Python/C API 直接在 C 中实现的话,则是最有用的练习,但还是那句话,它需要我们对 Python/C API 有一个很深的了解,而这是一个非常难得的事情,因此使用 Cython 就变成了我们最佳的选择。