【流暢的Python】第一章 Python數據模型

目錄

 

特殊方法

1.1一撂Python風格的紙牌

1.2如何使用特殊方法

1.3特殊方法一覽

1.4爲什麼len不是普通方法

1.5本章小結


特殊方法

特殊方法以兩個下劃線開頭,兩個下劃線結尾(例如__getitem__)。

比如obj[key]的背後就是__getitem__方法,爲了能求得my_collection[key]的值,python解釋器實際調用my_collection.__getitem__(key)。

特殊方法也叫雙下方法(dunder method)。

1.1一撂Python風格的紙牌

接下來用一個簡單的例子展示如何實現__getitem__和__len__這兩個特殊方法。

首先利用collections.namedtuple建立一個紙牌類:

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]

需求1:查看一疊牌有多少張

實現:__len__方法

需求2:從一疊牌中抽取特定的一張

實現:__getitem__方法,還能使對象變得可迭代,支持切片操作。

需求3:隨機抽取一張紙牌

實現:需要單獨寫一個方法嗎?不需要,使用random.choice即可。

from random import choice
print(choice(deck))

現在已經可以體會到特殊方法的兩個好處:

①用戶不必記住標準的各式名稱(“怎麼得到元素的總數?是.size()還是.length()還是別的什麼?”)。

②更加方便地利用Python的標準庫,例如random.choice函數,而不必重新發明輪子。

再看看以下需求的實現。

需求4:給紙牌排序

實現:定義排序規則函數

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)

需求5:如何洗牌

實現:打猴子補丁,在運行時修改類或模塊,而不修改源碼

from random import shuffle
def set_card(deck,position,card):
	deck._cards[position]=card
FrenchDeck.__setitem__=set_card
shuffle(deck)
print(deck[:5])

1.2如何使用特殊方法

首先明確,特殊方法是爲了給python解釋器調用的。

例如在執行len(my_object)的時候,如果my_object是自定義數據類型對象,那麼python會調用你實現的__len__方法。

然而如果是python內置類型,比如列表(list)、字符串(str)、字節序列(bytearray)等,那麼python解釋器會抄個近路,__len__實際上會返回PyVarObject的ob_size屬性。PyVarObject是表示內存中長度可變的內置對象的C語言結構體。直接讀取這個值顯然比調用一個方法要快很多。

很多時候,特殊方法的調用是隱式的。比如for i in x:這個語句,背後其實用的是iter(x),而這個函數背後則是x.__iter__()方法。當然前提是這個方法在x中被實現了。

通常無需直接使用特殊方法,除非有大量的元編程存在。唯一的例外是__init__方法,你的代碼裏可能會經常使用。

另外,不要想當然地隨意添加特殊方法,例如__foo__之類的,因爲雖然現在這個名字沒有被Python內部使用,以後就不一定了。

1.2.1 模擬數值類型

以一個簡單地二維向量類爲例:

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)

v1=Vector(2,4)
v2=Vector(2,1)
print('v1+v2=',v1+v2)
print('abs(v1)=',abs(v1))
print('v2*3=',v2*3)

1.2.2 字符串表示形式

Python 有一個內置的函數叫 repr,它能把一個對象用字符串的形式表達出來以便辨認,這就是“字符串表示形式”。repr 就是通過 __repr__ 這個特殊方法來得到一個對象的字符串表示形式的。如果沒有實現 __repr__,當我們在控制檯裏打印一個向量的實例時,得到的字符串可能會是 <Vector object at 0x10e100070>。

交互式控制檯和調試程序(debugger)用 repr 函數來獲取字符串表示形式;在老的使用 % 符號的字符串格式中,這個函數返回的結果用來代替 %r 所代表的對象;同樣,str.format 函數所用到的新式字符串格式化語法也是利用了 repr,才把 !r 字段變成字符串。

__repr__ 所返回的字符串應該準確、無歧義,並且儘可能表達出如何用代碼創建出這個被打印的對象。因此這裏使用了類似調用對象構造器的表達形式(比如 Vector(3, 4) 就是個例子)。

__repr__ 和 __str__ 的區別在於,後者是在 str() 函數被使用,或是在用 print 函數打印一個對象的時候才被調用的,並且它返回的字符串對終端用戶更友好。

如果你只想實現這兩個特殊方法中的一個,__repr__ 是更好的選擇,因爲如果一個對象沒有 __str__ 函數,而 Python 又需要調用它的時候,解釋器會用 __repr__ 作爲替代。

1.2.3 算術運算符

通過 __add__ 和 __mul__,示例 1-2 爲向量類帶來了 + 和 * 這兩個算術運算符。值得注意的是,這兩個方法的返回值都是新創建的向量對象,被操作的兩個向量(self 或 other)還是原封不動,代碼裏只是讀取了它們的值而已。中綴運算符的基本原則就是不改變操作對象,而是產出一個新的值。

乘法交換律將用__rmul__解決。

1.2.4 自定義的布爾值

儘管 Python 裏有 bool 類型,但實際上任何對象都可以用於需要布爾值的上下文中(比如 if 或 while 語句,或者 and、or 和 not 運算符)。爲了判定一個值 x 爲真還是爲假,Python 會調用 bool(x),這個函數只能返回 True 或者 False。

默認情況下,我們自己定義的類的實例總被認爲是真的,除非這個類對 __bool__ 或者 __len__ 函數有自己的實現。bool(x) 的背後是調用 x.__bool__() 的結果;如果不存在 __bool__ 方法,那麼 bool(x) 會嘗試調用 x.__len__()。若返回 0,則 bool 會返回 False;否則返回 True。

我們對 __bool__ 的實現很簡單,如果一個向量的模是 0,那麼就返回 False,其他情況則返回 True。因爲 __bool__ 函數的返回類型應該是布爾型,所以我們通過 bool(abs(self)) 把模值變成了布爾值。

在 Python 標準庫的文檔中,有一節叫作“Built-in Types”,其中規定了真值檢驗的標準。通過實現 __bool__,你定義的對象就可以與這個標準保持一致。

如果想讓 Vector.__bool__ 更高效,可以採用這種實現:

def __bool__(self):
    return bool(self.x or self.y)

它不那麼易讀,卻能省掉從 abs 到 __abs__ 到平方再到平方根這些中間步驟。通過 bool 把返回類型顯式轉換爲布爾值是爲了符合 __bool__ 對返回值的規定,因爲 or 運算符可能會返回 x 或者 y 本身的值:若 x 的值等價於真,則 or 返回 x 的值;否則返回 y 的值。

1.3特殊方法一覽

具體參考Python 語言參考手冊中的“Data Model”。

1.4爲什麼len不是普通方法

”Python之禪“:“實用勝於純粹。” “不能讓特例特殊到開始破壞既定規則。”

換句話說,len 之所以不是一個普通方法,是爲了讓 Python 自帶的數據結構可以走後門,abs 也是同理。但是多虧了它是特殊方法,我們也可以把 len 用於自定義數據類型。這種處理方式在保持內置類型的效率和保證語言的一致性之間找到了一個平衡點。

1.5本章小結

通過實現特殊方法,自定義數據類型可以表現得跟內置類型一樣,從而讓我們寫出更具表達力的代碼——或者說,更具 Python 風格的代碼。

Python 對象的一個基本要求就是它得有合理的字符串表示形式,我們可以通過 __repr__ 和 __str__ 來滿足這個要求。前者方便我們調試和記錄日誌,後者則是給終端用戶看的。這就是數據模型中存在特殊方法 __repr__ 和 __str__ 的原因。

對序列數據類型的模擬是特殊方法用得最多的地方,這一點在 FrenchDeck 類的示例中有所展現。在第 2 章中,我們會着重介紹序列數據類型,然後在第 10 章中,我們會把 Vector 類擴展成一個多維的數據類型,通過這個練習你將有機會實現自定義的序列。

Python 通過運算符重載這一模式提供了豐富的數值類型,除了內置的那些之外,還有 decimal.Decimal 和 fractions.Fraction。這些數據類型都支持中綴算術運算符。在第 13 章中,我們還會通過對 Vector 類的擴展來學習如何實現這些運算符,當然還會提到如何讓運算符滿足交換律和增強賦值。

Python 數據模型的特殊方法還有很多,本書會涵蓋其中的絕大部分,探討如何使用和實現它們。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章