高阶编程:魔术方法应用实战

魔术方法是什么呢?顾名思义,魔术方法是一种特殊的方法,可以让自定义类具有某种魔法。魔术方法名开头结尾都包含两个下划线,例如 init 方法,负责对实例对象进行初始化。

下表将常用的魔术方法分门别类,你或多或少可能已有所了解:

合理应用魔术方法,让自定义类更具 Python 格调,更好地践行 Python 数据抽象以及设计哲学,是每个 Python 工程师必备的编程技巧。接下来,我们一起来考察几个典型的案例,以此抛砖引玉。

运算符重载

运算符,诸如加减乘除( +-*/ ),处理逻辑由 add 等数值操作魔术方法控制。以加法运算符 + 为例,表达式 a + b 在 Python 内部是这样求值的:

  1. 如果 a 定义了魔术方法 add ,则调用 a.add(b) 进行求值;
  2. 如果 b 定义了魔术方法 radd ,则调用 b.radd(a) 进行求值;

因此,只需要提供相关魔术方法,非数值型对象也可以支持算术运算符。

举个例子, str 对象以加法进行对象拼接,以乘法进行对象重复,而除法却没有定义:

1
2
3
4
5
6
7
8
>>> 'hello' + ' ' + 'world'
'hello world'
>>> 'abc' * 10
'abcabcabcabcabcabcabcabcabcabc'
>>> 'hello world' / ' '
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for /: 'str' and 'str'

字符串切分是一个比较常见的操作,若能借助除法操作符来进行则方便许多。实现这个目标并不难,我们只需为 str 编写一个派生类,并实现魔术方法 truediv 即可:

1
2
3
4
class SmartString(str):

    def __truediv__(self, other):
        return self.split(other)

Python 内部,除法操作 / 由 truediv 魔术方法处理。请注意,在老版本 Python 中,除法操作符由 div 魔术方法处理,名字略有差异。

就这么几行代码,我们的字符串类便支持通过除法操作进行字符串切分了:

1
2
3
>>> s = SmartString('hello world')
>>> s / ' '
['hello', 'world']

数值型运算

结构稍微复杂一些的类型,也可以通过实现数值型魔术函数,让它具备数值类型的行为,支持常见的算术操作。

举个简单的例子,我们可以实现一个类来表示向量,xy 属性分别存储向量的坐标:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def __add__(self, other):
        return Vector(x=self.x+other.x, y=self.y+other.y)
    def __sub__(self, other):
        return Vector(x=self.x-other.x, y=self.y-other.y)
    def __mul__(self, scale):
        return Vector(x=self.x*scale, y=self.y*scale)
    def __truediv__(self, scale):
        return Vector(x=self.x/scale, y=self.y/scale)
    def __repr__(self):
        return 'Vector(x={}, y={})'.format(self.x, self.y)
  • add 魔术方法,实现向量加法;
  • sub 魔术方法,实现向量减法;
  • mul 魔术方法,实现向量数乘;
  • truediv 魔术方法,实现向量数除;
  • repr 魔术方法,实现向量表示;

这样一来,我们就可以通过常用算术运算符进行向量运算:

1
2
3
4
5
6
7
8
>>> v1 = Vector(1, 2)
>>> v2 = Vector(3, 4)
>>> v1 + v2
Vector(x=4, y=6)
>>> v1 * 3
Vector(x=3, y=6)
>>> v1 / 2
Vector(x=0.5, y=1.0)

将自定义类与 Python 的运行哲学相融合,还可带来一些额外的收益——充分发挥 Python 的强大执行能力。

举个例子,我们可以借助现成的 sum 内建函数对多个向量进行求和,完全不需要任何额外的代码:

1
2
3
4
5
>>> v1 = Vector(1, 2)
>>> v2 = Vector(3, 4)
>>> v3 = Vector(10, 2)
>>> sum((v1, v2, v3), Vector(0, 0))
Vector(x=14, y=8)

看,Python 语言的表达能力就是这样强大!只需对 Python 设计哲学以及相关运行时约定稍有了解,即可将这一切发挥得淋漓尽致!

属性描述符

上一小节,我们考察了属性描述符,知道它控制着属性查找的行为。属性描述符分为两种:

  • 非数据描述符 :只实现了 get 方法;
  • 数据描述符 :至少实现了 set 或者 delete 方法;

我们对非数据描述符已经有所了解:函数对象就是其中最典型的一个,它包含着类方法 self 参数绑定的全部秘密。然而,我们对数据描述符却一无所知!别急,我们将通过一个典型的例子,将这部分知识补齐。

对于对象 o ,其类型对象为 t 。如果 t 包含数据描述符属性 a ,那么属性设置操作 o.a = x 被 a.set 方法接管;同理,属性删除操作 del o.a 则被 a.delete 方法接管。

如果对象 o 属性空间也存在属性 a ,到底以谁为准呢?简而言之,Python 将照以下优先级逐一确定:

  1. 数据描述符:如果类型对象(含父类)定义了同名数据描述符属性,属性操作将被其接管;
  2. 对象属性:除了①,属性操作默认在属性空间中完成;
  3. 非数据描述符:属性访问时,如果①②均不成功,而类型对象(含父类)定义了同名非数据描述符,属性访问将被其接管;

因此,数据描述符优先级最高,对象属性空间次之,非数据描述符最低。下图是一个典型的例子:

  • 对于属性 a ,由于类型对象 t 属性空间定义了数据描述符,将屏蔽实例对象 o 属性空间中的定义;
  • 对于属性 b ,由于类型对象 t 属性空间定义的只是非数据描述符,仍以实例对象 o 属性空间定义的为准;
  • 对于属性 c ,由于实例对象 o 属性空间未定义,属性访问将以类型对象 t 属性空间定义的非数据描述符为准;
  • 对于属性 c ,由于类型对象 t 属性空间定义的只是非数据描述符,属性设置、删除仍以实例对象 o 属性空间为准;

那么,数据描述符到底有什么用处呢?点击“阅读原文”,获取更多细节!

【Python源码剖析】系列文章首发于公众号【小菜学编程】,敬请关注:

【Python源码剖析】系列文章首发于公众号【小菜学编程】,敬请关注: