摘要:有三個用例通過和方法定義相等性檢測和值不可變對象對于有些無狀態對象,例如這些不能被更新的類型。請注意,我們將為不可變對象定義以上兩個。
__hash__() 方法注:原書作者 Steven F. Lott,原書名為 Mastering Object-oriented Python
內置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()函數看到,如下:
>>> 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對象中,我們需要修改這個。在下一節中,我們將看一個示例,該示例一個卡片的兩個實例被視為是同一個對象。
1. 判斷什么需要hash不是每一個對象都需要提供一個hash值。具體地說,如果我們創建一個有狀態、可變對象的類,該類萬萬不能返回hash值。__hash__應該定義為None。
另一方面,不可變對象返回一個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__()方法會為相同的行為編寫更少的代碼。
我們可以詳細的看看這三種情況。
2. 為不可變對象繼承定義讓我們看看默認定義操作。下面是一個簡單的類層次結構,使用默認的__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__()方法,這樣每個實例都唯一出現。然而,這并不總是我們想要的。
3. 覆寫不可變對象的定義下面是一個簡單的類層次結構,它為我們提供了__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")}
對于復雜的不可變對象是符合我們預期的。我們必須覆蓋這兩個特殊方法獲得一致的、有意義的結果。
4. 覆寫可變對象的定義這個例子將繼續使用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 "", line 1, in TypeError: unhashable type: "AceCard3"
__hash__被設置為None,這些Card3對象不能被hash,不能為hash()函數提供值。和我們預期的是一樣的。
我們可以執行相等性比較,如下代碼片段所示:
>>> print(c1 == c2) True
相等性測試工作正常,才能很好的讓我們比較牌。它們只是不能被插入到集合或用作字典的key。
我們試試會發生什么:
>>> print(set([c1, c2])) Traceback (most recent call last): File "", line 1, in TypeError: unhashable type: "AceCard3"
當試圖把這些放到集合中,我們會得到這樣一個異常。
顯然,這不是一個正確的定義,在現實生活中和牌一樣是不可變對象。這種風格的定義更適合有狀態的對象,如Hand,它的內容總是在變化的。我們將通過第二個示例為您提供一個有狀態的對象在接下來的章節。
5. 從可變手牌變為凍結手牌如果我們想對具體的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,收集每副手牌計數的問題就可以解決了。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/44166.html
摘要:這些基本的特殊方法在類中定義中幾乎總是需要的。和方法對于一個對象,有兩種字符串表示方法。這些都和內置函數以及方法緊密結合。帶有說明符的合理響應是返回。 注:原書作者 Steven F. Lott,原書名為 Mastering Object-oriented Python 有許多特殊方法允許類與Python緊密結合,標準庫參考將其稱之為基本,基礎或本質可能是更好的術語。這些特殊...
摘要:比較運算符方法有六個比較運算符。根據文檔,其映射工作如下第七章創建數字我們會再次回到比較運算符這塊。同一個類的對象的比較實現我們來看看一個簡單的同一類的比較通過觀察一個更完整的類現在我們已經定義了所有六個比較運算符。 注:原書作者 Steven F. Lott,原書名為 Mastering Object-oriented Python __bool__()方法 Python對假有個很...
摘要:當引用計數為零,則不再需要該對象且可以銷毀。這表明當變量被刪除時引用計數正確的變為零。方法只能在循環被打破后且引用計數已經為零時調用。這兩步的過程允許引用計數或垃圾收集刪除已引用的對象,讓弱引用懸空。這允許在方法設置對象屬性值之前進行處理。 注:原書作者 Steven F. Lott,原書名為 Mastering Object-oriented Python __del__()方法 ...
摘要:第二章與的無縫集成基本特殊方法筆記中有有一些特殊的方法它們允許我們的類和更好的集成和方法通常方法表示的對象對用戶更加友好這個方法是有對象的方法實現的什么時候重寫跟非集合對象一個不包括其他集合對象的簡單對象這類對象格式通常不會特別復 第二章 與Python的無縫集成----基本特殊方法.(Mastering Objecting-oriented Python 筆記) python中有有一...
閱讀 1070·2023-04-25 14:35
閱讀 2832·2021-11-16 11:45
閱讀 3421·2021-09-04 16:48
閱讀 2187·2021-08-10 09:43
閱讀 533·2019-08-30 13:17
閱讀 1627·2019-08-29 13:27
閱讀 892·2019-08-26 13:58
閱讀 2157·2019-08-26 13:48