楔子
通过魔法方法可以对运算符进行重载,魔法方法的特点就是它的名称以双下划线开头、并以双下划线结尾。我们之前讨论了 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 就变成了我们最佳的选择。
以上就是本次分享的所有内容,如果你觉得文章还不错,欢迎关注公众号:Python编程学习圈,每日干货分享,内容覆盖Python电子书、教程、数据库编程、Django,爬虫,云计算等等。或是前往编程学习网,了解更多编程技术知识。