流畅的Python (Fluent Python) —— 第一部分
Python 最好的品质之一是一致性。
魔术方法(magic method)是特殊方法的昵称。特殊方法也叫双下方法。
1.1 一摞Python风格的纸牌
1 import collections 2 Card = collections.namedtuple('Card', ['rank', 'suit']) # 创建了一个有名字的元组 3 4 5 class FrenchDeck: # 隐式继承了Object类 6 ranks = [str(n) for n in range(2, 11)] + list('JQKA') # 可选的序号 7 suits = 'spades diamonds clubs hearts'.split() # 可选的花色 8 9 def __init__(self): # 创建该类的对象时,会执行此方法 10 self._cards = [Card(rank, suit) for suit in self.suits 11 for rank in self.ranks] 12 13 def __len__(self): # 调用 len(deck) 时,实际上是执行 len.__len__ 方法 14 return len(self._cards) 15 16 def __getitem__(self, position): # 调用 deck[0] 时,实际上是执行 deck.__getitem__(key=0) 17 return self._cards[position] 18 19 20 deck = FrenchDeck() 21 print(len(deck)) # 判断个数的定义,是由__len__实现的 22 print(deck[0]) # 根据位置抽取,此方法是由__getitem__实现的
通过实现特殊方法来利用 Python 数据模型的两个好处 :
1. 作为你的类的用户,他们不必去记住标准操作的各式名称(“怎么得到元素的总数? 是 .size() 还是 .length() 还是别的什么? ”)。
2. 可以更加方便地利用 Python 的标准库,比如 random.choice 函数,从而不用重新发明轮子。
同时,__getitem__ 方法把 [ ] 操作交给了 self._cards 列表,所以我们的 deck 类自动支持切片(slicing)操作。 另外,仅仅实现了 __getitem__ 方法,这一摞牌就变成可迭代的了 。
迭代通常是隐式的,譬如说一个集合类型没有实现 __contains__ 方法,那么 in 运算符就会按顺序做一次迭代搜索。
对牌堆进行排序:
1 # 对牌堆排序 2 suit_values = dict(spades=3, hearts=2, diamonds=1, clubs=0) 3 4 5 def spades_high(card): 6 rank_value = FrenchDeck.ranks.index(card.rank) 7 ''' 8 card.rank:某张牌对象的rank属性(2-A),FrenchDeck.ranks.index(card.rank)返回该属性的位置 9 len(suit_values): 花色的种类,也就是4 10 suit_values[card.suit]: 就是根据某张牌对象的花色,取出该花色对应的值(权重) 11 ''' 12 return rank_value * len(suit_values) + suit_values[card.suit] # 返回这张排在牌堆中的序号(唯一) 13 14 15 for card in sorted(deck, key=spades_high): 16 print(card)
如何洗牌
按照目前的设计, FrenchDeck 是不能洗牌的,因为这摞牌是不可变的(immutable):卡牌和它们的位置都是固定的,除非我们破坏这个类的封装性,直接对 _cards 进行操作。第 11 章会讲到,其实只需要一行代码来实现 __setitem__方法,洗牌功能就不是问题了。
1.2 如何使用特殊方法
首先明确一点,特殊方法(双下方法)的存在是为了被 Python 解释器调用的,你自己并不需要调用它们。
然而如果是 Python 内置的类型,比如列表(list)、字符串(str)、字节序列(bytearray)等,那么 CPython 会抄个近路, __len__ 实际上会直接返回 PyVarObject 里的 ob_size 属性。 PyVarObject 是表示内存中长度可变的内置对象的 C语言结构体。直接读取这个值比调用一个方法要快很多。
很多时候,特殊方法的调用是隐式的,比如 for i in x: 这个语句,背后其实用的是iter(x),而这个函数的背后则是 x.__iter__() 方法。当然前提是这个方法在 x 中被实现了。
通常你的代码无需直接使用特殊方法。除非有大量的元编程(meta programming)存在,直接调用特殊方法的频率应该远远低于你去实现它们的次数。唯一的例外可能是 __init__ 方法,你的代码里可能经常会用到它,目的是在你自己的子类的 __init__ 方法中调用超类的构造器。通过内置的函数(例如 len、 iter、 str,等等)来使用特殊方法是最好的选择。这些内置函数不仅会调用特殊方法,通常还提供额外的好处,而且对于内置的类来说,它们的速度更快。 14.12 节中有详细的例子。不要自己想当然地随意添加特殊方法,因为虽然现在这个名字没有被 Python 内部使用,以后就不一定了。
1 from math import hypot 2 3 4 class Vector: 5 def __init__(self, x=0, y=0): 6 self.x = x 7 self.y = y 8 9 ''' 10 一个对象用字符串的形式表达出来以便辨认,如果没有实现 __repr__,当我们在控制台里打印一个向量的实例时,得到的字符串可能会是 <Vector object at 0x10e100070>。 11 ''' 12 def __repr__(self): 13 return 'Vector(%r, %r)' % (self.x, self.y) 14 # __repr__方便我们调试和记录日志, 15 # __str__是给终端用户用的 16 17 def __abs__(self): 18 return hypot(self.x, self.y) 19 20 # 模是 0 就返回 False,其他返回 True 21 def __bool__(self): 22 # return bool(abs(self)) 23 return bool(self.x or self.y) # 高效写法 24 25 # + 操作 26 def __add__(self, other): 27 x = self.x + other.x 28 y = self.y + other.y 29 return Vector(x, y) 30 31 # * 操作 32 def __mul__(self, other): 33 return Vector(self.x * other, self.y * other)
如果一个对象没有 __str__ 函数,而 Python 又需要调用它的时候,解释器会用 __repr__ 作为替代。