注:原書做者 Steven F. Lott,原書名爲 Mastering Object-oriented Pythonpython
__hash__()
方法內置hash()
函數會調用給定對象的__hash__()
方法。這裏hash就是將(多是複雜的)值縮減爲小整數值的計算。理想狀況下,一個hash值反映了源值的全部信息。還有一些hash計算常常用於加密,生成很是大的值。算法
Python包含兩個hash庫。在hashlib
模塊中有高品質加密hash函數。zlib
模塊有兩個高速hash函數:adler32()
和crc32()
。對於相對簡單的值,咱們不使用這些。而對於大型、複雜的值,使用這些算法會有很大幫助。數據結構
hash()
函數(和相關的__hash__()
方法)用於建立集合中使用的小整數key,以下集合:set
、frozenset
和dict
。這些集合使用不可變對象的hash值來快速定位對象。函數
在這裏不變性是很重要的,咱們會屢次提到它。不可變對象不會改變它們的狀態。例如,數字3並無改變狀態,它老是3。更復雜的對象也是同樣的,能夠有一個不變的狀態。Python字符串是不可變的,這樣它們能夠用來映射做集合的key。測試
默認的__hash__()
繼承自對象自己,返回一個基於對象的內部ID值。這個值能夠經過id()
函數看到,以下:ui
>>> x = object() >>> hash(x) 269741571 >>> id(x) 4315865136 >>> id(x) / 16 269741571.0
由此,咱們能夠看到在做者的系統中,hash值就是對象的id / 16
。這一細節針對不一樣平臺可能會有所不一樣。例如,CPython使用可移植的C庫,Jython依賴於Java JVM。加密
相當重要的是,內部ID和默認__hash__()
方法間有一種強聯繫。這意味着每一個對象默認是能夠hash且徹底不一樣的,即便它們彷佛相同。設計
若是咱們想將有相同值的不一樣對象合併到單個可hash對象中,咱們須要修改這個。在下一節中,咱們將看一個示例,該示例一個卡片的兩個實例被視爲是同一個對象。code
不是每個對象都須要提供一個hash值。具體地說,若是咱們建立一個有狀態、可變對象的類,該類萬萬不能返回hash值。__hash__
應該定義爲None
。orm
另外一方面,不可變對象返回一個hash值,這樣對象就可用做字典中的key或集合中的一員。在這種狀況下,hash值須要用並行的方式檢測相等性。對象有不一樣的hash值但被看做相等的對象是糟糕的。相反的,對象具備相同hash值,實際上不相等是能夠接受的。
咱們在比較運算符中看到的__eq__()
方法與hash關係密切。
有三種級別的等式比較:
相同的hash值:這意味着兩個對象多是相等的。該hash值爲咱們提供了一個快速檢查對象相等的可能性。若是hash值是不一樣的,兩個對象不多是相等的,他們也不多是相同的對象。
等號比較:這意味着hash值也必定相等。這是==
操做符的定義。對象多是相同的對象。
相同的IDD:這意味着他們是同一個對象。進行了等號比較且有相同的hash值。這是is
操做符的定義。
Hash的基本規律(FLH)是:對象等號比較必須具備相同的hash值。
在相等性檢測中咱們能想到的第一步是hash比較。
然而,反過來是不正確的。對象能夠有相同的hash值但比較是不相等的。在建立集合或字典時致使一些預計的處理開銷是正當的。咱們不能確切的從更大的數據結構建立不一樣的64位hash值。將有不相等的對象被簡化爲一致相等的hash值。
在使用集合和字典時比較hash值是一個預期的開銷,它們是同時發生的。這些集合有內部的算法在hash衝突時會使用替換位置進行處理。
有三個用例經過__eq__()
和__hash__()
方法定義相等性檢測和hash值:
不可變對象:對於有些無狀態對象,例如tuples
、namedtuples
、frozensets
這些不能被更新的類型。咱們有兩個選擇:
不定義__hash__()
和__eq__()
。這意味着什麼都不作,使用繼承的定義。在這種狀況下__hash__()
返回一個簡單的函數對象的ID值,而後__eq__()
比較ID值。默認的相等性檢測有時是違反直覺的。咱們的應用程序可能須要兩個Card(1, Clubs)
實例檢測相等性和計算相同的hash,默認狀況下是不會發生這種狀況的。
定義__hash__()
和__eq__()
。請注意,咱們將爲不可變對象定義以上兩個。
可變對象:這些是有狀態的對象,能夠進行內部修改。咱們有一個選擇:
定義__eq__()
,但__hash__()
設置爲None
。這些不能被用做dict
中的key或set
中的項目。
請注意,有一個額外可能的組合:定義__hash__()
但對__eq__()
使用一個默認的定義。這實際上是浪費時間,做爲默認的__eq__()
方法其實和is
操做符是同樣的。默認的__hash__()
方法會爲相同的行爲編寫更少的代碼。
咱們能夠詳細的看看這三種狀況。
讓咱們看看默認定義操做。下面是一個簡單的類層次結構,使用默認的__hash__()
和__eq__()
定義:
class Card: insure= False def __init__(self, rank, suit, hard, soft): self.rank = rank self.suit = suit self.hard = hard self.soft = soft def __repr__(self): return "{__class__.__name__}(suit={suit!r}, rank={rank!r})" .format(__class__=self.__class__, **self.__dict__) def __str__(self): return "{rank}{suit}".format(**self.__dict__) class NumberCard(Card): def __init__(self, rank, suit): super().__init__(str(rank), suit, rank, rank) class AceCard(Card): def __init__(self, rank, suit): super().__init__("A", suit, 1, 11) class FaceCard(Card): def __init__(self, rank, suit): super().__init__({11: 'J', 12: 'Q', 13: 'K'}[rank], suit, 10, 10)
這是一個不可變對象的類層次結構。咱們尚未實現特殊方法防止屬性更新。在下一章咱們將看看屬性訪問。
當咱們使用這個類層次結構時,看看會發生什麼:
>>> c1 = AceCard(1, '♣') >>> c2 = AceCard(1, '♣')
咱們定義的兩個相同的Card
實例。咱們能夠檢查id()
的值,以下代碼片斷所示:
>>> print(id(c1), id(c2)) 4302577232 4302576976
他們有不一樣的id()
號,不一樣的對象。這符合咱們的預期。
咱們可使用is
操做符來檢查它們是否同樣,以下代碼片斷所示:
>>> c1 is c2 False
「is
測試」是基於id()
的數字,它告訴咱們,它們確實是獨立的對象。
咱們能夠看到,它們的hash值是不一樣的:
>>> print(hash(c1), hash(c2)) 268911077 268911061
這些hash值直接來自id()
值。這是咱們指望繼承的方法。在這個實現中,咱們能夠從id()
函數中計算出hash值,以下代碼片斷所示:
>>> id(c1) / 16 268911077.0 >>> id(c2) / 16 268911061.0
hash值是不一樣的,它們之間的比較必須不相等。這符合hash的定義和相等性定義。然而,這違背了咱們對這個類的指望。下面是一個相等性檢查:
>>> print(c1 == c2) False
咱們使用相同的參數建立了它們。它們比較後不相等。在某些應用程序中,這樣很差。例如,當處理牌的時候累加計數,咱們不想給一張牌作6個計數由於使用的是6副牌牌盒。
咱們能夠看到,他們是不可變對象,咱們能夠把它們放在一個集合裏:
>>> print(set([c1, c2])) {AceCard(suit='♣', rank=1), AceCard(suit='♣', rank=1)}
這是標準庫參考文檔中記錄的行爲。默認狀況下,咱們會獲得一個基於對象ID的__hash__()
方法,這樣每一個實例都惟一出現。然而,這並不老是咱們想要的。
下面是一個簡單的類層次結構,它爲咱們提供了__hash__()
和__eq__()
的定義:
class Card2: insure = False def __init__(self, rank, suit, hard, soft): self.rank = rank self.suit = suit self.hard = hard self.soft = soft def __repr__(self): return "{__class__.__name__}(suit={suit!r}, rank={rank!r})". format(__class__=self.__class__, **self.__dict__) def __str__(self): return "{rank}{suit}".format(**self.__dict__) def __eq__(self, other): return self.suit == other.suit and self.rank == other.rank def __hash__(self): return hash(self.suit) ^ hash(self.rank) class AceCard2(Card2): insure = True def __init__(self, rank, suit): super().__init__("A", suit, 1, 11)
原則上這個對象是不可變的。尚未正式的機制來讓它不可變。關於這個機制咱們將在第3章《屬性訪問、屬性和描述符》中看看如何防止屬性值變化。
同時,注意前面的代碼省略了的兩個子類,從前面的示例來看並無顯著的改變。
__eq__()
方法函數比較這兩個基本值:suit
和rank
。它不比較派生自rank
的hard
值和soft
值。
21點的規則使這個定義有點可疑。花色在21點中實際上並不重要。咱們只是比較牌值嗎?咱們是否應該定義一個額外的方法,而不是僅僅比較牌值?或者,咱們應該依靠應用程序比較牌值的正確性?對於這些問題沒有最好的回答,只是作好一個權衡。
__hash__()
方法函數計算的位模式使用兩個值做爲基礎進行hash,而後對hash值進行異或計算。使用^
操做符是一種應急的hash方法,頗有用。對於更大、更復雜的對象,使用更復雜的hash會更合適。在構造某個東東以前使用ziplib
會有bug哦。
讓咱們來看看這些類對象的行爲。咱們指望它們比較是相等的且可以在集合和字典中正常使用。這裏有兩個對象:
>>> c1 = AceCard2(1, '♣') >>> c2 = AceCard2(1, '♣')
咱們定義的兩個實例彷佛是相同的牌。咱們能夠檢查ID值,以確保他們是不一樣的對象:
>>> print(id(c1), id(c2)) 4302577040 4302577296 >>> print(c1 is c2) False
這些有不一樣的id()
數字。當咱們經過is
操做符檢測,咱們看到它們是大相徑庭的。
讓咱們來比較一下hash值:
>>> print(hash(c1), hash(c2)) 1259258073890 1259258073890
hash值是相同的。這意味着他們多是相等的。
等號操做符告訴咱們,他們是相等的
>>> print(c1 == c2) True
它們是不可變的,咱們能夠把它們放到一個集合中,以下所示:
>>> print(set([c1, c2])) {AceCard2(suit='♣', rank='A')}
對於複雜的不可變對象是符合咱們預期的。咱們必須覆蓋這兩個特殊方法得到一致的、有意義的結果。
這個例子將繼續使用Cards
類。可變的牌是很奇怪的想法,甚至是錯誤的。然而,咱們想小小調整一下前面的例子。
如下是一個類層次結構,爲咱們提供了適合可變對象的__hash__()
和__eq__()
的定義:
class Card3: insure = False def __init__(self, rank, suit, hard, soft): self.rank = rank self.suit = suit self.hard = hard self.soft = soft def __repr__(self): return "{__class__.__name__}(suit={suit!r}, rank={rank!r})". format(__class__=self.__class__, **self.__dict__) def __str__(self): return "{rank}{suit}".format(**self.__dict__) def __eq__(self, other): return self.suit == other.suit and self.rank == other.rank # and self.hard == other.hard and self.soft == other.soft __hash__ = None class AceCard3(Card3): insure= True def __init__(self, rank, suit): super().__init__("A", suit, 1, 11)
讓咱們來看看這些類對象的行爲。咱們指望它們比較是相等的,可是在集合和字典中徹底不起做用。咱們建立以下兩個對象:
>>> c1 = AceCard3(1, '♣') >>> c2 = AceCard3(1, '♣')
咱們定義的兩個實例彷佛是相同的牌。咱們能夠檢查ID值,以確保他們是不一樣的對象:
>>> print(id(c1), id(c2)) 4302577040 4302577296
若是咱們嘗試獲取hash值,毫無心外,咱們將會看到以下情形:
>>> print(hash(c1), hash(c2)) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unhashable type: 'AceCard3'
__hash__
被設置爲None
,這些Card3
對象不能被hash,不能爲hash()
函數提供值。和咱們預期的是同樣的。
咱們能夠執行相等性比較,以下代碼片斷所示:
>>> print(c1 == c2) True
相等性測試工做正常,才能很好的讓咱們比較牌。它們只是不能被插入到集合或用做字典的key。
咱們試試會發生什麼:
>>> print(set([c1, c2])) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unhashable type: 'AceCard3'
當試圖把這些放到集合中,咱們會獲得這樣一個異常。
顯然,這不是一個正確的定義,在現實生活中和牌同樣是不可變對象。這種風格的定義更適合有狀態的對象,如Hand
,它的內容老是在變化的。咱們將經過第二個示例爲您提供一個有狀態的對象在接下來的章節。
若是咱們想對具體的Hand
實例進行統計分析,咱們可能須要建立一個字典來映射Hand
實例到計數中。咱們不能用一個可變Hand
類做爲一個映射的key。然而,咱們能夠並行的設計set
和frozenset
而且建立兩個類:Hand
和FrozenHand
。這容許咱們能經過FrozenHand
類「凍結」Hand
類;凍結版本是不可變的,能夠做爲一個字典的key。
下面是一個簡單的Hand
定義:
class Hand: def __init__(self, dealer_card, *cards): self.dealer_card = dealer_card self.cards = list(cards) def __str__(self): return ", ".join(map(str, self.cards)) def __repr__(self): return "{__class__.__name__}({dealer_card!r}, {_cards_str})" .format(__class__=self.__class__, _cards_str=", " .join(map(repr, self.cards)), **self.__dict__) def __eq__(self, other): return self.cards == other.cards and self.dealer_card == other.dealer_card __hash__ = None
這是一個可變對象(__hash__
是None
),它有一個恰當的相等性檢測來比較兩副手牌。
下面是關於Hand
的一個「凍結」版本:
import sys class FrozenHand(Hand): def __init__(self, *args, **kw): if len(args) == 1 and isinstance(args[0], Hand): # Clone a hand other = args[0] self.dealer_card = other.dealer_card self.cards = other.cards else: # Build a fresh hand super().__init__(*args, **kw) def __hash__(self): h = 0 for c in self.cards: h = (h + hash(c)) % sys.hash_info.modulus return h
凍結版本有一個構造函數,將從另外一個Hand
類構建一個Hand
類。它定義了一個__hash__()
方法,計算牌的hash值的總和,這個值受sys.hash_info.modules
限制。大多數狀況,這種基於模塊的計算,在計算複合對象hash時效果至關好。
咱們如今可使用這些類進行操做,以下代碼片斷所示:
stats = defaultdict(int) d = Deck() h = Hand(d.pop(), d.pop(), d.pop()) h_f = FrozenHand(h) stats[h_f] += 1
咱們須要初始化統計字典——stats
爲defaultdict
字典,能夠收集整型計數。爲此咱們可使用一個collections.Counter
對象。
經過凍結Hand
類,咱們能夠把它做爲一個字典的key,收集每副手牌計數的問題就能夠解決了。