Skip to content

Python 魔术方法(Magic Methods / Dunder Methods)全面解析 ​

引言:魔术方法的本质 —— Python 协议的实现载体 ​

你有没有想过,为什么下面这些"看起来完全不像函数调用"的写法,却能执行出正确的结果?

python
len([1, 2, 3])          # 3
"a" + "b"               # "ab"
for x in [1, 2]: ...    # 能遍历
with open("a.txt") as f: ...

len()、+、for、with……这些语法是怎么"认识"你的自定义对象的?答案是——它们不认识你的对象,它们只认识"协议"(Protocol)。

什么是协议? 协议就是 Python 规定的一套"暗号":只要你的类实现了某个特殊名字的方法,Python 解释器就会在特定时机自动调用它,从而让你的对象"具备"某种能力。这些以双下划线 __ 开头、结尾的方法,就是魔术方法(也叫 Dunder Methods,Dunder 是 Double Underscore 的缩写)。

用生活化类比来理解:

  • 魔术方法是"插座接口":你家的电器(自定义类)只要插上"三孔插头"(实现 __len__),就能接上"供电系统"(len() 函数),而供电系统根本不需要知道你是冰箱还是电视;
  • 魔术方法是"遥控器按键":按下 + 号键,Python 就去调用你的 __add__ 方法,就像遥控器发红外信号、电视收到信号后执行对应的动作;
  • 魔术方法是"面试官的标准提问":面试官(Python 解释器)不问"你是谁",只问"你会不会这个技能(协议)",会就录用,不会就报错。

魔术方法的三大特征:

  1. 名字固定:以 __ 开头和结尾,名字是官方规定死的,不能乱起(如 __len__、__add__);
  2. 无需手动调用:由 Python 解释器在特定时机自动触发,你几乎从不直接写 obj.__len__();
  3. 绑定到内置语法/函数:每个魔术方法对应一个内置语法或函数,例如 len(obj) 内部就是调用 obj.__len__()。

下面这段代码揭示了"协议"的本质——内置函数只是魔术方法的"语法糖壳":

python
class Box:
    def __len__(self):
        return 42

b = Box()
print(len(b))        # 42     —— 等价于下一行
print(b.__len__())   # 42     —— 你通常不会直接这么写,但底层就是它

记住这句话:Python 里几乎所有的"内置行为",底层都是魔术方法。 理解了魔术方法,你就理解了 Python 的对象模型本身。接下来,我们按功能维度,把官方标准中的魔术方法一个不落地拆解清楚。


一、基础实例创建与销毁:__new__ / __init__ / __del__ ​

这三个方法掌控着一个对象的"生老病死",是理解 Python 对象模型的第一道门槛。

1.1 三者调用顺序(核心) ​

一句话概括分工:

  • __new__:负责"创建"对象(分配内存、返回实例),是"造房子";
  • __init__:负责"初始化"对象(给属性赋值),是"装修房子";
  • __del__:负责"销毁"对象前的清理,是"拆房子"。

调用顺序:实例化时,Python 先调 __new__ 创建实例,再调 __init__ 初始化;对象引用计数归零时(CPython 下)再调 __del__。

python
class Demo:
    def __new__(cls, *args, **kwargs):
        print("1. __new__  创建实例")
        return super().__new__(cls)     # 必须返回一个实例!
    def __init__(self):
        print("2. __init__ 初始化实例")
    def __del__(self):
        print("3. __del__  销毁实例")

obj = Demo()        # 输出:1. __new__ → 2. __init__
del obj             # 输出:3. __del__
import gc; gc.collect()  # 手动触发一次垃圾回收,确保 __del__ 被看到

1.2 __new__:真正的"构造函数" ​

很多人误以为 __init__ 是构造函数,其实真正的构造函数是 __new__。它有几个关键点:

  • 是隐式静态方法,第一个参数是 cls(类本身),返回值为实例对象;
  • 返回的实例会作为 self 传给 __init__;
  • 如果 __new__ 没有返回当前类的实例,__init__ 不会被调用。

__new__ 最常见的实战场景是单例模式——保证一个类全局只有一个实例:

python
class Singleton:
    _instance = None
    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance
    def __init__(self, name=None):
        self.name = name

a = Singleton("A")
b = Singleton("B")
print(a is b)            # True   —— 是同一个对象
print(a.name, b.name)    # B B    —— 第二次初始化覆盖了 name

1.3 __init__:最熟悉的"初始化函数" ​

__init__ 是使用频率最高的魔术方法,要点:

  • 第一个参数是 self,表示刚创建好的实例;
  • 必须返回 None,返回其他值会报 TypeError;
  • 它的职责只是"填属性",创建实例这件事已经由 __new__ 完成了。
python
class Student:
    def __init__(self, name, age):
        self.name = name
        self.age = age

s = Student("小明", 18)
print(s.name, s.age)     # 小明 18

1.4 __del__:请谨慎使用的"析构函数" ​

__del__ 在对象被垃圾回收时调用,但它有三大"坑",不建议用它做资源释放:

  1. 时机不可控:CPython 用引用计数,del obj 后通常立刻触发;但其他实现(如 PyPy)用别的 GC 策略,触发时机未知;
  2. 解释器退出时:程序结束时,部分对象(含模块全局变量)可能已先被销毁,__del__ 里引用的东西可能已经不存在;
  3. 循环引用:两个对象互相引用,引用计数永远不为零,__del__ 可能永远不被调用。

最佳实践:资源释放请用 __enter__/__exit__ 上下文管理器(见第七章),而不是 __del__。


二、字符串与字节表示:__str__ / __repr__ / __bytes__ / __format__ ​

2.1 __str__ 与 __repr__ 的区别(高频考点) ​

维度__repr____str__
触发方式repr(obj)、交互式命令行直接输入变量str(obj)、print(obj)、f-string
目标读者开发者(调试用)最终用户(展示用)
风格定位无歧义、可还原,最好能 eval 回来可读、友好
兜底逻辑无兜底,必须实现若未定义 __str__,回退到 __repr__

一个经典的"黄金法则":__repr__ 追求"能一眼看出是什么",__str__ 追求"看着舒服"。

python
class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y
    def __repr__(self):
        return f"Point({self.x}, {self.y})"     # 可还原、无歧义
    def __str__(self):
        return f"({self.x}, {self.y})"          # 简洁友好

p = Point(1, 2)
print(repr(p))     # Point(1, 2)
print(str(p))      # (1, 2)
print(p)           # (1, 2)   —— print 走 __str__
print(f"{p}")      # (1, 2)   —— f-string 也走 __str__
print(f"{p!r}")    # Point(1, 2)  —— !r 强制走 __repr__

如果只实现 __repr__ 不实现 __str__,print 会自动兜底用 __repr__ 的结果:

python
class OnlyRepr:
    def __repr__(self):
        return "只有 __repr__"

o = OnlyRepr()
print(o)     # 只有 __repr__  —— __str__ 未定义,自动回退到 __repr__

2.2 __bytes__:字节序列表示 ​

由 bytes(obj) 触发,用于把对象转成字节串(网络传输、落盘存储常用)。

python
class Temperature:
    def __init__(self, celsius):
        self.c = celsius
    def __bytes__(self):
        return f"{self.c}°C".encode("utf-8")

t = Temperature(100)
print(bytes(t))          # b'100\xc2\xb0C'   —— "°" 的 UTF-8 编码是 \xc2\xb0

2.3 __format__:格式化输出 ​

由 format(obj, spec) 和 f-string 的 :spec 触发,让你自定义格式化规格。

python
class Temperature:
    def __init__(self, celsius):
        self.c = celsius
    def __format__(self, spec):
        if spec == "f":                      # f 规格 = 转华氏度
            return f"{self.c * 9 / 5 + 32:.1f}°F"
        return f"{self.c}°C"

t = Temperature(100)
print(format(t))       # 100°C
print(f"{t}")          # 100°C
print(f"{t:f}")        # 212.0°F

补充:__fspath__(由 os.fspath() 触发,返回文件系统路径)也属于本类,配合 os 模块的路径函数使用,用于把自定义对象"伪装"成路径对象。


三、数值运算类魔术方法(算术 / 比较 / 一元运算符) ​

"完整覆盖"离不开这一大类——没有它们,你自定义的数字类型就无法参与 +、-、>、abs() 等运算。

3.1 算术运算符与"反射 / 原地"三兄弟 ​

对每个二元运算符,Python 都准备了三个版本:

类型前缀示例触发场景
普通无__add__a + b,a 在左边
反射r__radd__b + a,a 在右边且 b 不支持时
原地i__iadd__a += b

关键机制:返回值约定 NotImplemented——当 a + b 时,先调 a.__add__(b);若返回 NotImplemented(表示"我不会算"),Python 再去尝试 b.__radd__(a);两边都 NotImplemented 才抛 TypeError。

python
class Money:
    def __init__(self, amount):
        self.amount = amount
    def __add__(self, other):
        if isinstance(other, Money):
            return Money(self.amount + other.amount)
        return NotImplemented        # 告诉解释器"这个我不处理,换人试试"
    def __radd__(self, other):
        if isinstance(other, (int, float)):   # 支持 10 + Money(5)
            return Money(self.amount + other)
        return NotImplemented
    def __repr__(self):
        return f"Money({self.amount})"

print(Money(5) + Money(10))   # Money(15)
print(10 + Money(5))          # Money(15)   —— 左边是 int,先调 int.__add__ 失败,再调 Money.__radd__

常见的算术魔术方法名(add/sub/mul/matmul/truediv/floordiv/mod/divmod/pow/lshift/rshift/and/xor/or 均可套用三兄弟):

  • + → __add__ / __radd__ / __iadd__
  • - → __sub__ / __rsub__ / __isub__
  • * → __mul__ / __rmul__ / __imul__
  • @ → __matmul__(矩阵乘法)
  • / → __truediv__;// → __floordiv__;% → __mod__;** → __pow__
  • << / >> → __lshift__ / __rshift__
  • & / | / ^ → __and__ / __or__ / __xor__

3.2 一元运算符 ​

语法魔术方法
-x__neg__
+x__pos__
abs(x)__abs__
~x__invert__
python
class Vector2D:
    def __init__(self, x, y):
        self.x, self.y = x, y
    def __neg__(self):
        return Vector2D(-self.x, -self.y)
    def __abs__(self):
        return (self.x ** 2 + self.y ** 2) ** 0.5

v = Vector2D(3, 4)
print(abs(v))       # 5.0
print((-v).x)       # -3

3.3 比较运算符与 functools.total_ordering ​

六个比较方法:__lt__、__le__、__eq__、__ne__、__gt__、__ge__。

  • Python 3 中 __ne__ 会自动取 __eq__ 的反值,无需手写;
  • 只写 __eq__ 和 __lt__,用 @total_ordering 装饰器即可自动推导出其余四个。
python
from functools import total_ordering

@total_ordering
class Version:
    def __init__(self, major, minor):
        self.major, self.minor = major, minor
    def __eq__(self, other):
        return (self.major, self.minor) == (other.major, other.minor)
    def __lt__(self, other):
        return (self.major, self.minor) < (other.major, other.minor)

v1, v2 = Version(1, 2), Version(1, 3)
print(v1 < v2)     # True
print(v1 <= v2)    # True   —— 由 total_ordering 自动推导
print(v1 >= v2)    # False
print(v1 != v2)    # True   —— __ne__ 自动取 __eq__ 反值

3.4 类型转换与数值判定 ​

语法/函数魔术方法说明
bool(obj)__bool__未实现时回退到 __len__
int(obj)__int__
float(obj)__float__
complex(obj)__complex__
hash(obj)__hash__定义 __eq__ 后默认为 None(不可哈希)
索引、bin()__index__把对象当作"整数下标"用
round(obj)__round__
python
class IntBox:
    def __init__(self, value):
        self.value = value
    def __int__(self):
        return self.value
    def __bool__(self):
        return self.value != 0
    def __index__(self):
        return self.value

b = IntBox(5)
print(int(b))              # 5
print(bool(b))             # True
print([0, 1, 2, 3, 4, 5, 6][b])   # 5   —— __index__ 让对象能当列表下标
print(bin(b))              # 0b101

避坑提醒:一旦你定义了 __eq__ 而不定义 __hash__,Python 会把 __hash__ 自动设为 None,你的对象就不能放进 set 或当 dict 的键。若对象本质上是"不可变值对象",记得同时实现 __hash__。


四、容器与序列操作:__len__ / __getitem__ / __setitem__ / __delitem__ / __iter__ / __next__ / __contains__ / __reversed__ ​

这一组让你把自定义类"变成"一个列表、字典般的容器。

4.1 基础容器协议:__len__ / __getitem__ / __setitem__ / __delitem__ ​

方法触发语法
__len__len(obj)
__getitem__obj[i](取值)
__setitem__obj[i] = v(赋值)
__delitem__del obj[i](删除)
python
class Cart:
    def __init__(self):
        self._items = []
    def __len__(self):
        return len(self._items)
    def __getitem__(self, i):
        return self._items[i]
    def __setitem__(self, i, v):
        self._items[i] = v
    def __delitem__(self, i):
        del self._items[i]

c = Cart()
c._items = ["苹果", "香蕉", "橙子"]
print(len(c))        # 3
print(c[1])          # 香蕉
c[1] = "西瓜"        # 修改
del c[2]             # 删除"橙子"
print(c._items)      # ['苹果', '西瓜']

进阶要点:只要实现了 __getitem__,即使没实现 __iter__,Python 也能靠"从下标 0 依次取值直到 IndexError"来自动支持 for 循环和 in 判断(旧式序列协议)。

4.2 迭代协议:__iter__ / __next__ ​

for x in obj 的本质是三步曲:

  1. 调用 obj.__iter__() 拿到一个迭代器;
  2. 反复调用迭代器的 __next__() 取下一个值;
  3. 当 __next__ 抛出 StopIteration 时,循环结束。

经典案例——自己实现一个斐波那契迭代器:

python
class Fib:
    def __init__(self, n):
        self.n = n
        self.a, self.b = 0, 1
        self.count = 0
    def __iter__(self):
        return self                 # 迭代器返回自身
    def __next__(self):
        if self.count >= self.n:
            raise StopIteration     # 结束信号
        self.a, self.b = self.b, self.a + self.b
        self.count += 1
        return self.a

print(list(Fib(5)))   # [1, 1, 2, 3, 5]

4.3 __contains__:自定义 in 判断 ​

item in obj 会优先调用 obj.__contains__(item);若未定义,则回退到 __iter__ 或 __getitem__ 逐个比对。

python
class Team:
    def __init__(self, members):
        self.members = members
    def __contains__(self, name):
        return name in self.members

t = Team(["小明", "小红"])
print("小明" in t)    # True
print("小刚" in t)    # False

4.4 __reversed__ 与 __missing__ ​

  • __reversed__:由 reversed(obj) 触发,返回逆向迭代器(未定义时靠 __len__ + __getitem__ 逆序兜底);
  • __missing__:字典子类专属,当 dict[key] 找不到键时触发(__getitem__ 内部),是 defaultdict 的实现基础。
python
class DefaultDict(dict):
    def __missing__(self, key):
        return f"键 {key!r} 不存在"

d = DefaultDict({"a": 1})
print(d["a"])    # 1
print(d["b"])    # 键 'b' 不存在  —— 没抛 KeyError

五、可调用对象:__call__ ​

实现了 __call__ 的类,其实例可以像函数一样被调用 obj(...)。这让对象同时拥有"数据 + 行为"两种身份,是许多设计模式(策略、装饰器、缓存)的基石。

python
class Multiplier:
    def __init__(self, factor):
        self.factor = factor
    def __call__(self, x):
        return x * self.factor

double = Multiplier(2)
triple = Multiplier(3)
print(double(5))    # 10
print(triple(5))    # 15

经典实战:用 __call__ 实现一个带"状态"的装饰器类(比用闭包更清晰):

python
class CountCalls:
    def __init__(self, func):
        self.func = func
        self.count = 0
    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"第 {self.count} 次调用")
        return self.func(*args, **kwargs)

@CountCalls
def hello(name):
    return f"你好, {name}"

print(hello("小明"))   # 第 1 次调用\n你好, 小明
print(hello("小红"))   # 第 2 次调用\n你好, 小红

六、属性访问控制:__getattr__ / __setattr__ / __delattr__ / __getattribute__ ​

这组方法拦截属性的"读、写、删",是实现懒加载、属性校验、代理对象的利器,但也是递归陷阱的重灾区。

6.1 __getattribute__:所有属性访问的"总闸门" ​

只要访问属性(无论存在与否),都会先经过 __getattribute__。它优先级最高、最"霸道",一旦实现,任何 obj.attr 都会进来。

6.2 __getattr__:仅当"找不到"时才兜底 ​

__getattr__ 只在常规查找失败(属性不存在)时才被触发,相当于"查漏补缺"的兜底逻辑。

两者触发差异的直观演示:

python
class Lazy:
    def __init__(self):
        self.real = "真实存在的属性"
    def __getattr__(self, name):
        return f"动态生成: {name}"     # 仅当属性不存在时触发

l = Lazy()
print(l.real)     # 真实存在的属性   —— 属性存在,走正常查找,不进 __getattr__
print(l.other)    # 动态生成: other  —— 属性不存在,触发 __getattr__

__getattr__ 的经典场景是懒加载与远程代理(把不存在的方法转发给别的对象)。

6.3 __setattr__ / __delattr__:拦截写入与删除 ​

  • __setattr__ 在任何 obj.x = value 赋值时触发;
  • __delattr__ 在 任何 del obj.x 时触发。

6.4 避坑要点:递归陷阱(必看) ​

陷阱一:__getattribute__ 里用 self.xxx 会无限递归。因为 self.xxx 本身就是一次属性访问,又会触发 __getattribute__,形成死循环:

python
class Bad:
    def __getattribute__(self, name):
        return self.__dict__[name]   # 错误!self.__dict__ 再次触发 __getattribute__ → 无限递归

正确写法——用 object.__getattribute__ 或 super() 绕过:

python
class Good:
    def __getattribute__(self, name):
        if name == "secret":
            raise AttributeError("禁止访问 secret")
        return object.__getattribute__(self, name)   # 正确:走底层实现,不再触发自身

陷阱二:__setattr__ 里用 self.xxx = ... 也会无限递归。同理,必须用 object.__setattr__:

python
class Safe:
    def __init__(self, value):
        object.__setattr__(self, "value", value)   # 正确
    def __setattr__(self, name, value):
        if name == "value" and value < 0:
            raise ValueError("不能为负数")
        object.__setattr__(self, name, value)

s = Safe(10)
s.value = 5    # 正常
# s.value = -1  # 触发 ValueError

一句话总结避坑原则:在 __getattribute__ / __setattr__ / __delattr__ 内部操作属性时,一律走 object.__xxx__(或 super().__xxx__),绝不用 self.xxx 语法。

补充:__dir__ 也属本类,由 dir(obj) 触发,用于自定义"对象有哪些属性"的返回列表。


七、上下文管理器:__enter__ / __exit__ ​

with 语句的背后就是这两个方法,是 Python 资源管理的最佳实践,也是 __del__ 的"正牌替代者"。

  • __enter__:进入 with 块时调用,返回给 as 后面的变量;
  • __exit__:离开 with 块时调用,参数依次是 (异常类型, 异常值, traceback);返回 True 表示吞掉异常,返回 False(或 None)表示异常继续向外抛。

实战案例:用 __enter__/__exit__ 实现数据库连接的自动释放:

python
class FakeConn:
    def query(self, sql):
        return f"执行: {sql}"
    def close(self):
        print("连接已关闭")

class Database:
    def __init__(self, dsn):
        self.dsn = dsn
    def __enter__(self):
        print("建立连接")
        self.conn = FakeConn()
        return self.conn
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.conn.close()      # 无论是否异常,都会释放连接
        return False           # 不吞异常

with Database("mysql://localhost") as db:
    print(db.query("SELECT 1"))

# 输出:
# 建立连接
# 执行: SELECT 1
# 连接已关闭

核心价值:with 块内无论正常结束还是抛出异常,__exit__ 都会执行,资源绝不会泄漏——这正是 __del__ 做不到的确定性。


八、描述符与元编程:__get__ / __set__ / __delete__ / __set_name__ / __prepare__ / __instancecheck__ / __subclasscheck__ ​

这一章进入 Python 的"元编程"深水区——描述符与元类。它们是 property、classmethod、staticmethod、ORM、类型系统的底层基石。

8.1 描述符协议:__get__ / __set__ / __delete__ / __set_name__ ​

一个类只要实现了 __get__(可再选实现 __set__/__delete__),它就成为描述符,可以挂在别的类上"接管"属性访问:

  • 同时实现 __get__ + __set__(或 __delete__)→ 数据描述符,优先级最高;
  • 只实现 __get__ → 非数据描述符,优先级低于实例属性。

实战案例:用描述符实现"字段值必须为正数"的校验:

python
class Positive:
    def __set_name__(self, owner, name):
        self.name = name                 # 自动拿到属性名,无需手动传参
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.name)
    def __set__(self, obj, value):
        if value <= 0:
            raise ValueError("必须为正数")
        obj.__dict__[self.name] = value

class Account:
    balance = Positive()      # 描述符实例,绑定到类属性

acc = Account()
acc.balance = 100
print(acc.balance)     # 100
# acc.balance = -5     # 抛出 ValueError: 必须为正数

8.2 元类相关:__prepare__ / __new__ / __init__ / __call__ ​

元类(metaclass)是"类的类",通过 metaclass= 指定。它控制类被创建时的全过程,调用顺序如下:

  1. __prepare__:准备存放类属性的命名空间(可自定义为有序字典等);
  2. __new__:创建类对象本身;
  3. __init__:初始化类对象。

而元类的 __call__ 则在实例化(SomeClass())时触发,是控制实例创建的终极入口。

python
class Meta(type):
    @classmethod
    def __prepare__(mcs, name, bases):
        print(f"1. __prepare__: {name}")
        return {}
    def __new__(mcs, name, bases, namespace):
        print(f"2. __new__: 创建类 {name}")
        return super().__new__(mcs, name, bases, namespace)
    def __init__(cls, name, bases, namespace):
        print(f"3. __init__: 初始化类 {name}")
        super().__init__(name, bases, namespace)
    def __call__(cls, *args, **kwargs):
        print(f"4. __call__: 实例化 {cls.__name__}")
        return super().__call__(*args, **kwargs)

class A(metaclass=Meta):
    def __init__(self):
        print("5. A.__init__")

a = A()
# 输出:1 → 2 → 3(定义类时);4 → 5(实例化时)

8.3 __instancecheck__ / __subclasscheck__:改写 isinstance / issubclass ​

在元类中定义它们,可以自定义 isinstance() / issubclass() 的判定逻辑,实现**"鸭子类型"式的虚拟子类**(abc 抽象基类的底层原理)。

python
class ShapeMeta(type):
    def __instancecheck__(cls, instance):
        return hasattr(instance, "area")   # 只要有 area 方法就算"形状"

class Shape(metaclass=ShapeMeta):
    pass

class Circle:
    def area(self):
        return 3.14

c = Circle()
print(isinstance(c, Shape))    # True  —— 不靠继承,靠"有没有 area"

8.4 其他元编程方法 ​

  • __init_subclass__:当某个类被继承时自动调用(类方法),常用于"注册所有子类"的插件系统:
python
class Plugin:
    registry = []
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        cls.registry.append(cls)

class PluginA(Plugin): pass
class PluginB(Plugin): pass

print(len(Plugin.registry))    # 2   —— 自动收集了 A、B 两个子类
  • __class_getitem__:支持 MyClass[...] 语法(泛型标注 list[int] 的底层机制):
python
class Box:
    def __class_getitem__(cls, item):
        print("泛型参数:", item)
        return cls

Box[int]     # 泛型参数: <class 'int'>

九、异步与协程相关:__await__ / __aiter__ / __anext__ / __aenter__ / __aexit__ ​

Python 的 async/await 同样靠魔术方法驱动,是同步版的"异步孪生"。

同步异步触发语法
__iter____aiter__async for
__next____anext__async for(取值)
__enter____aenter__async with
__exit____aexit__async with(退出)
—__await__await obj(可等待对象)
python
import asyncio

class AsyncRange:
    def __init__(self, n):
        self.n = n
    def __aiter__(self):
        return self
    async def __anext__(self):
        if self.n <= 0:
            raise StopAsyncIteration
        self.n -= 1
        return self.n

async def main():
    result = [x async for x in AsyncRange(3)]
    print(result)          # [2, 1, 0]

asyncio.run(main())

异步上下文管理器(__aenter__/__aexit__)用于异步资源的自动释放,如异步数据库连接池:

python
import asyncio

class AsyncDB:
    async def __aenter__(self):
        print("异步建立连接")
        return self
    async def __aexit__(self, *exc):
        print("异步释放连接")

async def main():
    async with AsyncDB() as db:
        print("执行业务")

asyncio.run(main())
# 输出:异步建立连接 → 执行业务 → 异步释放连接

十、序列化与拷贝:__reduce__ / __getstate__ / __setstate__ / __copy__ / __deepcopy__ ​

这一组让你控制对象的序列化(pickle)与拷贝行为。

  • __getstate__ / __setstate__:定制 pickle 时"存什么、怎么恢复"(常用于剔除敏感字段或处理不可序列化属性);
  • __reduce__ / __reduce_ex__:更底层的序列化协议定制;
  • __copy__ / __deepcopy__:定制 copy.copy() / copy.deepcopy() 的行为。
python
import copy

class Person:
    def __init__(self, name, password):
        self.name = name
        self.password = password
    def __getstate__(self):
        state = self.__dict__.copy()
        state.pop("password", None)     # 序列化时剔除密码
        return state
    def __setstate__(self, state):
        self.__dict__.update(state)
        self.password = None            # 恢复时密码置空
    def __copy__(self):
        print("浅拷贝被调用")
        return Person(self.name, self.password)

p = Person("小明", "123456")
p2 = copy.copy(p)          # 浅拷贝被调用
print(p2.name, p2.password)   # 小明 123456

十一、常见误区汇总(查漏补缺清单) ​

误区正确理解
__str__ 和 __repr__ 分不清__repr__ 给开发者看(调试、无歧义),__str__ 给用户看(友好、可读);print 优先 __str__,未定义时回退 __repr__
以为 __init__ 是构造函数真正的构造函数是 __new__,__init__ 只是初始化;__init__ 必须返回 None
__getattr__ 与 __getattribute__ 混淆__getattribute__ 拦截所有属性访问,__getattr__ 只在找不到时兜底
在 __getattribute__/__setattr__ 里写 self.xxx会无限递归,必须用 object.__xxx__ 或 super().__xxx__
定义了 __eq__ 却没管 __hash__Python 会把 __hash__ 置为 None,对象变不可哈希,无法放入 set/作 dict 键
给可变对象定义了 __hash__内容一变,哈希值就变,会导致 set/dict 行为错乱,应避免
运算符不返回 NotImplemented直接 return None 或抛错,会让 __rxxx__ 反射机制失效、报错信息不友好
用 __del__ 做资源释放时机不可控(循环引用、解释器退出),应改用 __enter__/__exit__
__contains__ 语义搞错它判断的是"值是否在容器内",不是"是否包含某子串"这种自定义逻辑,实现时别与 __getitem__ 混用
__iter__ 返回了非迭代器__iter__ 必须返回一个实现了 __next__ 的迭代器对象,否则 for 循环报错

十二、实战综合案例:自定义向量类 Vector ​

下面这个类把前文多类魔术方法整合到一个真实对象里:它既是一个容器(支持 len、索引、遍历),又是一个数值(支持加减乘、比较),还可以像函数一样被调用。

python
import math

class Vector:
    def __init__(self, *components):
        self.components = list(components)

    # —— 表示 ——
    def __repr__(self):
        return f"Vector({', '.join(map(str, self.components))})"

    # —— 容器协议 ——
    def __len__(self):
        return len(self.components)

    def __getitem__(self, index):
        return self.components[index]

    def __setitem__(self, index, value):
        self.components[index] = value

    def __iter__(self):
        return iter(self.components)

    # —— 数值运算 ——
    def __add__(self, other):
        if not isinstance(other, Vector):
            return NotImplemented
        if len(self) != len(other):
            raise ValueError("维度不一致,无法相加")
        return Vector(*(a + b for a, b in zip(self, other)))

    def __sub__(self, other):
        if not isinstance(other, Vector):
            return NotImplemented
        if len(self) != len(other):
            raise ValueError("维度不一致,无法相减")
        return Vector(*(a - b for a, b in zip(self, other)))

    def __mul__(self, scalar):
        if isinstance(scalar, (int, float)):
            return Vector(*(x * scalar for x in self))
        return NotImplemented

    __rmul__ = __mul__          # 支持 3 * v 与 v * 3

    def __neg__(self):
        return Vector(*(-x for x in self))

    def __abs__(self):
        return math.sqrt(sum(x * x for x in self))

    # —— 比较与判定 ——
    def __eq__(self, other):
        if not isinstance(other, Vector):
            return NotImplemented
        return self.components == other.components

    def __bool__(self):
        return any(self.components)

    # —— 可调用对象 ——
    def __call__(self):
        m = abs(self)
        return self * (1 / m) if m else Vector(*self.components)   # 返回单位向量


# ===== 测试 =====
v1 = Vector(3, 4)
v2 = Vector(1, 1)

print(v1)                 # Vector(3, 4)
print(len(v1))            # 2
print(v1[0])              # 3
print(list(v1))           # [3, 4]

v3 = v1 + v2
print(v3)                 # Vector(4, 5)

v4 = v1 * 2
print(v4)                 # Vector(6, 8)
print(3 * v1)             # Vector(9, 12)

print(abs(v1))            # 5.0
print(v1 == Vector(3, 4)) # True
print(bool(Vector(0, 0))) # False
print(v1())               # Vector(0.6, 0.8)  —— 调用 __call__ 返回单位向量

这个案例的价值:一个 Vector 类同时具备了"容器访问"(v[0]、for x in v)与"数值运算"(v1 + v2、3 * v)能力,代码读起来就像内置类型一样自然——这正是魔术方法把"自定义对象"升级为"一等公民"的力量。


十三、总结:魔术方法的核心价值 ​

回望全文,魔术方法的本质可以浓缩成一句话:

魔术方法是 Python 协议的实现载体——你不需要继承某个父类,只需实现约定的方法,就能让自定义对象无缝接入 Python 的语法体系。

它的核心价值体现在三点:

  1. 统一的对象模型:len()、+、in、with、for 这些语法背后的机制完全一致——都去调用约定好的魔术方法,让语言保持极简而强大;
  2. 优雅的"鸭子类型":只要实现了 __iter__,你的对象就能被 for 遍历;只要实现了 __enter__/__exit__,就能用 with——"看起来像鸭子、叫起来像鸭子,那它就是鸭子";
  3. 强大的元编程能力:描述符、元类、__getattribute__ 让你能介入对象生命周期的每一个环节,构建出 ORM、装饰器、插件系统等高级框架。

学习路径建议:

  • 入门者:先吃透第一、二、四、五、七章(创建/销毁、字符串、容器、可调用、上下文管理器),这些是日常开发最高频的部分;
  • 进阶者:深入第六、八章(属性访问、描述符与元类),它们是阅读 Django、SQLAlchemy 等框架源码的钥匙;
  • 查漏补缺:对照第十一节的误区清单,逐条自测,确保没有踩坑。

官方完整清单请查阅 Python 文档的 Data model 章节,那里列出了每一个魔术方法的精确定义与调用时机,本文的分类框架可作为你的"索引地图"。