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__方法之前,需要仔细考虑对象是否适合作为哈希表的键。