要实现自定义对象的比较,需定义富比较方法如__eq__、__lt__等,确保类型检查时返回NotImplemented,并通过functools.total_ordering简化代码;若重写__eq__,还需正确实现__hash__以保证对象可哈希,尤其在对象不可变时基于相等属性计算哈希值;对于包含列表或嵌套对象的复杂结构,递归利用元素自身的比较方法进行深度比较,确保逻辑一致性和正确性。

在Python中,要让你的自定义对象能够像内置类型一样进行比较,比如判断相等(
==
<
>
__eq__
__lt__
__gt__
实现对象的比较操作,主要依赖于Python的特殊方法(也称为“魔法方法”或“dunder methods”)。这些方法允许你自定义对象在特定操作下的行为。对于比较操作,主要的有:
__eq__(self, other)
==
__ne__(self, other)
!=
__eq__
__ne__
not self.__eq__(other)
__lt__(self, other)
<
__le__(self, other)
<=
__gt__(self, other)
>
__ge__(self, other)
>=
当你在类中定义了这些方法后,Python在进行比较时会优先调用它们。一个很重要的点是,这些方法应该在无法进行有效比较时返回
NotImplemented
False
NotImplemented
other
a.__eq__(b)
NotImplemented
b.__eq__(a)
下面是一个简单的例子,我们创建一个
Point
__eq__
__lt__
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Point({self.x}, {self.y})"
def __eq__(self, other):
if not isinstance(other, Point):
return NotImplemented # 告诉Python,我们不知道怎么和非Point对象比较
return self.x == other.x and self.y == other.y
def __lt__(self, other):
if not isinstance(other, Point):
return NotImplemented
# 这里我们定义一个简单的比较规则:先比x,x相同再比y
if self.x < other.x:
return True
if self.x == other.x and self.y < other.y:
return True
return False
# 测试
p1 = Point(1, 2)
p2 = Point(1, 2)
p3 = Point(2, 1)
p4 = Point(1, 3)
print(f"p1 == p2: {p1 == p2}") # True
print(f"p1 == p3: {p1 == p3}") # False
print(f"p1 < p3: {p1 < p3}") # True (1 < 2)
print(f"p1 < p4: {p1 < p4}") # True (x相同,2 < 3)
print(f"p3 < p1: {p3 < p1}") # False (2不小于1)
print(f"p1 != p3: {p1 != p3}") # True (因为__eq__返回False,所以!=是True)
# 尝试与不同类型比较
print(f"p1 == 'hello': {p1 == 'hello'}") # False (因为NotImplemented,然后Python决定它们不相等)可以看到,一旦我们定义了
__eq__
__lt__
__le__
__gt__
__ge__
functools.total_ordering
__eq__
__lt__
__le__
__gt__
__ge__
total_ordering
from functools import total_ordering
@total_ordering
class PointWithTotalOrdering:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"PointWithTotalOrdering({self.x}, {self.y})"
def __eq__(self, other):
if not isinstance(other, PointWithTotalOrdering):
return NotImplemented
return self.x == other.x and self.y == other.y
def __lt__(self, other):
if not isinstance(other, PointWithTotalOrdering):
return NotImplemented
if self.x < other.x:
return True
if self.x == other.x and self.y < other.y:
return True
return False
# 测试
p_a = PointWithTotalOrdering(1, 2)
p_b = PointWithTotalOrdering(1, 3)
p_c = PointWithTotalOrdering(2, 1)
print(f"p_a <= p_b: {p_a <= p_b}") # True (total_ordering自动生成)
print(f"p_c > p_a: {p_c > p_a}") # True (total_ordering自动生成)这样一来,代码会更简洁,也更不容易出错。
__eq__
__hash__
这其实是个挺有意思的话题,因为
__eq__
__hash__
__eq__
__hash__
__hash__
为什么这么说呢?你想啊,字典(
dict
set
__hash__
如果你的
__eq__
__hash__
obj1
H1
obj2
obj1
obj1 == obj2
True
H2
obj2
obj1
obj2
H2
H2
obj1
所以,Python有这么几条规则:
__eq__
object
__eq__
object
__hash__
__eq__
__hash__
__eq__
__hash__
__hash__
None
__eq__
__hash__
obj1 == obj2
hash(obj1) == hash(obj2)
__hash__
举个例子,如果我们的
Point
x
y
__hash__
None
Point
__hash__
class ImmutablePoint:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"ImmutablePoint({self.x}, {self.y})"
def __eq__(self, other):
if not isinstance(other, ImmutablePoint):
return NotImplemented
return self.x == other.x and self.y == other.y
def __hash__(self):
# 使用tuple的hash方法,因为tuple是不可变的,并且其hash值由内部元素决定
return hash((self.x, self.y))
p1 = ImmutablePoint(1, 2)
p2 = ImmutablePoint(1, 2)
p3 = ImmutablePoint(2, 1)
my_set = {p1, p3}
print(f"Set before adding p2: {my_set}") # {ImmutablePoint(1, 2), ImmutablePoint(2, 1)}
my_set.add(p2) # p2和p1相等,哈希值也相等,所以不会重复添加
print(f"Set after adding p2: {my_set}") # {ImmutablePoint(1, 2), ImmutablePoint(2, 1)}
my_dict = {p1: "first point"}
print(f"Dict with p1: {my_dict}") # {ImmutablePoint(1, 2): 'first point'}
print(f"Accessing dict with p2: {my_dict[p2]}") # 'first point' (因为p2和p1相等且哈希值相同)你看,一旦
__hash__
说实话,实现这些比较方法,看似简单,但里面坑还真不少。我个人觉得,最容易踩的几个坑,以及一些可以让你少走弯路的小技巧,值得我们聊聊。
常见的陷阱:
类型检查不足或错误处理: 这是最常见的。很多人在
__eq__
__lt__
other
other.x
other.y
other
AttributeError
other
NotImplemented
# 错误示范
# def __eq__(self, other):
# return self.x == other.x and self.y == other.y # 如果other不是Point,会出错
# 正确做法
def __eq__(self, other):
if not isinstance(other, MyClass): # 或者 type(other) is MyClass
return NotImplemented
# ... 比较逻辑 ...返回
NotImplemented
False
NotImplemented
False
不一致的比较逻辑: 比如你定义了
__lt__
__gt__
a < b
True
b > a
False
functools.total_ordering
递归比较的死循环: 如果你的对象内部包含相同类型的其他对象,并且你的比较逻辑是递归的,那么就得小心了。比如一个
Node
next_node
next_node
Node
__eq__
忘记 __hash__
__eq__
__hash__
最佳实践:
始终进行类型检查并返回 NotImplemented
isinstance(other, MyClass)
NotImplemented
利用 functools.total_ordering
<
<=
==
!=
>
>=
@functools.total_ordering
__eq__
__lt__
明确“相等”的定义: 在开始编写
__eq__
__eq__
__hash__
保持对象不可变性(如果可能): 如果你的对象是不可变的,那么实现
__hash__
__hash__
为调试提供良好的 __repr__
__repr__
当我们的自定义对象不仅仅是几个简单属性的集合,而是包含了列表、字典,甚至是其他自定义对象实例时,实现比较操作就变得稍微复杂一些了。这时候,你的比较逻辑需要能够“深入”到这些复杂结构内部去。这其实是一个递归或者说迭代的过程。
我们来设想一个
Playlist
Song
Playlist
首先,定义我们的
Song
@total_ordering
class Song:
def __init__(self, title, artist, duration_seconds):
self.title = title
self.artist = artist
self.duration_seconds = duration_seconds
def __repr__(self):
return f"Song(title='{self.title}', artist='{self.artist}', duration={self.duration_seconds}s)"
def __eq__(self, other):
if not isinstance(other, Song):
return NotImplemented
return (self.title == other.title and
self.artist == other.artist and
self.duration_seconds == other.duration_seconds)
def __lt__(self, other):
if not isinstance(other, Song):
return NotImplemented
# 按照标题、艺术家、时长顺序比较
return ((self.title, self.artist, self.duration_seconds) <
(other.title, other.artist, other.duration_seconds))
def __hash__(self):
# 歌曲通常是不可变的,可以哈希
return hash((self.title, self.artist, self.duration_seconds))
现在,我们有了
Song
Playlist
__eq__
songs
class Playlist:
def __init__(self, name, songs=None):
self.name = name
self.songs = list(songs) if songs is not None else []
def __repr__(self):
return f"Playlist(name='{self.name}', songs={self.songs})"
def add_song(self, song):
if isinstance(song, Song):
self.songs.append(song)
else:
raise TypeError("Only Song objects can be added to a playlist.")
def __eq__(self, other):
if not isinstance(other, Playlist):
return NotImplemented
# 首先比较播放列表的名称
if self.name != other.name:
return False
# 然后比较歌曲列表
# 这里的关键是:列表的比较操作会委托给列表中元素的__eq__方法
# 并且列表长度也必须相同
if len(self.songs) != len(other.songs):
return False
# 逐个比较列表中的歌曲
# 注意:Python列表默认的__eq__就是按元素顺序和__eq__进行比较的
# 所以我们可以直接用列表的相等判断
return self.songs == other.songs
# 如果需要排序,也需要实现__lt__以上就是如何实现对象的比较操作(__eq__, __lt__等)?的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号