数据模型其实是对 Python 框架的描述,它规范了这门语言自身构建模块的接口,这些模块包括但不限于序列、迭代器、函数、类和上下文管理器。

__getitem__)。比如 obj[key] 的背后就是 __getitem__ 方法,为了能求得 my_collection[key] 的值,解释器实际上会调用 my_collection.__getitem__(key)

这些特殊方法名能让你自己的对象实现和支持以下的语言构架,并与之交互:

  • 迭代
  • 集合类
  • 属性访问
  • 运算符重载
  • 函数和方法的调用
  • 对象的创建和销毁
  • 字符串表示形式和格式化
  • 管理上下文(即 with 块)

1.1 一摞Python风格的纸牌

接下来我会用一个非常简单的例子来展示如何实现 __getitem__ 和 __len__

import collections

# 用 collections.namedtuple 构建了一个简单的类来表示一张纸牌。
Card = collections.namedtuple('Card', ['rank', 'suit'])

class FrenchDeck:
    # 牌面:2、3、...、A
    ranks = [str(n) for n in range(2, 11)] + list('JQKA')
    # 花色:黑、红、花、块
    suits = 'spades diamonds clubs hearts'.split()

    def __init__(self):
        # 生成一副牌
        self._cards = [Card(rank, suit) for suit in self.suits
                                        for rank in self.ranks]

    def __len__(self):
        # 返回牌数
        return len(self._cards)

    def __getitem__(self, position):
        # 获取指定索引的牌
        return self._cards[position]


if __name__ == '__main__':
    deck = FrenchDeck()

    # 查看一叠牌有多少张, 实际上是由 len() 调用 deck.__len__() 实现的。
    print('总牌数:', len(deck))

    # 抽取特定的一张牌, 实际上是由 [] 运算符调用 deck.__getitem__(position) 实现的。
    print('最后一张牌:', deck[-1])

    # __getitem__ 支持随机抽取一张牌
    print('随机抽取一张牌:', choice(deck))

    # __getitem__ 方法把 [] 操作交给了 self._cards 列表,所以 deck 类自动支持切片
    print('切片:', deck[:3])

    # __getitem__ 支持可迭代/反向迭代
    for card in reversed(deck):
        print(card)

 

1.2 如何使用特殊方法

首先明确一点,特殊方法的存在是为了被 Python 解释器调用的,你自己并不需要调用它们。也就是说没有 my_object.__len__() 这种写法,而应该使用 len(my_object)。在执行 len(my_object) 的时候,如果 my_object 是一个自定义类的对象,那么 Python 会自己去调用其中由你实现的 __len__

然而如果是 Python 内置的类型,比如列表(list)、字符串(str)、字节序列(bytearray)等,那么 CPython 会抄个近路,__len__ 实际上会直接返回 PyVarObject 里的 ob_size 属性。PyVarObject

很多时候,特殊方法的调用是隐式的,比如 for i in x: 这个语句,背后其实用的是 iter(x),而这个函数的背后则是 x.__iter__() 方法。当然前提是这个方法在 x 中被实现了。

通常你的代码无需直接使用特殊方法。除非有大量的元编程存在,直接调用特殊方法的频率应该远远低于你去实现它们的次数。唯一的例外可能是 __init__ 方法,你的代码里可能经常会用到它,目的是在你自己的子类的 __init__

示例 1 一个简单的二维向量类:

from math import hypot

class Vector:

    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y

    def __repr__(self):
        return 'Vector(%r, %r)' % (self.x, self.y)

    def __abs__(self):
        return hypot(self.x, self.y)

    def __bool__(self):
        return bool(abs(self))

    def __add__(self, other):
        x = self.x + other.x
        y = self.y + other.y
        return Vector(x, y)

    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)

下面我们通过这个类来讲些这些特殊方法。

1.2.1 字符串表示形式

Python 有一个内置的函数叫 repr,它能把一个对象用字符串的形式表达出来以便辨认,这就是“字符串表示形式”。repr 就是通过 __repr__ 这个特殊方法来得到一个对象的字符串表示形式的。如果没有实现 __repr__,当我们在控制台里打印一个向量的实例时,得到的字符串可能会是 <Vector object at 0x10e100070>

__repr__ 和 __str__ 的区别在于,后者是在 str() 函数被使用,或是在用 print

如果你只想实现这两个特殊方法中的一个,__repr__ 是更好的选择,因为如果一个对象没有 __str__ 函数,而 Python 又需要调用它的时候,解释器会用 __repr__

1.2.2 算术运算符

通过 __add__ 和 __mul__,示例 1-2 为向量类带来了 + 和 * 这两个算术运算符。值得注意的是,这两个方法的返回值都是新创建的向量对象,被操作的两个向量(self 或 other)还是原封不动,代码里只是读取了它们的值而已。中缀运算符的基本原则就是不改变操作对象,而是产出一个新的值。

1.2.3 自定义的布尔值

尽管 Python 里有 bool 类型,但实际上任何对象都可以用于需要布尔值的上下文中(比如 if 或 while 语句,或者 andor 和 not 运算符)。为了判定一个值 x 为还是为,Python 会调用 bool(x),这个函数只能返回 True 或者 False

默认情况下,我们自己定义的类的实例总被认为是真的,除非这个类对 __bool__ 或者 __len__ 函数有自己的实现。bool(x) 的背后是调用 x.__bool__() 的结果;如果不存在 __bool__ 方法,那么 bool(x) 会尝试调用 x.__len__()。若返回 0,则 bool 会返回 False;否则返回 True

1.3 特殊方法一览

表1-1:跟运算符无关的特殊方法

类别

方法名

字符串 / 字节序列表示形式

__repr____str____format____bytes__

数值转换

__abs____bool____complex____int____float____hash____index__

集合模拟

__len____getitem____setitem____delitem____contains__

迭代枚举

__iter____reversed____next__

可调用模拟

__call__

上下文管理

__enter____exit__

实例创建和销毁

__new____init____del__

属性管理

__getattr____getattribute____setattr____delattr____dir__

属性描述符

__get____set____delete__

跟类相关的服务

__prepare____instancecheck____subclasscheck__

表1-2:跟运算符相关的特殊方法

类别

方法名和对应的运算符

一元运算符

__neg__ -__pos__ +__abs__ abs()

众多比较运算符

__lt__ <__le__ <=__eq__ ==__ne__ !=__gt__ >__ge__ >=

算术运算符

__add__ +__sub__ -__mul__ *__truediv__ /__floordiv__ //__mod__ %__divmod__ divmod()__pow__ ** 或pow()__round__ round()

反向算术运算符

__radd____rsub____rmul____rtruediv____rfloordiv____rmod____rdivmod____rpow__

增量赋值算术运算符

__iadd____isub____imul____itruediv____ifloordiv____imod____ipow__

位运算符

__invert__ ~__lshift__ <<__rshift__ >>__and__ &__or__ |__xor__ ^

反向位运算符

__rlshift____rrshift____rand____rxor____ror__

增量赋值位运算符

__ilshift____irshift____iand____ixor____ior__

⚠️ 当交换两个操作数的位置时,就会调用反向运算符(b * a 而不是 a * b)。增量赋值运算符则是一种把中缀运算符变成赋值运算的捷径(a = a * b 就变成了 a *= b)。