分享

魔法方法在 Cython 中更加魔法

 古明地觉O_o 2022-12-08 发布于北京

楔子


通过魔法方法可以对运算符进行重载,魔法方法的特点就是它的名称以双下划线开头、并以双下划线结尾。我们之前讨论了 __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

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多