《流暢的Python》筆記。
本篇是Python進階篇的開始。本篇主要是對Python特殊方法的概述。
1. 前言
資料模型其實是對Python框架的描述,它規範了這門語言自身構件模組的介面,這些模組包括但不限於序列、迭代器、函式、類和上下文管理器。不管在哪種框架下寫程式,都會花費大量時間去實現那些會被框架本身呼叫的方法,Python也不例外。Python直譯器碰到特殊句法時,會使用特殊方法去啟用一些基本的物件操作,這些特殊方法的名字以兩個下劃線開頭,以兩個下劃線結尾(所以特殊方法也叫雙下方法 dunder method),這些特殊方法名能讓自己編寫的物件實現和支援以下的語言構架,並與之互動:
迭代、集合類、屬性訪問、運算子過載、函式和方法的呼叫、物件的建立和銷燬、字串表示形式和格式化、管理上下文(即with
塊)。
下面通過一些例子來介紹常用的特殊方法。
2. 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 = "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
,即命名元組,類似於C/C++
中的struct
,定義如下:
collections.namedtuple(typename, field_names, verbose=False, rename=False)
第一個引數是元組名;第二個是該元組中含的屬性名;第三個參數列示在構建該命名元組之前先列印出該命名元組的結構,如果在控制檯輸入第3行程式碼,並置verbose
為True
的話,會輸出該命名元組的內部結構,實際上它是一個繼承自tuple
的類,由於輸出過長,請大家自行實驗;如果該命名元組的元素名中有Python關鍵字,則需要置第四個引數為True
,這些與關鍵字重名的元素名會被特殊處理。
用命名元組建立一個不帶方法的物件十分簡單:
>>> from chapter20 import Card, FrenchDeck
>>> beer_card = Card("7", "diamonds")
>>> beer_card
Card(rank='7', suit='diamonds')
複製程式碼
由於FrenchDeck
實現了__getitem__
方法,所以可以像操作List
或Tuple
一樣操作FrenchDeck
,比如隨機訪問,切片:
>>> deck = FrenchDeck()
>>> len(deck)
52
>>> deck[0]
Card(rank='2', suit='spades')
>>> deck[-1]
Card(rank='A', suit='hearts')
>>> from random import choice
>>> choice(deck)
Card(rank='4', suit='clubs')
>>> choice(deck)
Card(rank='J', suit='clubs')
>>> deck[:3]
[Card(rank='2', suit='spades'), Card(rank='3', suit='spades'), Card(rank='4', suit='spades')]
>>> deck[12::13]
[Card(rank='A', suit='spades'), Card(rank='A', suit='diamonds'),
Card(rank='A', suit='clubs'), Card(rank='A', suit='hearts')]
複製程式碼
由於實現了該方法,FrenchDeck
還是個可迭代物件,即可以用for
迴圈對其訪問(也可以反向訪問reversed
):
>>> for card in deck:
>>> ... print(card)
Card(rank='2', suit='spades')
Card(rank='3', suit='spades')
Card(rank='4', suit='spades')
-- snip --
Card(rank='Q', suit='hearts')
Card(rank='K', suit='hearts')
Card(rank='A', suit='hearts')
複製程式碼
迭代通常是隱式的,譬如說一個集合型別沒有實現__contains__
方法,那麼in運算子就會按順序做一次迭代搜尋(呼叫__getitem__
),於是in
運算子可以用在FrenchDeck
上:
>>> Card('2', 'spades') in deck
True
複製程式碼
如果對上述deck
變數呼叫sorted
函式,Python將按ASCII
碼進行排序,但這並不是撲克牌的正確排序,所以下面我們自定義排序方法:
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)
複製程式碼
此時輸出的結果就是先按點數排序,再按花色排序。
3. 如何使用特殊方法
需要明確一點,特殊方法的存在是為了給Python直譯器呼叫到,作為程式設計師並不需要呼叫他們,也即是說,沒有my_object.__len__()
這種寫法,而應該是len(my_object)
。說到__len__
方法,如果是Python內建型別,CPython會抄個近路,該方法實際上會直接返回PyVarObject
裡的ob_size
屬性,而PyVarObject
是表示記憶體中長度可變的內痔物件的C語言結構體。
很多時候特殊方法的呼叫是隱式的,比如for i in x:
這個語句,背後其實用的是iter(x)
,而這個函式的背後則是x.__iter__()
方法,當然前提是這個方法在x
中被實現(如果沒被實現則會呼叫__getitem__
方法)。
直接呼叫這個值比呼叫一個方法快很多。直接呼叫特殊方法的頻率應該遠遠低於你去實現它們的次數。
通過內建的函式(例如len
,iter
,str
等)來使用特殊方法是最好的選擇。這些內建函式不僅會呼叫特殊方法,通常還提供額外的好處,而且對於內建的類來說,它們的速度更快。
還有一點值得注意:不要想當然地隨意新增特殊方法,比如__foo__
之類的,因為雖然現在這個名字沒有被Python內部使用,以後就不一定了。
3.1 自定義向量Vector
使用5個特殊方法實現Vector
的字串輸出,取絕對值(如果是複數則是取模),返回布林值,加法和數乘等運算:
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)
# 在Python中,只有0,NULL才是False,其餘均為True
def __bool__(self):
# 更簡單的寫法是:
# return bool(self.x or self.y)
return bool(abs(self))
# 實現加法運算子過載
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
# 實現乘法運算子過載,這裡是數乘,且還沒有實現交換律(需要實現__rmul__方法)
def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
複製程式碼
Python有一個內建函式叫做repr
。該函式通過特殊方法__repr__
來得到一個物件的字串表示形式,如果沒有該特殊方法,當我們在控制檯列印一個向量物件時,得到的字串可能是<Vector object at 0x10e00070>
:
# 程式碼:
v1 = Vector(2, 4)
v2 = Vector(2, 1)
print(v1 + v2)
print(abs(v1))
print(v1 * 3)
# 結果:
Vector(4, 5)
4.47213595499958
Vector(6, 12)
複製程式碼
__repr__
與__str__
的區別與聯絡:前者方便我們除錯和記錄日誌,後者則是給終端使用者看的。後者是在str()
函式被使用,或者是在print
函式列印一個物件的時候才被呼叫,它返回的字串對終端使用者友好。如果只想實現這兩個特殊方法中的一個,__repr__
是更好的選擇,因為如果一個物件沒有__str__
函式,Python又需要呼叫它時,直譯器會用__repr__
代替。
上述Vector
類實現了__bool__
方法,它可用於需要布林值的上下文中(if
, while
, and
, or
, not
等)。預設情況下,我們自己定義的類的例項總被認為是True
,除非重寫了這個類的__bool__
或__len__
方法。bool(x)
的背後是呼叫x.__bool__()
;如果不存在__bool__
方法,那麼bool(x)
會嘗試呼叫x.__len__()
,如果該方法返回0,則bool
返回False
,否則返回True
。
3.2 為什麼len不是普通方法
“實用勝於純粹”(Python之禪裡的一句話)。len
之所以不是一個普通方法,是為了讓Python自帶的資料結構可以走後門,abs
也是同理。但多虧了它是特殊方法,我們也可以把len
用於自定義資料型別。這種處理方式在保持內建型別的效率和保證語言的一致性之間找到了一個平衡點,也印證了“Python之禪”中的另一句話:“不能讓特例特殊到考試破壞既定規則”。
4. 總結
通過實現特殊方法,自定義資料型別可以表現得跟內建型別一樣,從而讓我們寫出更具Python風格(Pythonic)的程式碼。後面的內容將圍繞更多的特殊方法展開。
迎大家關注我的微信公眾號"程式碼港" & 個人網站 www.vpointer.net ~