资讯专栏INFORMATION COLUMN

[译] 与 Python 无缝集成——基本特殊方法 2

hzc / 2591人阅读

摘要:有三个用例通过和方法定义相等性检测和值不可变对象对于有些无状态对象,例如这些不能被更新的类型。请注意,我们将为不可变对象定义以上两个。

注:原书作者 Steven F. Lott,原书名为 Mastering Object-oriented Python

__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()函数看到,如下:

>>> 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值:

不可变对象:对于有些无状态对象,例如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 "", 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。然而,我们可以并行的设计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,收集每副手牌计数的问题就可以解决了。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/44166.html

相关文章

  • [] Python 无缝集成——基本特殊方法 1

    摘要:这些基本的特殊方法在类中定义中几乎总是需要的。和方法对于一个对象,有两种字符串表示方法。这些都和内置函数以及方法紧密结合。带有说明符的合理响应是返回。 注:原书作者 Steven F. Lott,原书名为 Mastering Object-oriented Python 有许多特殊方法允许类与Python紧密结合,标准库参考将其称之为基本,基础或本质可能是更好的术语。这些特殊...

    yzd 评论0 收藏0
  • [] Python 无缝集成——基本特殊方法 3

    摘要:比较运算符方法有六个比较运算符。根据文档,其映射工作如下第七章创建数字我们会再次回到比较运算符这块。同一个类的对象的比较实现我们来看看一个简单的同一类的比较通过观察一个更完整的类现在我们已经定义了所有六个比较运算符。 注:原书作者 Steven F. Lott,原书名为 Mastering Object-oriented Python __bool__()方法 Python对假有个很...

    2json 评论0 收藏0
  • [] Python 无缝集成——基本特殊方法 4

    摘要:当引用计数为零,则不再需要该对象且可以销毁。这表明当变量被删除时引用计数正确的变为零。方法只能在循环被打破后且引用计数已经为零时调用。这两步的过程允许引用计数或垃圾收集删除已引用的对象,让弱引用悬空。这允许在方法设置对象属性值之前进行处理。 注:原书作者 Steven F. Lott,原书名为 Mastering Object-oriented Python __del__()方法 ...

    Allen 评论0 收藏0
  • Python无缝集成----基本特殊方法.(Mastering Objecting-orient

    摘要:第二章与的无缝集成基本特殊方法笔记中有有一些特殊的方法它们允许我们的类和更好的集成和方法通常方法表示的对象对用户更加友好这个方法是有对象的方法实现的什么时候重写跟非集合对象一个不包括其他集合对象的简单对象这类对象格式通常不会特别复 第二章 与Python的无缝集成----基本特殊方法.(Mastering Objecting-oriented Python 笔记) python中有有一...

    iamyoung001 评论0 收藏0

发表评论

0条评论

hzc

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<