楔子
通过魔法方法可以对运算符进行重载,魔法方法的特点就是它的名称以双下划线开头、并以双下划线结尾。我们之前讨论了 __cinit__, __init__, __dealloc__,并了解了它们分别用于 C 一级的初始化、Python 一级的初始化、对象的释放(特指 C 中的指针)。
除了那三个,Cython 也支持其它的魔法方法,但是注意:Cython 的析构不是 __del__,它用于前面介绍的描述符。至于析构函数则由 __dealloc__ 负责实现,所以 __dealloc__ 不仅用于 C 指针指向内存的释放,还负责 Python 对象的析构。
算术魔法方法
假设在 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__;
代码演示一下:
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()) # A add print(B() + A()) # B add print(123 + B()) # B radd print(A() + 123) # A add
除了类似于 __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__方法。
当然这都比较简单,其它的算数魔法方法也是类似的。并且里面的 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 作为参数名。
但是说实话,用了 Python 这么些年,第一个参数不写成 self 感觉有点别扭。
cdef class Girl: def __add__(x, y): return x, y def __repr__(self): return "Girl 实例"
编译测试一下:
import pyximport pyximport.install(language_level=3) from cython_test import Girl print(Girl() + 123) print(123 + Girl()) """ (Girl 实例, 123) (123, Girl 实例) """
我们看到,__add__ 中的参数确实是由位置决定的,那么再来看一个例子。
cdef class Girl: cdef long a def __init__(self, a): self.a = a def __add__(x, y): # 这里必须要通过 <Girl> 转化一下 # 因为 x 和 y 都是外界传来的动态变量 # 而属性 a 不是一个 public 或者 readonly # 所以动态变量无法访问,真正的私有对动态变量是屏蔽的 # 但静态变量可以自由访问,所以我们需要转成静态变量 if isinstance(x, Girl): return (<Girl> x).a + y # 或者使用 cdef 重新静态声明一个静态变量 # 比如 cdef Girl y1 = y,然后 y1.a + x 也可以 return (<Girl> y).a + x
编译测试一下:
import pyximport pyximport.install(language_level=3) import cython_test g = cython_test.Girl(3) print(g + 2) # 5 print(2 + g) # 5 # 和浮点数运算也是可以的 print(g + 2.1) # 5.1 print(2.1 + g) # 5.1 g += 4 print(g) # 7
除了 __add__,Cython 也支持 __iadd__,此时的第一个参数是 self,因为 += 这种形式,第一个参数永远是实例对象。
另外这里说的 __add__ 和 __iadd__ 只是举例,其它的算术操作也是可以的。
富比较
Cython 的扩展类也可以使用 __eq, __ne__ 等等和 Python 一致的富比较魔法方法。
cdef class A: # 比较操作,Cython 和 Python 类似 # 第一个参数永远是 self # 调用谁的 __eq__,第一个参数就是谁 def __eq__(self, y): return self, y def __repr__(self): return "A 实例" print(A() == 123) print(123 == A()) """ (A 实例, 123) (A 实例, 123) """
其它的操作符也类似,可以自己试一下。
小结
Python 里面的魔法方法有很多,像迭代器协议、上下文管理、反射等等,Cython 都支持,并且用法一致,这里就不多说了。
注意:魔法方法只能用def定义,不可以使用cdef或者cpdef。
到目前为止,关于扩展类的内容就说完了。总之扩展类和内置类是等价的,都是直接指向了 C 一级的数据结构,不需要字节码的翻译过程。也正因为如此,它失去一些动态特性,但同时也获得了效率,因为这两者本来就是不可兼得的。
Cython 的类有点复杂,还是需要多使用,不过它毕竟在各方面都和 Python 保持接近,因此学习来也不是那么费劲。虽然创建扩展类最简单的方式是通过 Cython,但是通过 Python/C API 直接在 C 中实现的话,则是最有用的练习。
但还是那句话,这需要我们对 Python/C API 有一个很深的了解,而这是一件非常难得的事情,因此使用 Cython 就变成了我们最佳的选择。
E N D