我必须做什么才能使用我的自定义类型的对象作为Python字典中的键(其中我不希望“对象id”作为键),例如。

class MyThing:
    def __init__(self,name,location,length):
            self.name = name
            self.location = location
            self.length = length

我想使用MyThing作为键,如果名称和位置相同,则认为它们相同。 从c# /Java,我习惯于覆盖和提供一个等于和hashcode方法,并承诺不改变hashcode所依赖的任何东西。

我必须在Python中做什么来实现这一点?我应该这样做吗?

(在一个简单的情况下,就像这里,也许它会更好,只是放置(name,location)元组作为键-但考虑到我想要的键是一个对象)


如果你想要特殊的哈希语义,你可以覆盖__hash__,为了使你的类可用作键,你可以覆盖__cmp__或__eq__。比较相等的对象需要具有相同的哈希值。

Python期望__hash__返回一个整数,不建议返回Banana():)

用户定义的类默认有__hash__,调用id(self),正如你所注意到的。

文档中有一些额外的提示:

Classes which inherit a __hash__() method from a parent class but change the meaning of __cmp__() or __eq__() such that the hash value returned is no longer appropriate (e.g. by switching to a value-based concept of equality instead of the default identity based equality) can explicitly flag themselves as being unhashable by setting __hash__ = None in the class definition. Doing so means that not only will instances of the class raise an appropriate TypeError when a program attempts to retrieve their hash value, but they will also be correctly identified as unhashable when checking isinstance(obj, collections.Hashable) (unlike classes which define their own __hash__() to explicitly raise TypeError).


你需要添加2个方法,注意__hash__和__eq__:

class MyThing:
    def __init__(self,name,location,length):
        self.name = name
        self.location = location
        self.length = length

    def __hash__(self):
        return hash((self.name, self.location))

    def __eq__(self, other):
        return (self.name, self.location) == (other.name, other.location)

    def __ne__(self, other):
        # Not strictly necessary, but to avoid having both x==y and x!=y
        # True at the same time
        return not(self == other)

Python dict文档定义了这些关键对象的要求,即它们必须是可哈希的。


Python 2.6或以上版本的另一种方法是使用collections.namedtuple()——它可以节省你编写任何特殊方法:

from collections import namedtuple
MyThingBase = namedtuple("MyThingBase", ["name", "location"])
class MyThing(MyThingBase):
    def __new__(cls, name, location, length):
        obj = MyThingBase.__new__(cls, name, location)
        obj.length = length
        return obj

a = MyThing("a", "here", 10)
b = MyThing("a", "here", 20)
c = MyThing("c", "there", 10)
a == b
# True
hash(a) == hash(b)
# True
a == c
# False

我注意到在python 3.8.8(可能更早)中,你不再需要显式地声明__eq__()和__hash__()来有机会在dict中使用自己的类作为键。

class Apple:
    def __init__(self, weight):
        self.weight = weight
        
    def __repr__(self):
        return f'Apple({self.weight})'

apple_a = Apple(1)
apple_b = Apple(1)
apple_c = Apple(2)

apple_dictionary = {apple_a : 3, apple_b : 4, apple_c : 5}

print(apple_dictionary[apple_a])  # 3
print(apple_dictionary)  # {Apple(1): 3, Apple(1): 4, Apple(2): 5}

我假设Python可以自己管理它,但是我可能错了。


今天的答案是使用python >3.7中的数据类,我知道其他人可能会像我一样在这里结束。它有哈希函数和eq函数。


@dataclass(frozen=True)示例(Python 3.7)

@dataclass之前在https://stackoverflow.com/a/69313714/895245上提到过,但这里有一个例子。

这个很棒的新特性,除其他优点外,自动为你定义__hash__和__eq__方法,使其在dicts和set中正常工作:

dataclass_cheat.py

from dataclasses import dataclass, FrozenInstanceError

@dataclass(frozen=True)
class MyClass1:
    n: int
    s: str

@dataclass(frozen=True)
class MyClass2:
    n: int
    my_class_1: MyClass1

d = {}
d[MyClass1(n=1, s='a')] = 1
d[MyClass1(n=2, s='a')] = 2
d[MyClass1(n=2, s='b')] = 3
d[MyClass2(n=1, my_class_1=MyClass1(n=1, s='a'))] = 4
d[MyClass2(n=2, my_class_1=MyClass1(n=1, s='a'))] = 5
d[MyClass2(n=2, my_class_1=MyClass1(n=2, s='a'))] = 6

assert d[MyClass1(n=1, s='a')] == 1
assert d[MyClass1(n=2, s='a')] == 2
assert d[MyClass1(n=2, s='b')] == 3
assert d[MyClass2(n=1, my_class_1=MyClass1(n=1, s='a'))] == 4
assert d[MyClass2(n=2, my_class_1=MyClass1(n=1, s='a'))] == 5
assert d[MyClass2(n=2, my_class_1=MyClass1(n=2, s='a'))] == 6

# Due to `frozen=True`
o = MyClass1(n=1, s='a')
try:
    o.n = 2
except FrozenInstanceError as e:
    pass
else:
    raise 'error'

正如我们在这个例子中看到的,哈希值是基于对象的内容计算的,而不仅仅是基于实例的地址。这就是为什么会有这样的事情:

d = {}
d[MyClass1(n=1, s='a')] = 1
assert d[MyClass1(n=1, s='a')] == 1

即使第二个MyClass1(n=1, s='a')是一个与第一个完全不同的实例,具有不同的地址,也可以工作。

frozen=True是强制的,否则类是不可哈希的,否则用户可能会在容器用作键后修改对象而无意中使容器不一致。更多文档:https://docs.python.org/3/library/dataclasses.html

在Python 3.10.7, Ubuntu 22.10上测试。