Skip to content

08-魔术方法

Python 版本要求:Python 3.11+ 贯穿项目:oop_demo/ 代码位置oop_demo/app/domain/book/model.pyoop_demo/app/domain/member.pyoop_demo/app/services/library.py测试验证cd oop_demo && uv run pytest -k magic -v


概念铺垫

为什么需要魔术方法?

问题场景

你创建了 BookItem 类,希望它能:

  • print(book) 显示友好格式,而非 <app.domain.book.model.BookItem object at 0x...>
  • book1 == book2 按 ISBN 比较,而非按内存地址
  • len(library) 返回藏书数量
  • "9780000000001" in library 检查是否在馆
  • sorted(books) 按书名排序

不用魔术方法:

python
class BookItem:
    def display(self) -> str:
        status = "可借" if self._available else "已借出"
        return f"《{self.title}{self.author}{status})"

    def equals(self, other) -> bool:
        return self._isbn == other._isbn

b = BookItem("Python", "Guido", "9780000000001", 2020)
print(b.display())      # 必须记住方法名
print(b.equals(b2))     # 不能用 ==

用魔术方法:

python
class BookItem:
    def __str__(self) -> str:
        status = "可借" if self._available else "已借出"
        return f"《{self.title}{self.author}{status})"

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, BookItem):
            return NotImplemented
        return self._isbn == other._isbn

b = BookItem("Python", "Guido", "9780000000001", 2020)
print(b)        # 《Python》Guido(可借) ← 自然
print(b == b2)  # True ← 直觉

魔术方法的价值:让自定义类无缝融入 Python 的语法生态len()in==sorted() 等内置操作自动工作。


生活类比

魔术方法就像"魔法接口" — 给类安装了隐藏按钮。你定义了 __len__,Python 的 len() 就会自动按下这个按钮;你定义了 __eq__== 运算符就会触发。这些按钮对使用者透明,他们不需要知道方法名,只需使用 Python 的内置操作。


L1 理解层:会用

核心原理

str vs repr

python
# oop_demo/app/domain/book/model.py
class BookItem:
    def __str__(self) -> str:
        status = "可借" if self._available else "已借出"
        return f"《{self.title}{self.author}{status})"

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}(title={self.title!r}, isbn={self._isbn!r})"
维度__str____repr__
目标读者终端用户开发者/调试
触发方式print(obj)str(obj)repr(obj)obj 在 REPL
设计目标易读准确,理想情况下 eval(repr(obj)) 可重建
回退未定义时用 __repr__始终有默认 <类名 object at 0x...>

最佳实践:至少定义 __repr__。如果只选一个,__repr__ 更重要。

eq + hash

python
# oop_demo/app/domain/book/model.py
class BookItem:
    def __eq__(self, other: object) -> bool:
        if not isinstance(other, BookItem):
            return NotImplemented
        return self._isbn == other._isbn

    def __hash__(self) -> int:
        return hash(self._isbn)

关键规则:

规则说明
相等对象必须有相同哈希a == bhash(a) == hash(b)
定义了 __eq__ 不定义 __hash__ → 自动不可哈希Python 3 的安全设计
__hash__ 的值必须不变可变对象不应定义 __hash__
返回 NotImplemented 而非 False让 Python 尝试反向比较

demo_ch08 验证:

python
b1 = BookItem("Python", "Guido", "9780000000001", 2020)
b2 = BookItem("Python 入门", "不同作者", "9780000000001", 2021)
b1 == b2               # True(同 ISBN)
hash(b1) == hash(b2)   # True
{b1, b2}               # 长度 1(集合去重)

lt 排序

python
# oop_demo/app/domain/book/model.py
class BookItem:
    def __lt__(self, other: BookItem) -> bool:
        return self.title < other.title

只定义 __lt__sorted()list.sort() 就能工作。如果需要完整的比较运算符(<=>>=),配合 @functools.total_ordering 装饰器:

python
from functools import total_ordering

@total_ordering
class BookItem:
    def __eq__(self, other): ...
    def __lt__(self, other): ...
    # 自动获得 __le__、__gt__、__ge__

容器协议(len / iter / contains

python
# oop_demo/app/services/library.py
class Library:
    def __len__(self) -> int:
        return len(self._books)

    def __iter__(self):
        return iter(sorted(self._books.values()))

    def __contains__(self, isbn: str) -> bool:
        return isbn in self._books
魔术方法触发操作demo_ch08 效果
__len__len(lib)返回藏书数量
__iter__for book in lib按书名排序迭代
__contains__"isbn" in libO(1) 查找

定义这三个方法后,Library 就能像 Python 内置容器一样使用。


贯穿实战

运行 demo_ch08 查看完整演示:

bash
cd oop_demo && uv run python -m app
# 选择 8. 魔术方法

或用测试验证:

bash
cd oop_demo && uv run pytest -k magic -v

测试覆盖:

  • test_str_shows_availability__str__ 显示可借状态
  • test_repr_contains_class_name__repr__ 包含类名
  • test_eq_by_isbn / test_ne_different_isbn__eq__ 按 ISBN 比较
  • test_hash_same_isbn__hash__ 一致性
  • test_lt_sorts_by_title__lt__ 排序
  • test_library_len / test_library_iter_sorted / test_library_contains — 容器协议
  • test_member_str_and_repr — Member 的 __str__ / __repr__
  • test_member_eq_by_member_id / test_member_hash — Member 的 __eq__ / __hash__

常用魔术方法速查

类别方法触发操作
对象表示__str__print(obj)str(obj)
__repr__repr(obj)、REPL 显示
比较运算__eq__==
__ne__!=(默认由 __eq__ 推导)
__lt__<sorted()
__hash__hash(obj)、dict key、set
容器协议__len__len(obj)
__iter__for x in objiter(obj)
__contains__x in obj
__getitem__obj[key]
__setitem__obj[key] = value
类型转换__bool__bool(obj)if obj
可调用__call__obj()

L2 实践层:用好

推荐做法

做法原因示例
至少定义 __repr__调试必备,回退 __str__f"{cls}(field={val!r})"
__eq__ 返回 NotImplemented支持反向比较,类型安全if not isinstance(other, X): return NotImplemented
__eq__ + __hash__ 配对让对象可放入 set/dict基于不可变字段计算
@total_ordering 简化排序只需 __eq__ + __lt__sorted(books)
容器类实现 __len__ + __iter__ + __contains__融入 Python 生态len(lib)isbn in lib
魔术方法用语法触发,不直接调用代码可读性a == b 而非 a.__eq__(b)
可变对象不定义 __hash__哈希值改变后 dict/set 失效或用 __hash__ = None 显式禁止

反模式:不要这样做

python
# ❌ 只定义 __eq__ 不定义 __hash__ —— 对象变不可哈希
class Book:
    def __eq__(self, other):
        return self.isbn == other.isbn

b1 = Book("9780000000001")
b2 = Book("9780000000001")
print(b1 == b2)  # True
{b1, b2}         # TypeError: unhashable type

# ✅ 同时定义 __eq__ 和 __hash__
class Book:
    def __eq__(self, other):
        if not isinstance(other, Book):
            return NotImplemented
        return self.isbn == other.isbn
    def __hash__(self):
        return hash(self.isbn)
python
# ❌ __repr__ 返回不明确的字符串
def __repr__(self):
    return "Book object"  # 无用信息

# ✅ __repr__ 包含类名和关键字段
def __repr__(self):
    return f"BookItem(title={self.title!r}, isbn={self._isbn!r})"
python
# ❌ 可变对象定义 __hash__ — 哈希值改变后 set 找不到
class BadContainer:
    def __init__(self):
        self.items = []

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

    def __hash__(self):
        return hash(tuple(self.items))  # items 可变,哈希会变!

# ✅ 可变对象不定义 __hash__,或用 __hash__ = None
class GoodContainer:
    def __init__(self):
        self.items = []

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

    __hash__ = None  # 显式声明不可哈希
python
# ❌ 魔术方法绕过语法直接调用
print(book.__str__())     # 错误
print(book.__eq__(book2)) # 错误

# ✅ 使用语法触发
print(str(book))  # 或 print(book)
print(book == book2)

适用场景

场景推荐做法原因
需要打印友好输出__str__ + __repr__至少定义 __repr__
需要按值比较(而非身份)__eq__返回 NotImplemented 支持子类
需要放入 set / dict key__eq__ + __hash__基于不可变字段
需要排序__lt__ + @total_ordering实现最小接口
自定义容器__len__ + __iter__ + __contains__像内置容器一样使用
需要 with 语句支持__enter__ + __exit__上下文管理器
需要可调用对象__call__闭包的 OOP 替代

L3 专家层:深入

Python 如何实现:运算符重载与分发机制

二元运算符分发:__add__ vs __radd__

当 Python 执行 a + b 时:

a + b


┌─────────────────────────────────────────────────────┐
│  1. 尝试 type(a).__add__(a, b)                      │
│     ├── 返回 NotImplemented?                        │
│     │   └── 是 → 继续第 2 步                         │
│     └── 返回实际值 → 完成                             │
│                                                      │
│  2. 尝试 type(b).__radd__(b, a)  ← 反向(反射)操作  │
│     ├── 返回 NotImplemented?                         │
│     │   └── 是 → 继续第 3 步                         │
│     └── 返回实际值 → 完成                             │
│                                                      │
│  3. 抛出 TypeError: unsupported operand type(s)      │
└─────────────────────────────────────────────────────┘
python
# 验证:__add__ 和 __radd__ 的调用顺序
class Left:
    def __add__(self, other):
        print("Left.__add__ called")
        return NotImplemented  # 返回 NotImplemented 触发反向

class Right:
    def __radd__(self, other):
        print("Right.__radd__ called")
        return "result from Right"

l = Left()
r = Right()
print(l + r)
# Left.__add__ called
# Right.__radd__ called
# result from Right
python
# 验证:实际项目中 NotImplemented 的正确用法
class Vector:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __add__(self, other):
        if isinstance(other, Vector):
            return Vector(self.x + other.x, self.y + other.y)
        return NotImplemented  # 不是 NotImplementedError!

    def __radd__(self, other):
        # 当 other 不知道如何处理 Vector 时被调用
        if isinstance(other, (int, float)):
            return Vector(self.x + other, self.y + other)
        return NotImplemented

__getattr__ vs __getattribute__

方法调用时机优先级用途
__getattribute__每次属性访问都调用最高(拦截一切)极少使用,极难正确实现
__getattr__仅在常规查找失败时调用最低(兜底)动态属性、代理、懒加载
obj.attr 查找流程:


┌──────────────────┐
│ __getattribute__ │  ← 始终调用(如果定义了)
│ (自定义或默认)    │     自定义时必须调用 super().__getattribute__
└──────┬───────────┘


  ┌──────────┐
  │ 描述符    │  ← type(obj).__mro__ 查找
  │ __dict__  │
  │ MRO 链    │
  └──────┬───┘
         │ 没找到

  ┌──────────┐
  │__getattr__│  ← 仅当所有常规查找都失败时
  └──────────┘
python
# 验证:__getattr__ vs __getattribute__
class Demo:
    def __init__(self):
        self.x = 1

    def __getattr__(self, name):
        print(f"__getattr__ called for {name}")
        return f"dynamic {name}"

    def __getattribute__(self, name):
        print(f"__getattribute__ called for {name}")
        return super().__getattribute__(name)

d = Demo()
print(d.x)       # __getattribute__ called → 返回 1
print(d.y)       # __getattribute__ called (失败) → __getattr__ called → dynamic y
python
# 警告:__getattribute__ 极易导致无限递归
class Broken:
    def __getattribute__(self, name):
        return self.__dict__[name]  # 无限递归!
        # self.__dict__ 自身触发 __getattribute__

class Fixed:
    def __getattribute__(self, name):
        return super().__getattribute__(name)  # 正确

性能考量

操作时间复杂度说明
__eq__O(字段数)逐个字段比较
__hash__O(1)基于预计算哈希
__str__ / __repr__O(字段数)拼接字符串
__len__O(1) 或 O(n)取决于内部数据结构
__contains__O(1) 最佳取决于内部数据结构
__lt__ + sorted()O(n log n)排序算法自身
__getattribute__ 自定义每次属性访问额外函数调用有性能影响,谨慎使用

知识关联

魔术方法知识关联:
              ┌───────────────┐
              │  第 1 章:类   │
              │  基本定义     │
              └───────┬───────┘


              ┌───────────────┐
              │  第 8 章:魔术方法│
              └───────┬───────┘

   ┌──────────────────┼──────────────────┐
   ↓                  ↓                  ↓
┌─────────┐    ┌─────────────┐    ┌─────────────┐
│ 字符串  │    │  比较/哈希   │    │  容器协议   │
│ __str__ │    │  __eq__     │    │  __len__    │
│ __repr__│    │  __hash__   │    │  __iter__   │
│         │    │  __lt__     │    │  __contains__│
└────┬────┘    └──────┬──────┘    └──────┬──────┘
     │               │                   │
     ↓               ↓                   ↓
  调试输出        set/dict key        for/in/len
  日志记录        排序                迭代遍历
     │               │                   │
     ↓               ↓                   ↓
┌─────────┐    ┌─────────────┐    ┌─────────────┐
│__getattr│    │ __add__     │    │ __call__    │
│__getattr│    │ __radd__    │    │ 可调用对象  │
│ibute__  │    │ 运算符重载  │    │             │
└─────────┘    └─────────────┘    └─────────────┘

自检清单

  1. __str____repr__ 有什么区别?

    • __str__ 面向用户(易读),__repr__ 面向开发者(准确);未定义 __str__ 时用 __repr__ 回退
  2. 为什么 __eq__ 要返回 NotImplemented 而不是 False

    • NotImplemented 让 Python 尝试反向比较(other.__eq__(self)),支持不同类型之间的比较
  3. 定义了 __eq__ 后为什么对象变成不可哈希的?

    • Python 3 的安全设计:相等的对象必须有相同哈希,自定义 __eq__ 后默认哈希不再保证这一点
  4. 容器协议最少需要几个魔术方法?

    • 三个:__len__(长度)、__iter__(迭代)、__contains__(成员检查,可选,未定义时退化为遍历)
  5. 魔术方法定义在实例上会被调用吗?

    • 不会。Python 查找魔术方法时跳过实例 __dict__,直接从类字典开始查找

本章能力清单

学完本章后,你可以:

  • [ ] 用 __str__ / __repr__ 让对象打印友好
  • [ ] 用 __eq__ + __hash__ 实现基于字段的相等比较和哈希
  • [ ] 用 __lt__ + @total_ordering 支持对象排序
  • [ ] 用 __len__ / __iter__ / __contains__ 让自定义类像内置容器一样工作
  • [ ] 理解魔术方法的调用机制和查找规则