
咱们写 Python 时,肯定遇到过这种坑:自己定义了一个类(比如 Person),创建了几个实例,想比较大小或者判断是否相等,结果直接报错!比如这样:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
# 创建两个实例
p1 = Person("张三", 20)
p2 = Person("李四", 22)
# 尝试比较大小,直接报错!
print(p1 < p2) # 报错:TypeError: '<' not supported between instances of 'Person' and 'Person'为啥会这样?因为 Python 根本不知道你想 “按什么规则” 比较两个 Person 实例 —— 是按年龄比?还是按名字长度比?默认情况下,自定义类没这个逻辑,所以没法比。
这篇文章就教你怎么给自定义类加 “比较能力”,从相等判断到大小比较,再到列表排序,全给你讲明白!
要让自定义类支持比较,核心是定义 Python 的 “特殊方法”(就是前后带两个下划线的方法)。这些方法对应着咱们常用的比较运算符(==、<、> 这些)。
我先把常用的比较特殊方法整理成表格,一看就懂:
特殊方法名 | 对应运算符 | 作用说明 | 必须返回值 |
|---|---|---|---|
| == | 判断两个实例是否 “值相等” | 布尔值(True/False) |
| != | 判断两个实例是否 “值不相等” | 布尔值 |
| < | 判断当前实例是否 “小于” 另一个实例 | 布尔值 |
| 判断当前实例是否 “大于” 另一个实例 | 布尔值 | |
| <= | 判断当前实例是否 “小于等于” 另一个实例 | 布尔值 |
|
| 判断当前实例是否 “大于等于” 另一个实例 | 布尔值 |
这里有个关键知识点:不用所有方法都写!
Python 有 “反向推导” 逻辑,比如你定义了__lt__(<),Python 会自动推导__gt__(>)—— 因为 “a> b” 本质就是 “b < a”。同理,定义了__lt__和__eq__,__le__(<=)就能推导成 “a < b 或者 a == b”。
还有个更偷懒的工具:functools.total_ordering装饰器。只要你定义了__eq__,再随便定义一个比较方法(比如__lt__),这个装饰器能自动帮你实现剩下的 4 个方法!后面实战会讲怎么用。
最常用的比较就是 “判断两个实例是否相等”,这就需要写__eq__方法。
很多人刚开始写__eq__会犯这个错:直接拿属性比,不判断对方是不是同一个类的实例。比如:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
# 错误写法:没判断other的类型
def __eq__(self, other):
# 如果other不是Person实例(比如是个整数),会报错AttributeError
return self.name == other.name and self.age == other.age
p1 = Person("张三", 20)
print(p1 == 20) # 报错:AttributeError: 'int' object has no attribute 'name'因为你可能会不小心拿 Person 实例和整数、字符串比,这时候访问other.name肯定报错。
正确的__eq__要先通过isinstance(other, 类名)判断对方是不是同一个类的实例,不是就直接返回 False;是再比属性。
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
# 正确的__eq__方法
def __eq__(self, other):
# 第一步:判断other是不是Person实例,不是就返回False
if not isinstance(other, Person):
return False
# 第二步:按自己的规则比属性(这里按name和age都相等算相等)
return self.name == other.name and self.age == other.age
# 测试一下
p1 = Person("张三", 20)
p2 = Person("张三", 20)
p3 = Person("李四", 20)
p4 = 20 # 非Person实例
print(p1 == p2) # True(名字和年龄都一样)
print(p1 == p3) # False(名字不一样)
print(p1 == p4) # False(p4不是Person实例)__ne__对应 “!=” 运算符,默认情况下,如果你没写__ne__,Python 会自动根据__eq__的结果 “取反”。比如:
p1 == p2是 True,那p1 != p2就是 False;p1 == p2是 False,那p1 != p2就是 True。除非你有特殊需求(比如 “!=” 的逻辑和 “==” 取反不一样),否则不用单独写__ne__。
接下来实现更常用的 “大小比较”,比如按年龄比谁大谁小。咱们先手动写__lt__(<)和__gt__(>),再讲怎么用装饰器偷懒。
还是以 Person 类为例,规则:先按年龄比,年龄相同再按名字的字母顺序比(字符串比较默认按 ASCII 码,中文也能用)。
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
# 先实现相等判断(前面讲过的)
def __eq__(self, other):
if not isinstance(other, Person):
return False
return self.name == other.name and self.age == other.age
# 实现“小于”(<):按年龄→名字的顺序比
def __lt__(self, other):
# 第一步:先判断类型,不是Person实例就抛错(符合Python习惯)
if not isinstance(other, Person):
raise TypeError("不能拿Person实例和非Person实例比较!")
# 第二步:比较逻辑
if self.age != other.age:
# 年龄不一样,直接比年龄
return self.age < other.age
else:
# 年龄一样,比名字(字符串默认按顺序比较)
return self.name < other.name
# 实现“大于”(>):其实可以不写,Python会推导,但手动写更直观
def __gt__(self, other):
if not isinstance(other, Person):
raise TypeError("不能拿Person实例和非Person实例比较!")
if self.age != other.age:
return self.age > other.age
else:
return self.name > other.name
# 测试大小比较
p1 = Person("张三", 20)
p2 = Person("李四", 22)
p3 = Person("王五", 20) # 年龄和p1一样,名字比p1靠后
print(p1 < p2) # True(20 < 22)
print(p1 > p2) # False(20 > 22不成立)
print(p1 < p3) # True(年龄相同,"张三" < "王五")
print(p1 > p3) # False("张三" > "王五"不成立)刚才只写了__eq__、__lt__、__gt__,如果还要支持 <=、>=,难道还要再写__le__和__ge__?太麻烦了!
这时候functools.total_ordering装饰器就派上用场了。只要满足两个条件:
__eq__方法;__lt__、__gt__、__le__、__ge__中的一个);装饰器会自动帮你实现剩下的 4 个比较方法!
看例子:
# 第一步:导入total_ordering
from functools import total_ordering
# 第二步:给类加装饰器
@total_ordering
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
# 必须定义:__eq__方法
def __eq__(self, other):
if not isinstance(other, Person):
return False
return self.name == other.name and self.age == other.age
# 只需要定义一个比较方法(这里选__lt__)
def __lt__(self, other):
if not isinstance(other, Person):
raise TypeError("不能拿Person实例和非Person实例比较!")
if self.age != other.age:
return self.age < other.age
else:
return self.name < other.name
# 测试所有比较运算符(包括装饰器自动实现的<=、>=)
p1 = Person("张三", 20)
p2 = Person("李四", 22)
p3 = Person("王五", 20)
print(p1 <= p2) # True(20 <= 22)
print(p1 >= p3) # True(年龄相同,"张三" >= "王五"不成立?不对,再看)
# 哦,p1.name是"张三",p3.name是"王五","张三" < "王五",所以p1 >= p3是False?
print(p1 >= p3) # False(正确,因为p1 < p3是True,所以p1 >= p3是False)
print(p2 >= p1) # True(22 >= 20)看到没?只写了__eq__和__lt__,但 <=、>=、> 这些运算符都能用了!装饰器帮我们省了大量代码。
有了比较方法,咱们就能直接对 “自定义实例列表” 排序了 —— 不管是用list.sort()还是sorted(),都能直接用,不用额外传key参数!
from functools import total_ordering
@total_ordering
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __eq__(self, other):
if not isinstance(other, Person):
return False
return self.name == other.name and self.age == other.age
def __lt__(self, other):
if not isinstance(other, Person):
raise TypeError("不能拿Person实例和非Person实例比较!")
if self.age != other.age:
return self.age < other.age
else:
return self.name < other.name
# 加个__str__方法,打印实例时更直观
def __str__(self):
return f"Person(name='{self.name}', age={self.age})"
# 创建实例列表
people = [
Person("李四", 22),
Person("王五", 20),
Person("张三", 20),
Person("赵六", 25)
]
# 1. 用list.sort()排序(原地排序)
people.sort()
print("排序后(按年龄→名字):")
for p in people:
print(p) # 输出:张三(20)、王五(20)、李四(22)、赵六(25)
# 2. 用sorted()排序(返回新列表,不改变原列表)
people2 = [Person("孙七", 18), Person("周八", 20)]
sorted_people2 = sorted(people2)
print("nsorted()排序后:")
for p in sorted_people2:
print(p) # 输出:孙七(18)、周八(20)想按相反顺序排(比如年龄从大到小),不用改比较方法,直接加reverse=True就行:
people = [
Person("李四", 22),
Person("王五", 20),
Person("张三", 20),
Person("赵六", 25)
]
# 降序排序
people.sort(reverse=True)
print("降序排序后(按年龄→名字):")
for p in people:
print(p) # 输出:赵六(25)、李四(22)、王五(20)、张三(20)这部分全是干货!整理了大家最常踩的坑,每个坑都给错误代码、报错原因、正确代码。
错误代码:
class Person:
def __init__(self, age):
self.age = age
def __lt__(self, other):
# 没判断other类型,万一other不是Person实例呢?
return self.age < other.age
p1 = Person(20)
print(p1 < 18) # 报错:AttributeError: 'int' object has no attribute 'age'报错原因:拿 Person 实例和整数比,other.age会找整数的age属性,整数没有这个属性,所以报错。
正确代码:
class Person:
def __init__(self, age):
self.age = age
def __lt__(self, other):
# 先判断类型,不是Person实例就抛错或返回False
if not isinstance(other, Person):
raise TypeError("只能Person实例之间比较!")
return self.age < other.age
p1 = Person(20)
print(p1 < Person(18)) # True
# print(p1 < 18) # 会抛错:TypeError: 只能Person实例之间比较!错误代码:
class Person:
def __init__(self, age):
self.age = age
def __lt__(self, other):
# 错误:返回的是整数(self.age - other.age),不是布尔值
return self.age - other.age
p1 = Person(20)
p2 = Person(18)
print(p1 < p2) # 输出2(非0,Python里非0就是True),但实际20 < 18是False!报错原因:比较方法必须返回布尔值(True/False)。如果返回整数,Python 会把 “非 0 值” 当成 True,“0” 当成 False,导致逻辑完全错了。
正确代码:
class Person:
def __init__(self, age):
self.age = age
def __lt__(self, other):
if not isinstance(other, Person):
raise TypeError("只能Person实例之间比较!")
# 正确:返回布尔值
return self.age < other.age
p1 = Person(20)
p2 = Person(18)
print(p1 < p2) # False(正确)错误代码:
from functools import total_ordering
@total_ordering
class Person:
def __init__(self, age):
self.age = age
# 只定义了__lt__,没定义__eq__
def __lt__(self, other):
return self.age < other.age
p1 = Person(20)
p2 = Person(20)
print(p1 == p2) # 输出False?但两个实例年龄一样啊!报错原因:total_ordering依赖__eq__来定义 “相等”。如果没写__eq__,Python 会用默认的__eq__(比较实例的 “身份”,也就是内存地址,只有同一个实例才相等)。所以 p1 和 p2 是两个不同的实例,即使年龄一样,p1 == p2也会返回 False。
正确代码:
from functools import total_ordering
@total_ordering
class Person:
def __init__(self, age):
self.age = age
# 必须定义__eq__
def __eq__(self, other):
if not isinstance(other, Person):
return False
return self.age == other.age
def __lt__(self, other):
if not isinstance(other, Person):
raise TypeError("只能Person实例之间比较!")
return self.age < other.age
p1 = Person(20)
p2 = Person(20)
print(p1 == p2) # True(正确)错误代码:
class Person:
def __init__(self, age):
self.age = age
def __eq__(self, other):
return self.age == other.age
def __lt__(self, other):
# 矛盾逻辑:年龄大的反而“小于”年龄小的
return self.age > other.age
def __gt__(self, other):
# 又按正常逻辑:年龄大的“大于”年龄小的
return self.age > other.age
p1 = Person(20)
p2 = Person(18)
print(p1 < p2) # True(20>18,按__lt__逻辑返回True)
print(p1 > p2) # True(20>18,按__gt__逻辑返回True)
# 结果p1既小于p2,又大于p2,完全矛盾!报错原因:__lt__和__gt__的逻辑不一致,导致比较结果矛盾,排序时会出问题。
正确代码:
class Person:
def __init__(self, age):
self.age = age
def __eq__(self, other):
return self.age == other.age
def __lt__(self, other):
return self.age < other.age
# 要么不写__gt__(让Python推导),要么和__lt__逻辑一致
def __gt__(self, other):
return self.age > other.age
p1 = Person(20)
p2 = Person(18)
print(p1 < p2) # False
print(p1 > p2) # True(逻辑一致)自定义类比较是 Python 面试的高频考点,这里整理了 5 个常问题,帮你提前准备。
大白话回答:
因为 Python 不知道你想 “按什么规则” 比较啊!比如 Person 类,是按年龄比?还是按名字比?默认情况下,自定义类没有这些比较逻辑,所以一用比较运算符就报错。得咱们自己写__eq__、__lt__这些特殊方法,告诉 Python 怎么比。
__eq__方法和==运算符是什么关系?和is有什么区别?大白话回答:
==运算符本质就是调用__eq__方法 —— 你写a == b,Python 会执行a.__eq__(b),返回什么就是什么。is是判断 “两个变量是不是指向同一个实例”(看内存地址),和__eq__没关系。比如:p1 = Person("张三", 20)
p2 = Person("张三", 20)这里p1 == p2是 True(如果__eq__按名字和年龄比),但p1 is p2是 False,因为 p1 和 p2 是两个不同的实例,内存地址不一样。
functools.total_ordering装饰器有什么用?用的时候要注意什么?大白话回答:
这个装饰器是帮咱们 “偷懒” 的!本来要写__eq__、__lt__、__gt__、__le__、__ge__5 个方法才能支持所有比较,用了这个装饰器,只要写__eq__,再随便写一个比较方法(比如__lt__),它就自动帮你实现剩下的 4 个方法,省代码。
注意两点:
__eq__方法,不然相等判断会错;__lt__/__gt__/__le__/__ge__),不然装饰器没用。__lt__方法?大白话回答:
直接在__lt__里写逻辑就行,先比年龄,年龄一样再比名字的长度。代码大概这样:
def __lt__(self, other):
if not isinstance(other, Person):
raise TypeError("只能Person实例之间比较!")
# 第一步:比年龄
if self.age != other.age:
return self.age < other.age
# 第二步:年龄相同,比名字长度
return len(self.name) < len(other.name)比如 p1("张三",20)和 p2("李四四",20),张三名字长度 2,李四四 3,所以 p1 < p2 是 True。
__lt__方法,为什么a > b也能生效?大白话回答:
因为 Python 有 “反向推导” 的逻辑。它知道 “a > b” 和 “b < a” 是一回事,所以如果你没写__gt__方法,当你用a > b时,Python 会自动调用b.__lt__(a),根据这个结果来判断。
比如你写p1 > p2,Python 会算p2.__lt__(p1),如果返回 True,那p1 > p2就是 True;如果返回 False,那p1 > p2就是 False。这样就不用咱们重复写__gt__了。
咱们这篇文章把自定义类比较的知识点全讲透了:
__eq__(相等)和__lt__(小于)等特殊方法;functools.total_ordering装饰器能少写很多代码;sort()或sorted()排序;total_ordering要配__eq__。掌握这些,你写的自定义类就能像 Python 内置类型(int、str)一样灵活,不管是比较还是排序,都不在话下!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。