注:原书做者 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,收集每副手牌计数的问题就能够解决了。