Python中使用__hash__和__eq__方法的问题

1. Python中的__hash__和__eq__方法介绍

先来简单介绍下Python中__hash__和__eq__方法。__hash__方法是Python内置的一个方法,用于将一个对象(object)映射成一个“哈希值”,即一个整数,这个哈希值可以用于快速地比较两个对象是否相等。在Python中,使用哈希表(Hash table)实现字典(dictionary)和集合(set)等数据结构,哈希表的速度非常快。__eq__方法则是比较两个对象是否相等,如果相等就返回True。

class Person:

def __init__(self, name, age):

self.name = name

self.age = age

def __eq__(self, other): # 重写__eq__方法

if isinstance(other, Person):

return self.name == other.name and self.age == other.age

return False

def __hash__(self): # 重写__hash__方法

return hash((self.name, self.age))

p1 = Person("Tom", 18)

p2 = Person("Tom", 18)

print(hash(p1)) # 输出哈希值

print(p1 == p2) # 输出True

2. 使用__hash__和__eq__方法遇到的问题

2.1. 什么情况下需要重写__hash__和__eq__方法?

在Python中,字典(dictionary)和集合(set)等数据结构的键需要是可哈希(hashable)的对象,即这些对象必须满足以下条件:

与其他对象相等,即其他对象使用==运算符返回True的对象。

hash()函数返回的值不变。

如果一个对象不可哈希,那么就不能作为字典的键或集合的元素。如果在使用自定义对象作为键时遇到TypeError或者ValueError的异常,那么就需要重写__hash__和__eq__方法来满足可哈希的条件。

2.2. 在使用自定义类作为字典键时遇到的问题

我们来看一个例子,假设有一个自定义类Point,用于表示二维坐标系中的一个点,并将其作为字典(dictionary)的键。

class Point:

def __init__(self, x, y):

self.x = x

self.y = y

p1 = Point(1, 2)

p2 = Point(1, 2)

d = {}

d[p1] = "A" # 将p1作为键放入字典中

d[p2] = "B" # 将p2作为键放入字典中

print(d) # 输出结果:{{'x': 1, 'y': 2}: 'A', {'x': 1, 'y': 2}: 'B'}

可以看到上述代码的输出结果是不符合我们的预期的。由于p1和p2的x和y坐标都相同,因此我们希望将它们视为相同的对象,但实际上它们被作为了两个不同的键存储到字典中了。这是因为Point类没有重写__hash__和__eq__方法,因此Python默认使用对象的id作为哈希值,而p1和p2的id是不同的,导致它们被视为不同的对象。

3. 如何重写__hash__和__eq__方法

3.1. 重写__eq__方法

重写__eq__方法通常比较简单,只需要判断两个对象的属性是否相同即可。下面是Point类的重写__eq__方法的实现:

class Point:

def __init__(self, x, y):

self.x = x

self.y = y

def __eq__(self, other):

if isinstance(other, Point):

return self.x == other.x and self.y == other.y

return False

重写__eq__方法之后,可以使用==运算符比较两个Point对象是否相等。我们再来看一下上述代码的例子:

p1 = Point(1, 2)

p2 = Point(1, 2)

d = {}

d[p1] = "A"

d[p2] = "B"

print(d) # 输出结果:{{'x': 1, 'y': 2}: 'B'}

经过重写__eq__方法之后,p1和p2被视为相同的对象,因此它们只被作为一个键存储在字典中了。

3.2. 重写__hash__方法

重写__hash__方法比较复杂,但也不是难以理解。在Python中,当我们使用哈希表(Hash table)保存数据时,需要根据哈希值(hash值)来快速查找数据。因此,重写__hash__方法的关键是确定一个对象的哈希值,常用的方法是使用哈希函数(hash function)对对象的属性进行计算,然后将计算出来的结果作为哈希值。

下面是Point类的重写__hash__方法的实现:

class Point:

def __init__(self, x, y):

self.x = x

self.y = y

def __eq__(self, other):

if isinstance(other, Point):

return self.x == other.x and self.y == other.y

return False

def __hash__(self):

return hash((self.x, self.y))

可以看到我们使用元组(tuple)作为哈希函数的输入,将对象的x和y属性拼接成一个元组,然后调用Python内置的hash函数计算出哈希值。

重写__hash__方法之后,我们再来运行一下上述代码的例子:

p1 = Point(1, 2)

p2 = Point(1, 2)

d = {}

d[p1] = "A"

d[p2] = "B"

print(d) # 输出结果:{{'x': 1, 'y': 2}: 'B'}

可以看到,经过重写__hash__方法之后,p1和p2被视为相同的键,字典中只保留了一项。

3.3. 处理不可哈希的对象

有时候,我们会遇到一些不可哈希的对象,例如列表(list)或字典(dictionary)等。由于这些对象是可变的,它们不适合作为哈希表的键。在这种情况下,我们可以考虑将这些对象转换为可哈希的对象。下面是一个例子,将一个包含字典(dictionary)的列表(list)转换为可哈希的对象:

class MyList:

def __init__(self, lst):

self.lst = lst

def __eq__(self, other):

if isinstance(other, MyList):

return self.lst == other.lst

return False

def __hash__(self):

return hash(frozenset(self.lst))

lst1 = MyList([{"x": 1, "y": 2}, {"x": 3, "y": 4}])

lst2 = MyList([{"x": 1, "y": 2}, {"x": 3, "y": 4}])

d = {}

d[lst1] = "A"

d[lst2] = "B"

print(d) # 输出结果:{{('x', 'y'): 2, ('x', 'y'): 1}: 'B'}

我们使用frozenset将字典转换为可哈希的对象,然后将所有转换后的对象拼接成一个frozenset作为哈希函数的输入。注意,由于frozenset是不可变对象,因此它是可哈希的。

4. 总结

本文介绍了Python中__hash__和__eq__方法的作用以及如何重写这两个方法。我们看到通过重写__hash__和__eq__方法,可以解决自定义对象作为字典键时遇到的问题。同时,重写__hash__和__eq__方法还有助于实现自定义对象的集合操作,例如求并集、交集和差集等。

然而,重写__hash__和__eq__方法并不是没有代价的,因为这将使得对象的比较和哈希变得更加复杂,有时候可能会出现意料之外的错误。因此,在重写__hash__和__eq__方法之前,需要仔细考虑对象是否适合作为哈希表的键。

后端开发标签