摘要:有三個(gè)用例通過(guò)和方法定義相等性檢測(cè)和值不可變對(duì)象對(duì)于有些無(wú)狀態(tài)對(duì)象,例如這些不能被更新的類型。請(qǐng)注意,我們將為不可變對(duì)象定義以上兩個(gè)。
__hash__() 方法注:原書作者 Steven F. Lott,原書名為 Mastering Object-oriented Python
內(nèi)置hash()函數(shù)會(huì)調(diào)用給定對(duì)象的__hash__()方法。這里hash就是將(可能是復(fù)雜的)值縮減為小整數(shù)值的計(jì)算。理想情況下,一個(gè)hash值反映了源值的所有信息。還有一些hash計(jì)算經(jīng)常用于加密,生成非常大的值。
Python包含兩個(gè)hash庫(kù)。在hashlib模塊中有高品質(zhì)加密hash函數(shù)。zlib模塊有兩個(gè)高速hash函數(shù):adler32()和crc32()。對(duì)于相對(duì)簡(jiǎn)單的值,我們不使用這些。而對(duì)于大型、復(fù)雜的值,使用這些算法會(huì)有很大幫助。
hash()函數(shù)(和相關(guān)的__hash__()方法)用于創(chuàng)建集合中使用的小整數(shù)key,如下集合:set、frozenset和dict。這些集合使用不可變對(duì)象的hash值來(lái)快速定位對(duì)象。
在這里不變性是很重要的,我們會(huì)多次提到它。不可變對(duì)象不會(huì)改變它們的狀態(tài)。例如,數(shù)字3并沒(méi)有改變狀態(tài),它總是3。更復(fù)雜的對(duì)象也是一樣的,可以有一個(gè)不變的狀態(tài)。Python字符串是不可變的,這樣它們可以用來(lái)映射作集合的key。
默認(rèn)的__hash__()繼承自對(duì)象本身,返回一個(gè)基于對(duì)象的內(nèi)部ID值。這個(gè)值可以通過(guò)id()函數(shù)看到,如下:
>>> x = object() >>> hash(x) 269741571 >>> id(x) 4315865136 >>> id(x) / 16 269741571.0
由此,我們可以看到在作者的系統(tǒng)中,hash值就是對(duì)象的id / 16。這一細(xì)節(jié)針對(duì)不同平臺(tái)可能會(huì)有所不同。例如,CPython使用可移植的C庫(kù),Jython依賴于Java JVM。
至關(guān)重要的是,內(nèi)部ID和默認(rèn)__hash__()方法間有一種強(qiáng)聯(lián)系。這意味著每個(gè)對(duì)象默認(rèn)是可以hash且完全不同的,即使它們似乎相同。
如果我們想將有相同值的不同對(duì)象合并到單個(gè)可hash對(duì)象中,我們需要修改這個(gè)。在下一節(jié)中,我們將看一個(gè)示例,該示例一個(gè)卡片的兩個(gè)實(shí)例被視為是同一個(gè)對(duì)象。
1. 判斷什么需要hash不是每一個(gè)對(duì)象都需要提供一個(gè)hash值。具體地說(shuō),如果我們創(chuàng)建一個(gè)有狀態(tài)、可變對(duì)象的類,該類萬(wàn)萬(wàn)不能返回hash值。__hash__應(yīng)該定義為None。
另一方面,不可變對(duì)象返回一個(gè)hash值,這樣對(duì)象就可用作字典中的key或集合中的一員。在這種情況下,hash值需要用并行的方式檢測(cè)相等性。對(duì)象有不同的hash值但被看作相等的對(duì)象是糟糕的。相反的,對(duì)象具有相同hash值,實(shí)際上不相等是可以接受的。
我們?cè)诒容^運(yùn)算符中看到的__eq__()方法與hash關(guān)系密切。
有三種級(jí)別的等式比較:
相同的hash值:這意味著兩個(gè)對(duì)象可能是相等的。該hash值為我們提供了一個(gè)快速檢查對(duì)象相等的可能性。如果hash值是不同的,兩個(gè)對(duì)象不可能是相等的,他們也不可能是相同的對(duì)象。
等號(hào)比較:這意味著hash值也一定相等。這是==操作符的定義。對(duì)象可能是相同的對(duì)象。
相同的IDD:這意味著他們是同一個(gè)對(duì)象。進(jìn)行了等號(hào)比較且有相同的hash值。這是is操作符的定義。
Hash的基本規(guī)律(FLH)是:對(duì)象等號(hào)比較必須具有相同的hash值。
在相等性檢測(cè)中我們能想到的第一步是hash比較。
然而,反過(guò)來(lái)是不正確的。對(duì)象可以有相同的hash值但比較是不相等的。在創(chuàng)建集合或字典時(shí)導(dǎo)致一些預(yù)計(jì)的處理開銷是正當(dāng)?shù)?。我們不能確切的從更大的數(shù)據(jù)結(jié)構(gòu)創(chuàng)建不同的64位hash值。將有不相等的對(duì)象被簡(jiǎn)化為一致相等的hash值。
在使用集合和字典時(shí)比較hash值是一個(gè)預(yù)期的開銷,它們是同時(shí)發(fā)生的。這些集合有內(nèi)部的算法在hash沖突時(shí)會(huì)使用替換位置進(jìn)行處理。
有三個(gè)用例通過(guò)__eq__()和__hash__()方法定義相等性檢測(cè)和hash值:
不可變對(duì)象:對(duì)于有些無(wú)狀態(tài)對(duì)象,例如tuples、namedtuples、frozensets這些不能被更新的類型。我們有兩個(gè)選擇:
不定義__hash__()和__eq__()。這意味著什么都不做,使用繼承的定義。在這種情況下__hash__()返回一個(gè)簡(jiǎn)單的函數(shù)對(duì)象的ID值,然后__eq__()比較ID值。默認(rèn)的相等性檢測(cè)有時(shí)是違反直覺(jué)的。我們的應(yīng)用程序可能需要兩個(gè)Card(1, Clubs)實(shí)例檢測(cè)相等性和計(jì)算相同的hash,默認(rèn)情況下是不會(huì)發(fā)生這種情況的。
定義__hash__()和__eq__()。請(qǐng)注意,我們將為不可變對(duì)象定義以上兩個(gè)。
可變對(duì)象:這些是有狀態(tài)的對(duì)象,可以進(jìn)行內(nèi)部修改。我們有一個(gè)選擇:
定義__eq__(),但__hash__()設(shè)置為None。這些不能被用作dict中的key或set中的項(xiàng)目。
請(qǐng)注意,有一個(gè)額外可能的組合:定義__hash__()但對(duì)__eq__()使用一個(gè)默認(rèn)的定義。這其實(shí)是浪費(fèi)時(shí)間,作為默認(rèn)的__eq__()方法其實(shí)和is操作符是一樣的。默認(rèn)的__hash__()方法會(huì)為相同的行為編寫更少的代碼。
我們可以詳細(xì)的看看這三種情況。
2. 為不可變對(duì)象繼承定義讓我們看看默認(rèn)定義操作。下面是一個(gè)簡(jiǎn)單的類層次結(jié)構(gòu),使用默認(rèn)的__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)
這是一個(gè)不可變對(duì)象的類層次結(jié)構(gòu)。我們還沒(méi)有實(shí)現(xiàn)特殊方法防止屬性更新。在下一章我們將看看屬性訪問(wèn)。
當(dāng)我們使用這個(gè)類層次結(jié)構(gòu)時(shí),看看會(huì)發(fā)生什么:
>>> c1 = AceCard(1, "?") >>> c2 = AceCard(1, "?")
我們定義的兩個(gè)相同的Card實(shí)例。我們可以檢查id()的值,如下代碼片段所示:
>>> print(id(c1), id(c2)) 4302577232 4302576976
他們有不同的id()號(hào),不同的對(duì)象。這符合我們的預(yù)期。
我們可以使用is操作符來(lái)檢查它們是否一樣,如下代碼片段所示:
>>> c1 is c2 False
“is測(cè)試”是基于id()的數(shù)字,它告訴我們,它們確實(shí)是獨(dú)立的對(duì)象。
我們可以看到,它們的hash值是不同的:
>>> print(hash(c1), hash(c2)) 268911077 268911061
這些hash值直接來(lái)自id()值。這是我們期望繼承的方法。在這個(gè)實(shí)現(xiàn)中,我們可以從id()函數(shù)中計(jì)算出hash值,如下代碼片段所示:
>>> id(c1) / 16 268911077.0 >>> id(c2) / 16 268911061.0
hash值是不同的,它們之間的比較必須不相等。這符合hash的定義和相等性定義。然而,這違背了我們對(duì)這個(gè)類的期望。下面是一個(gè)相等性檢查:
>>> print(c1 == c2) False
我們使用相同的參數(shù)創(chuàng)建了它們。它們比較后不相等。在某些應(yīng)用程序中,這樣不好。例如,當(dāng)處理牌的時(shí)候累加計(jì)數(shù),我們不想給一張牌做6個(gè)計(jì)數(shù)因?yàn)槭褂玫氖?副牌牌盒。
我們可以看到,他們是不可變對(duì)象,我們可以把它們放在一個(gè)集合里:
>>> print(set([c1, c2])) {AceCard(suit="?", rank=1), AceCard(suit="?", rank=1)}
這是標(biāo)準(zhǔn)庫(kù)參考文檔中記錄的行為。默認(rèn)情況下,我們會(huì)得到一個(gè)基于對(duì)象ID的__hash__()方法,這樣每個(gè)實(shí)例都唯一出現(xiàn)。然而,這并不總是我們想要的。
3. 覆寫不可變對(duì)象的定義下面是一個(gè)簡(jiǎn)單的類層次結(jié)構(gòu),它為我們提供了__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)
原則上這個(gè)對(duì)象是不可變的。還沒(méi)有正式的機(jī)制來(lái)讓它不可變。關(guān)于這個(gè)機(jī)制我們將在第3章《屬性訪問(wèn)、屬性和描述符》中看看如何防止屬性值變化。
同時(shí),注意前面的代碼省略了的兩個(gè)子類,從前面的示例來(lái)看并沒(méi)有顯著的改變。
__eq__()方法函數(shù)比較這兩個(gè)基本值:suit和rank。它不比較派生自rank的hard值和soft值。
21點(diǎn)的規(guī)則使這個(gè)定義有點(diǎn)可疑?;ㄉ?1點(diǎn)中實(shí)際上并不重要。我們只是比較牌值嗎?我們是否應(yīng)該定義一個(gè)額外的方法,而不是僅僅比較牌值?或者,我們應(yīng)該依靠應(yīng)用程序比較牌值的正確性?對(duì)于這些問(wèn)題沒(méi)有最好的回答,只是做好一個(gè)權(quán)衡。
__hash__()方法函數(shù)計(jì)算的位模式使用兩個(gè)值作為基礎(chǔ)進(jìn)行hash,然后對(duì)hash值進(jìn)行異或計(jì)算。使用^操作符是一種應(yīng)急的hash方法,很有用。對(duì)于更大、更復(fù)雜的對(duì)象,使用更復(fù)雜的hash會(huì)更合適。在構(gòu)造某個(gè)東東之前使用ziplib會(huì)有bug哦。
讓我們來(lái)看看這些類對(duì)象的行為。我們期望它們比較是相等的且能夠在集合和字典中正常使用。這里有兩個(gè)對(duì)象:
>>> c1 = AceCard2(1, "?") >>> c2 = AceCard2(1, "?")
我們定義的兩個(gè)實(shí)例似乎是相同的牌。我們可以檢查ID值,以確保他們是不同的對(duì)象:
>>> print(id(c1), id(c2)) 4302577040 4302577296 >>> print(c1 is c2) False
這些有不同的id()數(shù)字。當(dāng)我們通過(guò)is操作符檢測(cè),我們看到它們是截然不同的。
讓我們來(lái)比較一下hash值:
>>> print(hash(c1), hash(c2)) 1259258073890 1259258073890
hash值是相同的。這意味著他們可能是相等的。
等號(hào)操作符告訴我們,他們是相等的
>>> print(c1 == c2) True
它們是不可變的,我們可以把它們放到一個(gè)集合中,如下所示:
>>> print(set([c1, c2])) {AceCard2(suit="?", rank="A")}
對(duì)于復(fù)雜的不可變對(duì)象是符合我們預(yù)期的。我們必須覆蓋這兩個(gè)特殊方法獲得一致的、有意義的結(jié)果。
4. 覆寫可變對(duì)象的定義這個(gè)例子將繼續(xù)使用Cards類。可變的牌是很奇怪的想法,甚至是錯(cuò)誤的。然而,我們想小小調(diào)整一下前面的例子。
以下是一個(gè)類層次結(jié)構(gòu),為我們提供了適合可變對(duì)象的__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)
讓我們來(lái)看看這些類對(duì)象的行為。我們期望它們比較是相等的,但是在集合和字典中完全不起作用。我們創(chuàng)建如下兩個(gè)對(duì)象:
>>> c1 = AceCard3(1, "?") >>> c2 = AceCard3(1, "?")
我們定義的兩個(gè)實(shí)例似乎是相同的牌。我們可以檢查ID值,以確保他們是不同的對(duì)象:
>>> print(id(c1), id(c2)) 4302577040 4302577296
如果我們嘗試獲取hash值,毫無(wú)意外,我們將會(huì)看到如下情形:
>>> print(hash(c1), hash(c2)) Traceback (most recent call last): File "", line 1, in TypeError: unhashable type: "AceCard3"
__hash__被設(shè)置為None,這些Card3對(duì)象不能被hash,不能為hash()函數(shù)提供值。和我們預(yù)期的是一樣的。
我們可以執(zhí)行相等性比較,如下代碼片段所示:
>>> print(c1 == c2) True
相等性測(cè)試工作正常,才能很好的讓我們比較牌。它們只是不能被插入到集合或用作字典的key。
我們?cè)囋嚂?huì)發(fā)生什么:
>>> print(set([c1, c2])) Traceback (most recent call last): File "", line 1, in TypeError: unhashable type: "AceCard3"
當(dāng)試圖把這些放到集合中,我們會(huì)得到這樣一個(gè)異常。
顯然,這不是一個(gè)正確的定義,在現(xiàn)實(shí)生活中和牌一樣是不可變對(duì)象。這種風(fēng)格的定義更適合有狀態(tài)的對(duì)象,如Hand,它的內(nèi)容總是在變化的。我們將通過(guò)第二個(gè)示例為您提供一個(gè)有狀態(tài)的對(duì)象在接下來(lái)的章節(jié)。
5. 從可變手牌變?yōu)閮鼋Y(jié)手牌如果我們想對(duì)具體的Hand實(shí)例進(jìn)行統(tǒng)計(jì)分析,我們可能需要?jiǎng)?chuàng)建一個(gè)字典來(lái)映射Hand實(shí)例到計(jì)數(shù)中。我們不能用一個(gè)可變Hand類作為一個(gè)映射的key。然而,我們可以并行的設(shè)計(jì)set和frozenset并且創(chuàng)建兩個(gè)類:Hand和FrozenHand。這允許我們能通過(guò)FrozenHand類“凍結(jié)”Hand類;凍結(jié)版本是不可變的,可以作為一個(gè)字典的key。
下面是一個(gè)簡(jiǎn)單的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
這是一個(gè)可變對(duì)象(__hash__是None),它有一個(gè)恰當(dāng)?shù)南嗟刃詸z測(cè)來(lái)比較兩副手牌。
下面是關(guān)于Hand的一個(gè)“凍結(jié)”版本:
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
凍結(jié)版本有一個(gè)構(gòu)造函數(shù),將從另一個(gè)Hand類構(gòu)建一個(gè)Hand類。它定義了一個(gè)__hash__()方法,計(jì)算牌的hash值的總和,這個(gè)值受sys.hash_info.modules限制。大多數(shù)情況,這種基于模塊的計(jì)算,在計(jì)算復(fù)合對(duì)象hash時(shí)效果相當(dāng)好。
我們現(xiàn)在可以使用這些類進(jìn)行操作,如下代碼片段所示:
stats = defaultdict(int) d = Deck() h = Hand(d.pop(), d.pop(), d.pop()) h_f = FrozenHand(h) stats[h_f] += 1
我們需要初始化統(tǒng)計(jì)字典——stats為defaultdict字典,可以收集整型計(jì)數(shù)。為此我們可以使用一個(gè)collections.Counter對(duì)象。
通過(guò)凍結(jié)Hand類,我們可以把它作為一個(gè)字典的key,收集每副手牌計(jì)數(shù)的問(wèn)題就可以解決了。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/44166.html
摘要:這些基本的特殊方法在類中定義中幾乎總是需要的。和方法對(duì)于一個(gè)對(duì)象,有兩種字符串表示方法。這些都和內(nèi)置函數(shù)以及方法緊密結(jié)合。帶有說(shuō)明符的合理響應(yīng)是返回。 注:原書作者 Steven F. Lott,原書名為 Mastering Object-oriented Python 有許多特殊方法允許類與Python緊密結(jié)合,標(biāo)準(zhǔn)庫(kù)參考將其稱之為基本,基礎(chǔ)或本質(zhì)可能是更好的術(shù)語(yǔ)。這些特殊...
摘要:比較運(yùn)算符方法有六個(gè)比較運(yùn)算符。根據(jù)文檔,其映射工作如下第七章創(chuàng)建數(shù)字我們會(huì)再次回到比較運(yùn)算符這塊。同一個(gè)類的對(duì)象的比較實(shí)現(xiàn)我們來(lái)看看一個(gè)簡(jiǎn)單的同一類的比較通過(guò)觀察一個(gè)更完整的類現(xiàn)在我們已經(jīng)定義了所有六個(gè)比較運(yùn)算符。 注:原書作者 Steven F. Lott,原書名為 Mastering Object-oriented Python __bool__()方法 Python對(duì)假有個(gè)很...
摘要:當(dāng)引用計(jì)數(shù)為零,則不再需要該對(duì)象且可以銷毀。這表明當(dāng)變量被刪除時(shí)引用計(jì)數(shù)正確的變?yōu)榱?。方法只能在循環(huán)被打破后且引用計(jì)數(shù)已經(jīng)為零時(shí)調(diào)用。這兩步的過(guò)程允許引用計(jì)數(shù)或垃圾收集刪除已引用的對(duì)象,讓弱引用懸空。這允許在方法設(shè)置對(duì)象屬性值之前進(jìn)行處理。 注:原書作者 Steven F. Lott,原書名為 Mastering Object-oriented Python __del__()方法 ...
摘要:第二章與的無(wú)縫集成基本特殊方法筆記中有有一些特殊的方法它們?cè)试S我們的類和更好的集成和方法通常方法表示的對(duì)象對(duì)用戶更加友好這個(gè)方法是有對(duì)象的方法實(shí)現(xiàn)的什么時(shí)候重寫跟非集合對(duì)象一個(gè)不包括其他集合對(duì)象的簡(jiǎn)單對(duì)象這類對(duì)象格式通常不會(huì)特別復(fù) 第二章 與Python的無(wú)縫集成----基本特殊方法.(Mastering Objecting-oriented Python 筆記) python中有有一...
閱讀 1124·2023-04-25 14:35
閱讀 2849·2021-11-16 11:45
閱讀 3447·2021-09-04 16:48
閱讀 2201·2021-08-10 09:43
閱讀 545·2019-08-30 13:17
閱讀 1638·2019-08-29 13:27
閱讀 912·2019-08-26 13:58
閱讀 2168·2019-08-26 13:48