一摞Python風格的紙牌
Python 最好的品質之一是一致性。當你使用 Python 工作一會兒后,就會開始理解 Python 語言,並能正確猜測出對你來說全新的語言特征。
用一個非常簡單的例子來展示如何實現 __getitme__ 和__len__ 這兩個特殊方法,通過這個例子我們也能見識到特殊方法的強大。
示例 1-1 里的代碼建立了一個紙牌類。
import collections Card = collections.namedtuple('Card', ['rank', 'suit']) class FrenchDeck: 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]`
如下面這個控制台會話所示,利用 namedtuple,我們可以很輕松地得到一個紙牌對象:
>>> beer_card = Card('7', 'diamonds') >>> beer_card Card(rank='7', suit='diamonds')
這個例子主要還是關注 FrenchDeck 這個類,它既短小又精悍。首先,它跟任何標准 Python 集合類型一樣,可以用 len() 函數來查看一疊牌有多少張
>>> deck = FrenchDeck() >>> len(deck) 52
從一疊牌中抽取特定的一張紙牌,比如說第一張或最后一張,是很容易的:deck[0] 或 deck[-1]。這都是由 __getitem__ 方法提供的:
>>> deck[0] Card(rank='2', suit='spades') >>> deck[-1] Card(rank='A', suit='hearts')
隨機抽取一張紙牌Python 已經內置了從一個序列中隨機選出一個元素的函數 random.choice,我們直接把它用在這一摞紙牌實例上就好:
>>> from random import choice >>> choice(deck) Card(rank='3', suit='hearts') >>> choice(deck) Card(rank='K', suit='spades') >>> choice(deck) Card(rank='2', suit='clubs')
現在已經可以體會到通過實現特殊方法來利用 Python 數據模型的兩個好處。
- 作為你的類的用戶,他們不必去記住標准操作的各式名稱(“怎么得到元素的總數?是 .size() 還是 .length() 還是別的什么?”)。
- 可以更加方便地利用 Python 的標准庫,比如 random.choice 函數,從而不用重新發明輪子。
因為 __getitem__ 方法把 [] 操作交給了 self._cards 列表,所以我們的 deck 類自動支持切片(slicing)操作。
僅僅實現了 __getitem__ 方法,這一摞牌就變成可迭代的了:
>>> for card in deck: # doctest: +ELLIPSIS ... print(card) Card(rank='2', suit='spades') Card(rank='3', suit='spades') Card(rank='4', suit='spades') ...
反向迭代也沒關系:
>>> for card in reversed(deck): ... print(card) Card(rank='A', suit='hearts') Card(rank='K', suit='hearts') Card(rank='Q', suit='hearts')
迭代通常是隱式的,譬如說一個集合類型沒有實現 __contains__ 方法,那么 in 運算符就會按順序做一次迭代搜索。於是,in 運算符可以用在我們的 FrenchDeck 類上,因為它是可迭代的:
>>> Card('Q', 'hearts') in deck True >>> Card('7', 'beasts') in deck False
當然我們也可以進行排序我們按照常規,用點數來判定撲克牌的大小,2 最小、A最大;同時還要加上對花色的判定,黑桃最大、紅桃次之、方塊再次、梅花最小。下面就是按照這個規則來給撲克牌排序的函數,梅花 2 的大小是 0,黑桃 A 是 51:
suit_values = dict(spades=3, hearts=2, diamonds=1, clubs=0) def spades_high(card): rank_value = FrenchDeck.ranks.index(card.rank) return rank_value * len(suit_values) + suit_values[card.suit]
>>> for card in sorted(deck, key=spades_high): ... print(card) Card(rank='2', suit='clubs') Card(rank='2', suit='diamonds') Card(rank='2', suit='hearts') ... (46 cards ommitted) Card(rank='A', suit='diamonds') Card(rank='A', suit='hearts') Card(rank='A', suit='spades')
雖然 FrenchDeck 隱式地繼承了 object 類, 但功能卻不是繼承而來的。我們通過數據模型和一些合成來實現這些功能。通過實現 __len__和 __getitem__ 這兩個特殊方法,FrenchDeck 就跟一個 Python 自有的序列數據類型一樣,可以體現出 Python 的核心語言特性(例如迭代和切片)。同時這個類還可以用於標准庫中諸如random.choice、reversed 和 sorted 這些函數。另外,對合成的運用使得 __len__ 和 __getitem__ 的具體實現可以代理給 self._cards
如何使用特殊方法
首先明確一點,特殊方法的存在是為了被 Python 解釋器調用的,你自己並不需要調用它們。也就是說沒有 my_object.__len__() 這種寫法,而應該使用 len(my_object)。在執行 len(my_object) 的時候,如果my_object 是一個自定義類的對象,那么 Python 會自己去調用其中由你實現的 __len__ 方法。然而如果是 Python 內置的類型,比如列表(list)、字符串(str)、字節序列(bytearray)等,那么 CPython 會抄個近路,__len__ 實際上會直接返回 PyVarObject 里的 ob_size 屬性。PyVarObject 是表示內存中長度可變的內置對象的 C 語言結構體。直接讀取這個值比調用一個方法要快很多。很多時候,特殊方法的調用是隱式的,比如 for i in x: 這個語句,背后其實用的是 iter(x),而這個函數的背后則是 x.__iter__() 方法。當然前提是這個方法在 x 中被實現了。通常你的代碼無需直接使用特殊方法。除非有大量的元編程存在,直接調用特殊方法的頻率應該遠遠低於你去實現它們的次數。唯一的例外可能是 __init__ 方法,你的代碼里可能經常會用到它,目的是在你自己的子類的 __init__ 方法中調用超類的構造器。通過內置的函數(例如 len、iter、str,等等)來使用特殊方法是最好的選擇。這些內置函數不僅會調用特殊方法,通常還提供額外的好處,而且對於內置的類來說,它們的速度更快。
模擬數值類型
利用特殊方法,可以讓自定義對象通過加號“+”(或是別的運算符)進行運算。
一個二維向量加法的例子,Vector(2,4) + Vextor(2,1) =Vector(4,5)
下面這一段代碼就是向量加法:
>>> v1 = Vector(2, 4) >>> v2 = Vector(2, 1) >>> v1 + v2 Vector(4, 5)
注意其中的 + 運算符所得到的結果也是一個向量,而且結果能被控制台友好地打印出來。
abs 是一個內置函數,如果輸入是整數或者浮點數,它返回的是輸入值的絕對值;如果輸入是復數(complex number),那么返回這個復數的模。為了保持一致性,我們的 API 在碰到 abs 函數的時候,也應該返回該向量的模:
>>> v = Vector(3, 4) >>> abs(v) 5.0
我們還可以利用 * 運算符來實現向量的標量乘法(即向量與數的乘法,得到的結果向量的方向與原向量一致 模變大)
>>> v * 3 Vector(9, 12) >>> abs(v * 3) 15.0
示例 1-2 包含了一個 Vector 類的實現,上面提到的操作在代碼里是用這些特殊方法實現的:__repr__、__abs__、__add__ 和 __mul__。
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)
字符串表示形式
Python 有一個內置的函數叫 repr,它能把一個對象用字符串的形式表達出來以便辨認,這就是“字符串表示形式”。repr 就是通過 __repr__這個特殊方法來得到一個對象的字符串表示形式的。如果沒有實現
__repr__,當我們在控制台里打印一個向量的實例時,得到的字符串可能會是 <Vector object at 0x10e100070>。
交互式控制台和調試程序(debugger)用 repr 函數來獲取字符串表示形式;在老的使用 % 符號的字符串格式中,這個函數返回的結果用來代替 %r 所代表的對象;同樣,str.format 函數所用到的新式字符串格
式化語法syntax)也是利用了 repr,才把 !r 字段變成字符串。
在 __repr__ 的實現中,我們用到了 %r 來獲取對象各個屬性的標准字符串表示形式——這是個好習慣,它暗示了一個關鍵:Vector(1, 2)和 Vector('1', '2') 是不一樣的,后者在我們的定義中會報錯,因為向量對象的構造函數只接受數值,不接受字符串 。
__repr__ 所返回的字符串應該准確、無歧義,並且盡可能表達出如何用代碼創建出這個被打印的對象。因此這里使用了類似調用對象構造器的表達形式(比如 Vector(3, 4) 就是個例子)。
__repr__ 和 __str__ 的區別在於,后者是在 str() 函數被使用,或是在用 print 函數打印一個對象的時候才被調用的,並且它返回的字符串對終端用戶更友好。如果你只想實現這兩個特殊方法中的一個,__repr__ 是更好的選擇,因為如果一個對象沒有 __str__ 函數,而 Python 又需要調用它的時候,解釋器會用 __repr__ 作為替代。
特殊方法一覽
類別 | 方法名 |
字符 |
__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__ |
跟運算符相關的特殊方法
類 |
方法名和對應的運算符 |
一 |
__neg__ - 、 __pos__ + 、 __abs__ abs() |
眾 |
__lt__ < 、 __le__ <= 、 __eq__ == 、 __ne__ != 、 __gt__ > 、 __ge__ >= |
算 |
__add__ + 、 __sub__ - 、 __mul__ * 、 __truediv__ / 、 __floordiv__ // 、 __mod__ % 、 __divmod__ |
反 |
__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__ |
為什么len不是普通方法
如果 x 是一個內置類型的實例,那么 len(x) 的速度會非常快。背后的原因是 CPython 會直接從一個 C 結構體里讀取對象的長度,完全不會調用任何方法。獲取一個集合中元素的數量是一個很常見的操作,在
str、list、memoryview 等類型上,這個操作必須高效。
換句話說,len 之所以不是一個普通方法,是為了讓 Python 自帶的數據結構可以走后門,abs 也是同理。但是多虧了它是特殊方法,我們也可以把 len 用於自定義數據類型。這種處理方式在保持內置類型的效率和保證語言的一致性之間找到了一個平衡點.
__ilshift__ 、 __irshift__ 、 __iand__ 、 __ixor__ 、 __ior__