Python 里有一类特殊方法,名字以双下划线开头和结尾,比如 __init__、__str__。它们被统称为"魔法方法"(magic methods)或"双下方法"(dunder methods)。
这些方法不是让你直接调用的,而是 Python 解释器在特定场景下自动触发。
掌握它们,意味着你能让自己的类像内置类型一样工作:用 print() 输出、用 [] 索引、用 with 管理资源,甚至让对象像函数一样被调用。
魔法方法是 Python 数据模型(Data Model)的钩子。当你写 len(obj) 时,Python 实际执行的是 obj.__len__();当你写 obj + other 时,Python 会找 obj.__add__(other)。
这种设计叫运算符重载,但远不止运算符 —— 它覆盖了对象构造、销毁、属性访问、上下文管理等方方面面。
注意:不要直接调用 obj.__xxx__(),而是使用对应的语法或内置函数,让 Python 替你调用。这保证了一致性,也方便后续替换底层实现。
__init__ 是初始化方法,在对象创建后立刻调用。真正的对象分配(__new__)很少需要自定义,日常开发中 __init__ 就是构造函数的同义词。
例如:
class Book:
def __init__(self, title: str, price: float, author: str = "Unknown"):
self.title = title
self.price = price
self.author = author
self._created = True # 内部状态标记
def info(self) -> str:
return f"《{self.title}》{self.author} 著,¥{self.price}"
# 实例化时自动调用 __init__
b = Book("Python 进阶", 59.0, "老王")
print(b.info()) # 《Python 进阶》老王 著,¥59.0
# 默认参数生效
b2 = Book("极简入门", 29.0)
print(b2.info()) # 《极简入门》Unknown 著,¥29.0运行实例,输出如下:
《Python 进阶》老王 著,¥59.0
《极简入门》Unknown 著,¥29.0注意:__init__ 没有返回值(或者返回 None)。如果你写 return self 会报错。另外,__init__ 可以配合类型注解做参数校验,但校验逻辑通常放在 __init__ 里或者交给 Pydantic/dataclass。
__del__ 在对象被垃圾回收时调用。它的用途是释放外部资源,比如关闭文件句柄、断开网络连接。但不要依赖它做关键清理,因为垃圾回收时机不确定,可能出现循环引用导致对象永不被回收。
例如:
class TempFile:
def __init__(self, path: str):
self.path = path
self.fp = open(path, 'w')
print(f"[TempFile] 打开 {path}")
def write(self, data: str):
self.fp.write(data)
def __del__(self):
# 尽量在 __del__ 里做防御式清理,但别依赖它
if hasattr(self, 'fp') and self.fp and not self.fp.closed:
self.fp.close()
print(f"[TempFile] 自动关闭 {self.path}")
# 使用
f = TempFile("/tmp/demo.txt")
f.write("hello")
del f # 手动删除引用,触发 __del__(不一定立即发生)
# 更好的做法:用 with 语句,后续将会介绍运行示例,输出如下:
[TempFile] 打开 /tmp/demo.txt
[TempFile] 自动关闭 /tmp/demo.txt注意了,下面是 __del__ 的坑:
如果对象存在循环引用(A 引用 B,B 引用 A),__del__ 可能永远不执行;
__del__ 执行时,解释器可能正在关闭,全局变量已经不存在,访问它们会报错;
异常处理:如果 __del__ 里抛出异常,会被忽略并打印到 stderr;
注意:如果要对关键资源清理,请使用 with + __enter__/__exit__,不要使用 __del__。
这两个方法控制对象被转换成字符串时的行为。其中, __str__ 面向用户,__repr__ 面向开发者。
str(obj) 和 print(obj) 会调用 __str__。它应该返回简洁、可读性高的描述。例如:
class Point:
def __init__(self, x: float, y: float):
self.x = x
self.y = y
def __str__(self) -> str:
return f"Point({self.x}, {self.y})"
p = Point(3, 4)
print(p) # Point(3, 4)
print(str(p)) # Point(3, 4)repr(obj) 和交互式解释器直接输入变量名回车时调用 __repr__。约定:返回的字符串如果能被 eval() 执行并重建对象,将是最佳实践。如果做不到,至少用 <ClassName ...> 的格式明确类型和核心状态。例如:
class Point:
def __init__(self, x: float, y: float):
self.x = x
self.y = y
def __str__(self) -> str:
return f"坐标({self.x}, {self.y})"
def __repr__(self) -> str:
# 理想情况:eval(repr(p)) 能重建 p
return f"Point({self.x!r}, {self.y!r})"
p = Point(3, 4)
print(p) # 坐标(3, 4) —— 调用 __str__
print(repr(p)) # Point(3, 4) —— 调用 __repr__
# 验证 eval 重建
p2 = eval(repr(p))
print(p2.x, p2.y) # 3 4如果没有定义 __str__,Python 会回退到 __repr__。所以只写 __repr__ 也能覆盖 print() 的场景,但反过来不行。
演示容器变量如何实现 __str__、__repr__ 等,例如:
class Stack:
def __init__(self):
self._items = []
def push(self, item):
self._items.append(item)
def __str__(self):
return f"Stack({len(self._items)} items)"
def __repr__(self):
return f"Stack({self._items!r})"
s = Stack()
s.push(1)
s.push(2)
print(s) # Stack(2 items)
print(repr(s)) # Stack([1, 2])注意,在 f-string 格式化里,!r 表示对变量自动调用 repr();!s 对应 str(),!a 对应 ascii()。
正常情况下,函数和类实例可以调用:func()、Class()。但如果你想让已经创建的对象也能被调用,就需要 __call__。
让类的实例对象变得可被调用,像普通函数一样直接加括号“实例()”执行。例如:
class Adder:
def __init__(self, base: int):
self.base = base
def __call__(self, x: int) -> int:
return self.base + x
# 注意看下面
# 创建了一个 add5 的实例,然后直接 add5(x) 进行调用
# 相当于 add5.__call__(x)
add5 = Adder(5)
print(add5(10)) # 15
print(add5(3)) # 8
# 检查类型
print(callable(add5)) # True__call__ 的典型场景:
装饰器类(保存状态,多次调用);
缓存/记忆化函数;
策略模式,把不同策略封装成可调用对象统一接口。
借助Python中类内置的__call__特殊方法来完成装饰器类的搭建与实现工作,例如:
import time
# 导入wraps装饰器,保留被装饰原函数的元信息(函数名、文档字符串、参数签名等)
from functools import wraps
class Timer:
"""
计时器装饰器类
依靠 __call__ 魔术方法,让类实例可以当作装饰器 @Timer() 使用
"""
def __init__(self, prefix: str = "耗时"):
self.prefix = prefix
def __call__(self, func):
"""
类被当作装饰器挂载时自动触发
:param func: 被当前装饰器包裹的目标函数
:return wrapper: 包装后的代理函数
"""
# @wraps(func) 修复装饰器带来的函数信息丢失,wrapper对外表现等同于原func
@wraps(func)
def wrapper(*args, **kwargs):
"""
真正执行前后计时的包装函数
*args 接收原函数所有位置参数
**kwargs 接收原函数所有关键字参数
"""
# 记录函数执行前的时间戳(单位:秒)
start = time.time()
# 执行原始业务函数,接收运行结果
result = func(*args, **kwargs)
# 计算总耗时:当前时间 - 开始时间
elapsed = time.time() - start
print(f"[{self.prefix}] {func.__name__}: {elapsed:.4f}s")
return result
# 返回包装函数,完成装饰器绑定
return wrapper
# 实例化Timer对象(传入自定义前缀“执行时间”),并作为装饰器修饰slow_sum函数
@Timer("执行时间")
def slow_sum(n: int) -> int:
# 强制休眠0.1秒,模拟业务代码运行耗时
time.sleep(0.1)
return sum(range(n))
# 调用被计时装饰器包裹的函数
print(slow_sum(100000))这个例子用 __call__ 让 Timer 实例变成了装饰器,比函数装饰器更灵活,因为可以传参配置 prefix。
这三个方法让对象支持 [] 操作:取值、赋值、删除。配合它们,你的类可以表现得像 list 或 dict。
该内置方法能够完整支持对象的下标取值访问模式,同时也兼容常规的切片截取操作,借助这一方法开发者可以像操作列表、元组这类内置序列类型一样,通过方括号书写下标数字精准获取对应位置的单个元素,也能按照起始索引、终止索引与步长的组合规则完成区间切片提取工作。例如:
class RingBuffer:
"""固定大小的循环缓冲区,支持索引和切片"""
def __init__(self, size: int):
self.size = size
self._buffer = [None] * size
self._head = 0
self._count = 0
def append(self, item):
self._buffer[self._head] = item
self._head = (self._head + 1) % self.size
self._count = min(self._count + 1, self.size)
def __getitem__(self, index):
"""支持 obj[i] 和 obj[a:b]"""
# slice(start, stop, step) 切片对象
if isinstance(index, slice):
# 切片:返回列表
# 计算切片的实际范围,然后使用range生成索引列表
indices = range(*index.indices(self._count))
return [self._buffer[(self._head - self._count + i) % self.size] for i in indices]
if not isinstance(index, int):
raise TypeError("索引必须是整数或切片")
if index < 0:
index += self._count
if not (0 <= index < self._count):
raise IndexError("索引越界")
# 把逻辑索引映射到物理数组位置
physical = (self._head - self._count + index) % self.size
return self._buffer[physical]
def __len__(self):
return self._count
def __repr__(self):
return f"RingBuffer({list(self)}, size={self.size})"
# 创建 RingBuffer 并添加数据,大小设置为5
rb = RingBuffer(5)
for i in range(10):
rb.append(i)
print(rb[0]) # 5 (循环覆盖后最早的数据)
print(rb[-1]) # 9 (最新数据)
print(rb[1:4]) # [6, 7, 8] (切片)
print(rb) # RingBuffer([5, 6, 7, 8, 9], size=5)__getitem__ 接收的 index 不一定是整数,也可能是 slice 对象。处理切片时需要小心:
slice.indices(len) 会返回 (start, stop, step),已经做了负数索引的规范化;
迭代协议(for x in obj)也依赖 __getitem__,如果对象没有定义 __iter__,Python 会尝试用 __getitem__(0)、__getitem__(1)... 顺序访问,直到抛出 IndexError。
主要作用是让自定义类的实例对象能够像字典、列表这类内置容器一样,支持通过方括号[]执行赋值操作与元素删除操作。例如:
class ConfigDict:
"""支持点号和下标访问的配置字典"""
def __init__(self):
self._data = {}
# 取值
def __getitem__(self, key):
return self._data[key]
# 设置值
def __setitem__(self, key, value):
if not isinstance(key, str):
raise TypeError("Key must be string")
self._data[key] = value
# 删除值
def __delitem__(self, key):
del self._data[key]
# 判断是否包含值
def __contains__(self, key):
return key in self._data
# 迭代
def __iter__(self):
return iter(self._data)
def __repr__(self):
return f"ConfigDict({self._data!r})"
# 使用
cfg = ConfigDict()
cfg['host'] = 'localhost'
cfg['port'] = 3306
print(cfg['host']) # localhost
print('port' in cfg) # True
del cfg['port']
print(cfg) # ConfigDict({'host': 'localhost'})
# 迭代
for k in cfg:
print(k)这里额外引入了 __contains__(支持 in 操作)和 __iter__(支持 for 循环)。它们是序列协议的延伸,让自定义字典更完整。如果你只想快速实现一个类字典对象,继承 collections.UserDict 更省事,但手写魔法方法有助于理解底层机制。
with 语句是 Python 资源管理的最佳实践。它能保证代码块结束后,资源被正确释放,无论中间是否发生异常。支撑 with 的魔法方法就是 __enter__ 和 __exit__。
下面通过一个最简简单的示例,介绍 with 的魔法方法就是 __enter__ 和 __exit__ 是如何工作的。例如:
class ManagedFile:
def __init__(self, path: str, mode: str = 'r'):
self.path = path
self.mode = mode
self.fp = None
def __enter__(self):
print(f"[Enter] 打开 {self.path}")
self.fp = open(self.path, self.mode)
return self.fp # 返回给 as 变量
def __exit__(self, exc_type, exc_val, exc_tb):
print(f"[Exit] 关闭 {self.path}")
if self.fp:
self.fp.close()
# 返回 False 让异常继续传播,返回 True 则吞掉异常
return False
# 使用
with ManagedFile("test.txt", "w") as f:
f.write("hello with\n")
# 即使这里抛异常,__exit__ 也会执行
print("文件已关闭")运行示例,输出如下:
[Enter] 打开 test.txt
[Exit] 关闭 test.txt
文件已关闭代码执行完成后,会在当前目录创建一个 test.txt 文件,内容为“hello with”。我们在 __enter__ 方法中打开文件,在 __exit__ 中关闭文件,释放资源。注意,__enter__ 和 __exit__ 方法均是通过 with 语句自动触发,不要我们手动调用。
__exit__ 接收三个参数:
exc_type:异常类型,无异常时为 None;
exc_val:异常对象;
exc_tb:traceback 对象。
如果 __exit__ 返回 True,异常被压制,不会向上传播。这通常用于某些容错场景,但不要轻易吞掉异常,否则调试时会很痛苦。
继续使用 __enter__ 和 __exit__ 实现计时器,计算一段代码执行所消耗的时长,例如:
import time
from typing import Optional
class TimerCtx:
def __init__(self, name: str = "Block"):
self.name = name
self.start: Optional[float] = None
def __enter__(self):
self.start = time.time()
print(f"[{self.name}] 开始...")
return self # 返回自身,允许 as 变量访问
def __exit__(self, exc_type, exc_val, exc_tb):
elapsed = time.time() - self.start
status = "异常" if exc_type else "完成"
print(f"[{self.name}] {status},耗时: {elapsed:.4f}s")
return False # 不吞异常
# 使用
with TimerCtx("数据加载"):
time.sleep(0.2)
# 模拟加载
with TimerCtx("异常测试"):
time.sleep(0.1)
raise ValueError("模拟错误")
# 输出:耗时 + 异常,然后 ValueError 继续抛出运行代码,输出如下:
[数据加载] 开始...
[数据加载] 完成,耗时: 0.2010s
[异常测试] 开始...
[异常测试] 异常,耗时: 0.1006s
Traceback (most recent call last):
File "d:\share_dir\workspace\5.demo\python_demo\demo.py", line 27, in <module>
raise ValueError("模拟错误")
ValueError: 模拟错误该示例通过在 __enter__ 中获取当前时间,在 __exit__ 中使用当前时间减去 __enter__ 中获取的时间,就是整个 with 语句块执行的耗时。
结合前面学过的锁知识,使用上下文管理器可以实现一个优雅的锁封装,例如:
import threading
from contextlib import contextmanager
class NamedLock:
"""带名称的锁,支持 with 语句"""
def __init__(self, name: str):
self.name = name
self._lock = threading.Lock()
# 进入 with 时获取锁
def __enter__(self):
print(f"[Lock] {self.name} 正在获取...")
self._lock.acquire()
print(f"[Lock] {self.name} 已获取")
return self
# 退出 with 时释放锁,即使代码报错了,也会释放锁
def __exit__(self, exc_type, exc_val, exc_tb):
self._lock.release()
print(f"[Lock] {self.name} 已释放")
return False
# 使用
with NamedLock("数据库连接"):
print("执行数据库操作...")
# 等价于:
lock = NamedLock("数据库连接")
lock.__enter__()
try:
print("执行数据库操作...")
finally:
lock.__exit__(None, None, None)运行代码,输出如下:
[Lock] 数据库连接 正在获取...
[Lock] 数据库连接 已获取
执行数据库操作...
[Lock] 数据库连接 已释放
[Lock] 数据库连接 正在获取...
[Lock] 数据库连接 已获取
执行数据库操作...
[Lock] 数据库连接 已释放对于简单场景,也可以用 contextlib.contextmanager 装饰器把生成器变成上下文管理器:
# 导入上下文管理器专用装饰器工具,用于快速自定义 with 语法的上下文管理器
from contextlib import contextmanager
import time
# 使用装饰器快速将普通生成器函数包装成上下文管理器,支持with语句自动执行进入/退出逻辑
@contextmanager
def managed_resource(name):
"""
自动完成资源初始化、业务逻辑执行、资源收尾清理,即便中间代码异常也一定会执行清理逻辑
:param name: 资源标识名称,例如数据库连接、网络连接、文件句柄等
"""
# with语句进入时执行:资源初始化/打开操作
print(f"[Resource] {name} 初始化")
try:
# yield 分割上下文两段逻辑
# 1. yield 之前:进入with的前置初始化
# 2. yield 此处暂停函数,将yield后面的值赋值给with ... as 中的接收变量res
# 3. with代码块内部业务逻辑运行完毕后,回到yield下方继续执行
yield name # yield 的值传递给 with 语句中 as 绑定的变量
# 无论with内部正常结束、抛出异常、主动return,finally块必定执行
finally:
# 资源收尾:关闭连接、释放句柄、回收内存、解锁、回滚事务等清理工作
print(f"[Resource] {name} 清理")
# 使用with语法调用自定义上下文管理器
# 流程:
# 1. 进入with:执行managed_resource中yield之前的初始化代码
# 2. 将yield产出的"网络连接"赋值给变量res,执行缩进内的业务代码
# 3. 缩进代码执行完毕/异常中断,自动进入finally执行资源清理
with managed_resource("网络连接") as res:
# 业务逻辑:正常使用已初始化好的资源
print(f"使用 {res}...")运行示例,输出如下:
[Resource] 网络连接 初始化
使用 网络连接...
[Resource] 网络连接 清理虽然 contextmanager 更简洁,但遇到复杂状态(如需要处理多种异常类型)时,推荐手写 __enter__/__exit__ 更灵活。
下面以文件读取示例,更进一步演示 contextmanager 的用法,例如:
from contextlib import contextmanager
@contextmanager
def safe_file_handler(file_path: str, mode: str = "r", encoding: str = "utf-8"):
"""
自定义文件操作上下文管理器
自动完成:打开文件 → 交付文件对象给业务代码 → 无论是否异常都强制关闭文件
:param file_path: 文件路径
:param mode: 文件打开模式 r只读/w覆盖写入/a追加/r+读写
:param encoding: 文件编码格式
:yield: 打开后的文件对象,with ... as f 接收该对象
"""
# 1、进入上下文:执行前置逻辑,打开文件
print(f"准备打开文件:{file_path},打开模式:{mode}")
f = open(file_path, mode=mode, encoding=encoding)
try:
# 暂停函数执行,把文件对象抛出给with中的变量,业务代码在此期间操作文件
yield f
finally:
# 2、退出上下文必执行:回收资源,关闭文件句柄
# 不管with内部正常结束、抛出异常、break/return,finally一定会执行
f.close()
print(f"文件 {file_path} 已安全关闭,资源释放完成")
if __name__ == "__main__":
# 写入测试数据,先构造一个测试文本文件
with open("test.txt", "w", encoding="utf-8") as fp:
fp.write("第一行测试文本\n第二行测试内容")
# 使用自定义上下文管理器读取文件
with safe_file_handler("test.txt", "r") as file_obj:
print("===== 开始读取文件 =====")
content = file_obj.read()
print(content)运行示例,输出如下:
准备打开文件:test.txt,打开模式:r
===== 开始读取文件 =====
第一行测试文本
第二行测试内容
文件 test.txt 已安全关闭,资源释放完成| 方法 | 触发方式 | 典型场景 |
| __init__(self, ...) | Class(...) | 对象初始化 |
| __del__(self) | del obj / 引用归零 | 辅助清理(慎用) |
| __str__(self) | str(obj) / print(obj) | 用户友好输出 |
| __repr__(self) | repr(obj) / 交互回显 | 调试输出、可重建对象 |
| __call__(self, ...) | obj(...) | 装饰器、策略、缓存 |
| __getitem__(self, k) | obj[k] | 序列取值、字典取值 |
| __setitem__(self, k, v) | obj[k] = v | 序列赋值、字典赋值 |
| __delitem__(self, k) | del obj[k] | 删除元素 |
| __enter__(self) | with obj: | 资源初始化 |
| __exit__(self, exc...) | with 块结束 | 资源清理、异常处理 |
注意,魔法方法不是炫技,而是让 Python 的鸭子类型真正发挥作用。你的类不需要继承任何特定基类,只要实现了正确的方法,就能无缝融入 Python 的生态系统。这也是 Python 设计哲学的体现:"如果它走起路来像鸭子,叫起来像鸭子,那它就是鸭子。"