Python 风格的关键完全体现在 Python 的数据模型上,数据模型所描述的 API ,为使用最地道的语言特性来构建开发者自己的对象提供了工具。
当 Python 解析器遇到特殊句法时,会使用特殊方法去激活一些基本的对象操作。特殊方法以双下划线开头,以双下划线结尾。如:obj[key] 的背后就是 __getitem__ 方法。魔术方法是特殊方法的昵称,特殊方法也叫双下方法。
一. 一摞 Python 风格的纸牌
使用 __getitem__ 和 __len__ 创建一摞有序的纸牌:
import collections
Card = collections.namedtuple('Card', ['rank', 'suit'])
class FrenchDeck:
ranks = [str(n) for n in range(2, 11)] + list('JQKA')
# ♠, ♡, ♣, ♢,
suits = ['\u2660', '\u2661', '\u2663', '\u2662']
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, index):
return self._cards[index]
上面的例子,使用 collections.namedtuple 构建了一个简单的类来表示一张纸牌,namedtuple 用以构建只有少数属性但没有方法的类。
演示 1
我们自定义的 FrenchDeck 类可以像任何 python 标准集合类型一样使用 len() 函数,查看一叠牌有多少张:
>> d = FrenchDeck()
>> len(d)
52
演示 2
也可以像列表一样,使用位置索引, d[i] 将调用 __getitem__ 方法:
>> d[0]
Card(rank='2', suit='♠')
>> d[1]
Card(rank='3', suit='♠')
>> d[-1]
Card(rank='A', suit='♢')
演示 3
也可以使用标准库模块提供的 random.choice 方法,从序列中随机选取一个元素。下面,我们如随机取出一张纸牌:
>> import random
>> random.choice(d)
Card(rank='J', suit='♡')
>> random.choice(d)
Card(rank='J', suit='♠')
现在我们已经体会到通过 python 特殊方法,来使用 Python 数据模型的 2 个好处:
- 作为类的用户,无需去记住标准操作的各种名词,如获取长度是
.size,还是.length,还是别的什么... - 可以更加方便地利用python的标准库,如
random.choice函数。
演示 4
因为 __getitem__ 方法把 [] 操作交给了 self.cards 列表,所以我们的 FrenchDeck 实例自动支持切片:
>> d[:4]
[Card(rank='2', suit='♠'),
Card(rank='3', suit='♠'),
Card(rank='4', suit='♠'),
Card(rank='5', suit='♠')]
>> d[-4:]
[Card(rank='J', suit='♢'),
Card(rank='Q', suit='♢'),
Card(rank='K', suit='♢'),
Card(rank='A', suit='♢')]
演示 5
仅仅实现了 __getitem__ 方法,这一摞牌即变得可迭代:
for card in d:
print(card.suit + card.rank, end=',')
if card.rank == 'A':
print()
运行结果:

也可以直接调用内置的 reversed 函数,反向迭代 FrenchDeck 实例:
for card in reversed(d):
print(card.suit + card.rank, end=',')
if card.rank == '2':
print()
运行结果:

演示 6
迭代通常是隐式的,比如一个集合类型没有实现 __contains__ 方法,那么 in 运算符就会按顺序做一次迭代搜索。
因此,in 运算符可以用在我们的 FrenchDeck 实例上,因为它是可迭代的:
>> Card(rank='7', suit='♡') in d
True
>> Card(rank='20', suit='♠') in d
False
演示 7
FrenchDeck 还可以使用 Python 标准库中的 sorted 函数,实现排序:
首先定义一个排序依据的函数:
# 定义花色由大到小的顺序为:♠ ♡ ♢ ♣
SUIT_VALUES = {'\u2660': 3, '\u2661': 2, '\u2662': 1, '\u2663': 0}
def sort_rank(card):
rank_value = FrenchDeck.ranks.index(card.rank)
return rank_value * 10 + SUIT_VALUES[card.suit]
def sort_suit(card):
rank_value = FrenchDeck.ranks.index(card.rank)
return SUIT_VALUES[card.suit] * 100 + rank_value
优先按 rank 的大小排序,rank 相同时则比较 suit 的值:
for card in sorted(d, key=sort_rank):
print(card.suit + card.rank, end=',')
if card.suit == '\u2660':
print()
运行结果:

优先按 suit 的大小排序,suit 相同时则比较 rank 的值:
for card in sorted(d, key=sort_suit):
print(card.suit + card.rank, end=',')
if card.rank == 'A':
print()
运行结果:

演示 8
按照目前的设计,FrenchDeck 还不支持洗牌,因为它是不可变的:
>> random.shuffle(d)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
...
TypeError: 'FrenchDeck' object does not support item assignment
shuffle 函数要调换集合中元素的位置,而 FrenchDeck 只实现了不可变的序列协议,可变的序列还必须提供 __setitem__ 方法:
class FrenchDeck:
ranks = [str(n) for n in range(2, 11)] + list('JQKA')
suits = ['\u2660', '\u2661', '\u2663', '\u2662']
...
def __setitem__(self, index, card):
self._cards[index] = card
洗牌:
>> random.shuffle(d)
>>
没有任何的返回值,可见 random.shuffle 就地修改了可变序列 d 。为便于观察结果,我们定义输入的输出函数:
def print_cards(cards):
for i, card in enumerate(cards):
print(card.suit + card.rank, end=',')
if i != 0 and (i+1) % 13 == 0:
print()
运行结果:

每次洗牌,都是一个随机的序列:

二. 自定义一个二维向量类
首先明确一点,特殊方法的存在是为了被 Python 解析器调用的,例如:我们不会使用 obj.__len__() 这种写法,而是 len(obj)。在执行 len(obj) 时,如果 obj 是一个自定义类的对象,那么 Python 会自己去调用我们实现的 __len__ 方法。
对于 Python 内置的数据类型,比如列表、字符串、字节序列等,那么 CPython 会抄个近路,__len__ 实际上会返回 PyVarObject 里的 ob_size 属性,这是因为直接读取属性比调用一个方法要快得多。
很多时候,特殊方法的调用是隐式的,比如 for i in x: 这个语句其实是调用 iter(x),而这个函数的背后是 x.__iter__() 方法。
通过内置函数如来使用特殊方法是最好的选择。这些内置函数不仅会调用这些方法,通常还提供额外的好处,对于内置类型来说,它们的速度更快。
下面,我们通过定义一个简单的二维向量类,再来体会一下 Python 特殊方法的美妙:
from math import hypot
class Vector:
"""自定义二维向量"""
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def __repr__(self):
return f'Vector({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, scaler):
return Vector(self.x * scaler, self.y * scaler)
使用 Vector 类,就像使用 Python 内置的数据类型一样简单:
>> v1 = Vector(2, 4)
>> v2 = Vector(2, 1)
>> v1 + v2
Vector(4,5)
>> v3 = Vector(3,4)
>> abs(v3)
5.0
>> bool(v3)
True
>> v3 * 100
Vector(300,400)
三. Python 特殊方法一览
跟运算符无关的特殊方法
| 类别 | 方法名 |
|---|---|
| 字符串/字节序列表示形式 |
__repr__ 、 __str__、__format__、__bytes__
|
| 数值转换 |
__abs__、__bool__、__complex__、__int__、__float__、__hash__、__index__
|
| 集合模拟 |
__len__、__getitem__、__setitem__、__delitem__、__contains__
|
| 迭代枚举 |
__iter__、__reversed__、__next__
|
| 可调用模式 | __call__ |
| 上下文管理 |
__enter__、__exit__
|
| 实例的创建和销毁 |
__nex__、__init__、__del__
|
| 属性管理 |
__getattr__、__getattribute__、__setattr__、__delattr__、__dir__
|
| 属性描述符 |
__get__、__set__、__delete__
|
| 跟类相关的服务 |
__prepare__、___instancecheck__、__subclasscheck__
|
跟运算符相关的特殊方法
| 类别 | 方法名和对应的运算符 |
|---|---|
| 一元运算符 |
__neg__ -、__pos__ +、__abs__ abs()
|
| 比较运算符 |
__lt__ <、__le__ <=、__eq__ ==、__ne__ !=、__gt__ >、__ge__ >=
|
| 算数运算符 |
__add__ +、__sub__ -、__mul__ *、__truediv__ /、__floordiv__ //、__mod__ %、__divmod__ divmode()、__pow__ ** / pow()、__round__ round()
|
| 反向算数运算符 |
__radd__、__rsub__、__rmul__、__rtruediv__、__rfloordiv__、__rmod__、__rdivmod__
|
| 增量赋值算术运算符 |
__iadd__、__isub__、__imul__、__itruediv__、__ifloordiv__、__imod__、__ipow__
|
| 位运算符 |
__invert__ ~、__lshift__ <<、__rshift__ >>、__and__ &、__or__ |、 __xor__ ^
|
| 反向位运算符 |
__rlshift__、__rrshift__、__rand__、__ror__、__rxor__
|
| 增量赋值位运算符 |
__ilshift__、__irshift__、__iand__、__ior__、__ixor__
|










