[譯] 與 Python 無縫集成——基本特殊方法 2

注:原書做者 Steven F. Lott,原書名爲 Mastering Object-oriented Pythonpython

__hash__() 方法

內置hash()函數會調用給定對象的__hash__()方法。這裏hash就是將(多是複雜的)值縮減爲小整數值的計算。理想狀況下,一個hash值反映了源值的全部信息。還有一些hash計算常常用於加密,生成很是大的值。算法

Python包含兩個hash庫。在hashlib模塊中有高品質加密hash函數。zlib模塊有兩個高速hash函數:adler32()crc32()。對於相對簡單的值,咱們不使用這些。而對於大型、複雜的值,使用這些算法會有很大幫助。數據結構

hash()函數(和相關的__hash__()方法)用於建立集合中使用的小整數key,以下集合:setfrozensetdict。這些集合使用不可變對象的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

1. 判斷什麼須要hash

不是每個對象都須要提供一個hash值。具體地說,若是咱們建立一個有狀態、可變對象的類,該類萬萬不能返回hash值。__hash__應該定義爲Noneorm

另外一方面,不可變對象返回一個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值:

  • 不可變對象:對於有些無狀態對象,例如tuplesnamedtuplesfrozensets這些不能被更新的類型。咱們有兩個選擇:

    • 不定義__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__()方法函數比較這兩個基本值:suitrank。它不比較派生自rankhard值和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 "<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,它的內容老是在變化的。咱們將經過第二個示例爲您提供一個有狀態的對象在接下來的章節。

5. 從可變手牌變爲凍結手牌

若是咱們想對具體的Hand實例進行統計分析,咱們可能須要建立一個字典來映射Hand實例到計數中。咱們不能用一個可變Hand類做爲一個映射的key。然而,咱們能夠並行的設計setfrozenset而且建立兩個類:HandFrozenHand。這容許咱們能經過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

咱們須要初始化統計字典——statsdefaultdict字典,能夠收集整型計數。爲此咱們可使用一個collections.Counter對象。

經過凍結Hand類,咱們能夠把它做爲一個字典的key,收集每副手牌計數的問題就能夠解決了。

相關文章
相關標籤/搜索