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()
运行结果:
总结:虽然
FrenchDeck
隐式地继承了 object 类,但功能却不是继承而来的。通过实现__len__
和__getitem__
这两个特殊方法,使FrenchDeck
类就跟一个Python
自有的序列数据类型一样,可以体现出Python
的核心语言特性,如迭代和切片)。同时这个类还可用于标准库中诸如:random.choice
、reversed
和sorted
这些函数。
演示 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__ |