Python __hash__ : identity vs. 等价

标签 python dictionary hash identity equivalence

我有一个类,其实例要通过不同于它们携带的数据值的标识来区分。在我的代码中,我打算使用 == 来表示两个实例在它们的数据方面是等价的,并且 is 表示两个变量引用同一个实例,也就是说,他们是相同的。根据我的理解,这一切都很正常。

此外,我想在 set 中使用实例(等效或不等效),并作为 dict 中的键。这需要为类定义 __hash__ 函数。

但是在这方面我不明白__hash__的文档要求:

The only required property is that objects which compare equal have the same hash value.

这是否意味着两个不同但等价的对象不能用作 dict 中的不同键,或单独出现在 set 中? 在下面的代码中,我通过覆盖 __eq____hash__ 来打破这一要求,以反射(reflect)我的预期用途。它在 Python 2.7 和 3.7 中按预期工作。

如我在此处所做的那样,违反 __hash__ 的要求会产生哪些负面后果?

有没有更好的方法来实现我的目标?

class A( object ):
        def __init__( self, v1, v2 ):
                self.v = ( v1, v2 )

        def __eq__( self, b ):
                return self.v[0] == b.v[0] and self.v[1] == b.v[1]

        def __hash__( self ):
                return id( self )

        def __str__( self ):
                return str( self.v )

p = A( 1, 0 )
q = A( 1, 0 )

print( str( p ), str( q ) )
print( "identical?", p is q )
print( "equivalent?", p == q )
print( "hashes", hash(p), hash(q) )

s = set( [p, q] )
print( "set length", len( s ) )
print( "both in set?", p in s, q in s )

d = { p:3, q:4 }
print( "dict length", len( d ) )
print( "as distinct keys", d[p], d[q] )

最佳答案

The only required property is that objects which compare equal have the same hash value.

规范文本中的“比较相等”表示它们的 __eq__ 方法的结果 - 不要求它们是同一对象。

__hash__ 认为,必须基于 __eq__ 中使用的值,而不是对象的“id”——这部分在您的代码中不正确.为了让它工作,它必须是这样的:

只是做:

      ...
      def __eq__( self, b ):
           return self.v[0] == b.v[0] and self.v[1] == b.v[1]

      def __hash__( self ):
           return hash((self.v[0], self.v[1]))

Does that mean that two distinct but equivalent objects cannot be used as different keys in a dict, or appear individually in a set?

是的。这就是规范的意思。

解决方法是为您的类保留默认的 __eq__ 实现以符合规则,并实现您必须在代码中使用的替代形式的比较。

最直接的方法就是保留 __eq__ 的默认实现,它按身份进行比较,并有一个任意的方法用于比较,(语言中编码的习语不支持运算符覆盖的必须无论如何使用):

class A( object ):
    ...
    def equals( self, b ):
       return self.v[0] == b.v[0] and self.v[1] == b.v[1]

p = A( 1, 0 )
q = A( 1, 0 )

print( str( p ), str( q ) )
print( "identical?", p is q )
print( "equivalent?", p.equals(q) )

有一些方法可以对此进行一些改进 - 但基线是:__eq__ 必须符合规则,并进行身份比较。

一种方法是使用一个内部关联对象作为“命名空间”,您可以将其用于比较:

class CompareSpace:
    def __init__(self, parent):
        self.parent = parent

        def __eq__( self, other ):
            other = other.parent if isinstance(other, type(self)) else other 
            return self.parent.v[0] == other.v[0] and other.v[1] == b.parent.v[1]


    class A:
        def __init__( self, v1, v2 ):
            self.v = ( v1, v2 )
            self.comp = CompareSpace(self)

        def __str__( self ):
            return str( self.v )

p = A( 1, 0 )
q = A( 1, 0 )

print( str( p ), str( q ) )
print( "identical?", p is q )
print( "equivalent?", p.comp == q )
print( "hashes", hash(p), hash(q) )

splinter 的示范

现在我将插入一个例子来说明这是如何中断的——我正在故意创建一个更加中断的类,以确保在第一次尝试时出现问题。但是,即使每 200 万次出现一次问题,您的代码仍然会太破烂,无法用于任何实际应用,即使是个人代码:您将拥有一个不确定的字典:


class Broken:
    def __init__(self, name):
        self.name = name
    def __hash__(self):
        return id(self) % 256
    def __eq__(self, other):
        return True
    def __repr__(self):
        return self.name


In [23]: objs = [Broken(f"{i:02d}") for i in range(64)]                                        

In [24]: print(objs)                                                                           
[00, 01, 02, 03, 04, 05, 06, 07, 08, 09, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63]

In [25]: test = {}                                                                             

In [26]: for obj in objs: 
    ...:     if obj not in test: 
    ...:         test[obj] = 0 
    ...:                                                                                       

In [27]: print(test)                                                                           
{00: 0, 01: 0, 02: 0, 11: 0}

# Or with simple, unconditional, insertion:
In [29]: test = {obj: i for i, obj in enumerate(objs)}                                         

In [30]: test                                                                                  
Out[30]: {00: 57, 01: 62, 02: 63, 11: 60}

(我重复一遍,虽然你的 has 值本身不会冲突,但内部字典代码必须将哈希中的数字减少到其哈希表中的索引 - 不一定按模块(%) - 否则每个空字典将需要 2 ** 64 个空条目,并且仅当所有哈希仅为 64 位宽时)

关于Python __hash__ : identity vs. 等价,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58346286/

相关文章:

java - java中如何从Map键中获取值

c++ - 将 std::string 散列为 std::size_t 以外的东西

python - 套接字错误 "IP address not valid in its context"- Python

python - NetworkX graphviz_layout 不工作?

python - 如何更改 NaN 值之前和之后的值?

用于删除互联网行话/俚语/首字母缩略词的 python 模块

python - 为什么使用 dict.fromkeys() 和大括号为字典初始化赋值时会有差异?

在 C 中创建位置链接列表

python - 对 Python 哈希的操作

ruby - Ruby 中的数组哈希