[译] 与 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,收集每副手牌计数的问题就能够解决了。

相关文章
相关标签/搜索