| Day 17 | 魔法方法:__str__ / __eq__ / __lt__ 与运算符重载 |

1 魔法方法是什么 —— 名字带「魔法」,背后是约定

是什么?为什么学?

魔法方法(也叫「特殊方法」)是名字前后各两个下划线的方法,如 __init____str____eq__。它们不用你手动调用,Python 在特定场景下自动触发print(obj) 自动找 __str__obj1 == obj2 自动找 __eq__len(obj) 自动找 __len__。学会魔法方法,你的对象就能支持 ==<len()+in 这些「内置待遇」——代码写起来和用 Python 内置类型一样自然。

底层原理

魔法方法本质是约定:解释器在实现 str()len()==+in 等内置操作时,内部都会去查对象的类里有没有对应的 __xxx__ 方法,有就调用,没有就报错或走默认行为——比如 len(obj) 的实现大致是 obj.__len__()。这也是为什么「一切皆对象」:语法层面的事件,底层都是方法调用

生活类比

魔法方法像酒店房间的门牌约定:门上写着「请勿打扰」(约定),服务员路过就自动不敲门;门上写着「请打扫」,服务员路过就自动进来。你不需要喊住每个服务员交代一遍——挂对牌子,服务自动发生。双下划线就是那块牌子。

注意事项总结

1 六个常见魔法方法,看看它们分别在什么场景被自动调用

class Demo:
    def __init__(self, value):
        self.value = value          # 创建对象时自动调用

    def __str__(self):
        return f"Demo(值={self.value})"   # print/str 时自动调用

    def __eq__(self, other):
        return self.value == other.value  # == 时自动调用

    def __len__(self):
        return self.value                 # len() 时自动调用

    def __contains__(self, item):
        return item in str(self.value)    # in 时自动调用

d1 = Demo(10)
d2 = Demo(10)
print(d1)                # 触发 __str__
print(d1 == d2)          # 触发 __eq__
print(len(d1))           # 触发 __len__
print("1" in d1)         # 触发 __contains__

运行结果:

Demo(值=10)
True
10
True

整个程序里没有一行手动调用魔法方法,全是 Python 自动触发。这就是魔法方法的全部秘密:你只管按约定定义,Python 负责在合适时机调用

2 没定义对应魔法方法时,Python 会怎样

class Empty:
    pass

e = Empty()
# print(len(e))    # 会报错:TypeError: object of type 'Empty' has no len()

len(e) 去找 __len__,没找到,于是抛出 TypeError——内置操作只会走魔法方法这一条路。看到这类报错,第一反应就是去补对应的魔法方法。

易错点

  • 错误写法:把魔法方法名拼错(__str_ 少一个下划线) → 问题:不报错!但永远不会被自动调用,行为「静默失效」 → 正确写法:前后各两个下划线,写完数一遍。
  • 错误写法:以为魔法方法要手动调用:obj.__str__() → 问题:能用但不规范,失去了「自动触发」的意义 → 正确写法:用 str(obj)print(obj),让 Python 自动找。
  • 错误写法:定义魔法方法却不 return 正确类型 → 问题:报 TypeError: __str__ returned non-string 等 → 正确写法:每种魔法方法有约定返回类型:__str__ 返回字符串、__eq__ 返回布尔……

记忆口诀:双下划线是约定,Python 自动来触发;魔法方法不用调,内置操作走这条路。

2 strrepr —— 「给人看」与「给调试看」

是什么?为什么学?

__str__ 面向最终用户print()str() 调用,追求友好可读;__repr__ 面向开发者:交互式终端直接输对象名时显示,追求「明确、无歧义」,最好能看出这个对象是怎么构造出来的。就像用户说明书 vs 维修工单:同一件产品配两份文档,读者不同,信息量完全不同。

底层原理

两者的回退规则是:str(obj) 优先找 __str__找不到就退而用 __repr__;而 repr(obj)(以及交互式终端显示)只用 __repr__,找不到就用 object 基类的默认地址格式。另外记住:容器打印元素时用的是 repr——print([obj]) 里列表元素走 __repr__ 而不是 __str__

注意事项总结

1 str 与 repr 的分工

class Book:
    def __init__(self, title, price):
        self.title = title
        self.price = price

    def __str__(self):
        return f"《{self.title}》{self.price} 元"

    def __repr__(self):
        return f"Book(title={self.title!r}, price={self.price!r})"

b = Book("Python 入门", 39.9)
print(str(b))       # 走 __str__:友好
print(repr(b))      # 走 __repr__:带构造信息
print([b])          # 容器里的元素走 __repr__!

运行结果:

《Python 入门》39.9 元
Book(title='Python 入门', price=39.9)
[Book(title='Python 入门', price=39.9)]

print(str(b)) 输出书名加价格,是给人看的;repr(b) 输出「用这个表达式能再造出这个对象」的风格——!r 表示「用 repr 方式格式化字符串」(带引号);print([b]) 是关键细节:**列表打印元素时调用的是元素的 __repr__**。

2 回退规则:如果类只定义 __repr__ 而没定义 __str__print(obj) 会自动回退调用 __repr__,两种输出相同;两个都没定义时才显示内存地址。

易错点

  • 错误写法:只定义 __str__ 不定义 __repr__,在终端直接输对象名 → 问题:显示难看的内存地址(终端只用 repr) → 正确写法:调试类至少定义 __repr__
  • 错误写法:以为 print([obj])__str__ → 问题:容器元素走 __repr__,输出和预期不一样 → 正确写法:想让列表里的对象也好看,定义 __repr__
  • 错误写法:__repr__ 里用 !r 却不知道含义 → 问题:输出不带引号,看不出是字符串 → 正确写法:!r 表示按 repr 格式化(字符串会带引号)。

记忆口诀:str 给用户,repr 给调试;str 缺了找 repr,容器打印用 repr。

3 eq 与哈希 —— 相等的定义影响 set / dict

是什么?为什么学?

不定义 __eq__ 时,== 比较的是内存地址(本质是 is),两个内容相同的对象也不相等。定义 __eq__ 后,你可以规定「什么算相等」——学生分数相同算相等、坐标相同算相等。但这里藏着一个大坑:定义 __eq__ 会让对象默认变得「不可哈希」(unhashable),放进 set 或当 dict 的键会报错。

底层原理

Python 里 ==__eq__hash()__hash__。规则是:两个相等的对象,哈希值必须相同(否则 set/dict 的去重和查找就乱套了)。object 基类默认用 id 做哈希(与默认的 == 语义一致)。一旦你自定义了 __eq__,Python 无法保证你定义的相等关系和默认哈希还兼容,于是自动把 __hash__ 设为 None——对象不可哈希。想要 set 正常工作,必须同时重写 __hash__

生活类比

默认的 == 只认「同一个人」(id),定义了 __eq__ 后「同名就算同一人」;但 set 查重靠的是哈希值 = 档案柜编号,你改了相等规则却没改编号规则,档案柜就乱套了——所以 Python 干脆禁止你把人放进档案柜。

注意事项总结

1 默认 == 比较内存地址

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

p1 = Point(1, 2)
p2 = Point(1, 2)
print(p1 == p2)      # 内容一样,但默认比较地址
print(p1 is p2)      # 地址当然不同

运行结果:

False
False

没定义 __eq__== 退化成 is(比较内存地址)。

2 定义 eq 后,内容相等即相等

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __eq__(self, other):
        if not isinstance(other, Point):
            return False
        return self.x == other.x and self.y == other.y

p1 = Point(1, 2)
p2 = Point(1, 2)
print(p1 == p2)     # 坐标相同,True
print(p1 == "Point")  # 类型不同,返回 False

运行结果:

True
False

__eq__ 里先判断类型(不是 Point 直接返回 False,避免和字符串等比较时抛异常),再比较坐标。

3 **定义 eq 后 set 会拒绝——必须同时定义 hash**:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __eq__(self, other):
        if not isinstance(other, Point):
            return False
        return self.x == other.x and self.y == other.y

    def __hash__(self):
        return hash((self.x, self.y))   # 用坐标元组的哈希

p1 = Point(1, 2)
p2 = Point(1, 2)
print(len({p1, p2}))        # 内容相同,set 只保留一个
print(hash(p1) == hash(p2)) # 相等的对象哈希必须相同

运行结果:

1
True

同时定义 __eq____hash__ 后 set 就能正常工作了。**铁律:定义 __eq__ 就一定要定义 __hash__**。

易错点

  • 错误写法:只定义 __eq__ 就把对象放 set → 问题:报 TypeError: unhashable type: 'Point' → 正确写法:同时定义 __hash__,或换个数据结构。
  • 错误写法:__eq__ 里用 self.x == other.x 却不检查类型 → 问题:和别的类型比较时抛 AttributeError → 正确写法:先 isinstance(other, Point) 再比属性。
  • 错误写法:__eq__ 返回非布尔(如返回数字、None) → 问题:行为怪异(if 判断时按真值解释) → 正确写法:__eq__ 明确返回 True / False。
  • 错误写法:两个相等对象哈希不同 → 问题:set/dict 行为错乱:去重失效、查找不到 → 正确写法:保证相等 ⇒ 哈希相等(一起定义)。

记忆口诀:eq 定相等,hash 定档案;两个都定义,set 才不乱。

4 lt 与排序 —— 只写一个方法,sorted 全自动

是什么?为什么学?

sorted() 排序对象列表时,需要知道「谁在前谁在后」——它就是靠 < 比较(调用 __lt__)做到的。所以只定义 __lt__ 一个方法,你的类就能被 sorted()min()max() 使用

底层原理

sorted() 内部用 Timsort 算法对元素两两比较,比较时执行的是 a < b,即 a.__lt__(b)。所以对象要能排序,**最小要求是定义 __lt__**。Python 3 里 ><=>= 各有独立的方法(__gt____le____ge__),缺了就用不了——@total_ordering 装饰器会依据你定义的 __eq____lt__,自动推导补齐其余四个比较方法。

生活类比

__lt__排队规则:老师只说了一条规则「按个子矮的在前」,全班同学就能自动排好队;total_ordering校规自动补全:知道了「矮在前」,「高在后」(>)、「不比他矮」(>=)这些说法自动推导出来,不用每条都写。

注意事项总结

1 只定义 __lt__,sorted / min / max 全能用

class Student:
    def __init__(self, name, score):
        self.name = name
        self.score = score

    def __lt__(self, other):
        return self.score < other.score     # 按分数升序

    def __repr__(self):
        return f"{self.name}({self.score}分)"

students = [
    Student("李四", 78),
    Student("张三", 90),
    Student("王五", 85),
]
print(sorted(students))    # 排序:两两比较 <
print(min(students))       # 最小值:一路比较 <
print(max(students))       # 最大值:一路比较 <

运行结果:

[李四(78分), 王五(85分), 张三(90分)]
李四(78分)
张三(90分)

一个方法,三种能力。

2 total_ordering 补齐全部比较运算

from functools import total_ordering

@total_ordering
class Student:
    def __init__(self, name, score):
        self.name = name
        self.score = score

    def __eq__(self, other):
        return self.score == other.score

    def __lt__(self, other):
        return self.score < other.score

    def __repr__(self):
        return f"{self.name}({self.score}分)"

a = Student("张三", 90)
b = Student("李四", 78)
print(a > b)     # 自动推导:90 > 78
print(a >= b)    # 自动推导:90 >= 78

运行结果:

True
True

只写了 __eq____lt__,但 >>= 也能用了。注意它要求你必须提供 __eq__ 和至少一个排序方法,否则会报错。

易错点

  • 错误写法:只定义 __lt__ 就用 > 比较 → 问题:报 TypeError: '>' not supported between instances → 正确写法:用 @total_ordering 补齐,或定义 __gt__
  • 错误写法:__lt__ 里不 return 布尔 → 问题:排序结果错乱 → 正确写法:__lt__ 必须返回 True/False。
  • 错误写法:用 @total_ordering 却没定义 __eq__ → 问题:报错:total_ordering 要求至少一个排序方法 + __eq__ → 正确写法:两个都定义再装饰。
  • 错误写法:忘记 from functools import total_ordering → 问题:报 NameError → 正确写法:先导入再使用。

记忆口诀:lt 一个就能排,sorted min max 全通吃;想要全比较,total_ordering 帮你补。

5 add / len / contains —— 「返回新对象」的约定

是什么?为什么学?

__add__ 让对象支持 +__len__ 让对象支持 len()__contains__ 让对象支持 in。这三个魔法方法让自定义容器(购物车、书架、队伍)用起来和 list/dict 一样顺手。特别要注意 __add__ 的约定:返回一个新对象,而不是修改自己

底层原理

a + b 会调用 a.__add__(b)len(x) 调用 x.__len__()item in x 调用 x.__contains__(item)(没有 __contains__ 时,Python 会退而用 __iter__ 逐个遍历——Day 18 讲迭代器)。__add__ 返回新对象是不可变语义:加法在数学上不改变操作数,Python 内置类型(int、str、tuple)都遵守这点,自定义类也应遵循,否则 a + b 之后 a 悄悄变了,调用方会一脸懵。

生活类比

__add__把两盒积木倒进一个新盒子:原来的两盒原封不动(不修改自身),新盒子装的是合起来的那份。如果倒完旧盒子里的积木没了,你就没法「再来一次」了。

注意事项总结

1 购物车三件套——+ 合并、len 长度、in 成员

class Cart:
    def __init__(self, items=None):
        self.items = items if items else []

    def __add__(self, other):
        return Cart(self.items + other.items)   # 新对象

    def __len__(self):
        return len(self.items)

    def __contains__(self, item):
        return item in self.items

    def __repr__(self):
        return f"Cart({self.items})"

cart_a = Cart(["苹果", "牛奶"])
cart_b = Cart(["面包"])
cart_c = cart_a + cart_b     # 触发 __add__
print(cart_c)                # 合并结果
print(cart_a)                # 原车没变
print(cart_a is cart_c)      # 确实是两个不同的对象
print(len(cart_c))           # 触发 __len__:3
print("牛奶" in cart_c)      # 触发 __contains__:True
print(len(Cart()))           # 空购物车长度为 0

运行结果:

Cart(['苹果', '牛奶', '面包'])
Cart(['苹果', '牛奶'])
False
3
True
0

cart_a + cart_b 触发 cart_a.__add__(cart_b),用 self.items + other.items 拼出新的列表、包进新的 Cart 返回——cart_a 没被改动;len(cart)__len__"牛奶" in cart__contains__。注意 Cart() 没传参数时 items 默认是 []items if items else [] 处理了「传 None」的情况)——空容器也应该是合法容器

易错点

  • 错误写法:__add__ 里直接 self.items += other.items 然后 return self → 问题:原对象被修改,与内置 + 语义不符,a + b 后 a 变了 → 正确写法:新建对象:return Cart(self.items + other.items)
  • 错误写法:默认参数写成 def __init__(self, items=[]) → 问题:所有实例共享同一个列表,互相污染 → 正确写法:写 items=None,方法里 self.items = items if items else []
  • 错误写法:__contains__ 忘 return → 问题:返回 None,in 结果恒为 False(None 是假值) → 正确写法:必须 return 布尔值。
  • 错误写法:__len__ 返回负数或非整数 → 问题:报 ValueError / TypeError → 正确写法:返回 len(self.items) 这类非负整数。

记忆口诀:add 加出新对象,原物不动守约定;len 与 contains,容器三件套。

6 动手实践:增强版对象体验程序

做一个 Book 类,把今天学的魔法方法全部用上,再写一个演示函数:

# magic_books.py —— 增强版对象体验程序
class Book:
    def __init__(self, title, price):
        self.title = title
        self.price = price

    def __str__(self):
        return f"《{self.title}》{self.price} 元"

    def __repr__(self):
        return f"Book({self.title!r}, {self.price!r})"

    def __eq__(self, other):
        if not isinstance(other, Book):
            return False
        return self.title == other.title

    def __lt__(self, other):
        return self.price < other.price

    def __len__(self):
        return len(self.title)

    def __add__(self, other):
        return Book(f"{self.title} + {other.title}", self.price + other.price)

    def __contains__(self, keyword):
        return keyword in self.title


def demo():
    book1 = Book("Python 入门", 39.9)
    book2 = Book("Python 入门", 99.0)      # 不同版本不同价
    book3 = Book("Python 进阶", 59.9)
    print("--- __str__ / __repr__ ---")
    print(book1)
    print(repr(book1))
    print("--- __eq__ 按书名比较 ---")
    print(book1 == book2)     # 同名,True
    print(book1 == book3)     # 不同名,False
    print("--- __lt__ + sorted 按价格排序 ---")
    for b in sorted([book3, book1, book2]):
        print(b)
    print("--- __len__ 书名长度 ---")
    print(len(book1))
    print("--- __add__ 合并 ---")
    bundle = book1 + book3
    print(bundle)
    print("--- __contains__ ---")
    print("入门" in book1)
    print("爬虫" in book1)


if __name__ == "__main__":
    demo()

运行结果:

--- __str__ / __repr__ ---
《Python 入门》39.9 元
Book('Python 入门', 39.9)
--- __eq__ 按书名比较 ---
True
False
--- __lt__ + sorted 按价格排序 ---
《Python 入门》39.9 元
《Python 进阶》59.9 元
《Python 入门》99.0 元
--- __len__ 书名长度 ---
9
--- __add__ 合并 ---
《Python 入门 + Python 进阶》99.8 元
--- __contains__ ---
True
False

排序输出了三本书(39.9、59.9、99.0),按价格升序排列,说明 __lt__sorted() 正确调用。注意 len("Python 入门") 计算的是字符个数(含空格):Python 6 + 空格 1 + 入门 2 = 9。

升级挑战:给 Book__sub__(买书抵扣)和 __getitem__(用下标取书名中的字),再定义一个 BookShelf 类,用 __len__ 统计书架上的书,用 __getitem__ 按下标取书。

今日总结

魔法方法 = 双下划线 + 自动触发;内置操作(==+inlen()print)底层全是方法调用 __str__ 给用户、__repr__ 给调试;str 缺了退 __repr__,容器元素打印走 __repr__ 默认 == 比较内存地址;定义 __eq__ 后内容相同即相等,但必须同时定义 __hash__ 才能进 set/dict 只定义 __lt__ 就能被 sorted() / min() / max() 使用;@total_ordering 一键补齐全部比较 __add__ 返回新对象、不修改自己;__len__ 返回非负整数;__contains__ 返回布尔 可变默认参数的坑:def __init__(self, items=[]) 会让所有实例共享列表 增强版对象体验程序(Book 类)所有演示功能运行正常

报错原因修复
TypeError: 'Book' object is not callable把对象当函数调用(如 book()检查是否把 __call__ 相关用法搞混,或调用了不存在的函数
TypeError: '<' not supported between instances没定义 __lt__ 就用 <sorted() 比较对象在类里定义 __lt__
TypeError: 'Book' object does not support indexing对象不支持下标 []定义 __getitem__
RecursionError__str__print(self) 又触发 __str__,无限递归__str__ 里用 self.属性,不要再 print(self)
__add__ 返回值不对忘了返回新对象,或返回了别的类型确认返回同类新对象,不修改原对象
TypeError: unhashable type: 'Point'定义了 __eq__ 却没定义 __hash__,对象不能进 set/dict同时定义 __hash__,或避免使用 set/dict
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇