Python3 魔法方法(Dunder Methods)

🎉摘要:本文深入解析Python魔法方法(双下方法),涵盖__init__、__del__、__str__、__repr__、__call__、__getitem__、__setitem__、上下文管理器等核心方法,通过实例演示如何让自定义类像内置类型一样工作,是Python进阶必学内容。

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__ 与 __del__

__init__:构造方法,不是构造

__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__:析构方法,慎用

__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__ 的坑:

  1. 如果对象存在循环引用(A 引用 B,B 引用 A),__del__ 可能永远不执行;

  2. __del__ 执行时,解释器可能正在关闭,全局变量已经不存在,访问它们会报错;

  3. 异常处理:如果 __del__ 里抛出异常,会被忽略并打印到 stderr;

注意:如果要对关键资源清理,请使用 with + __enter__/__exit__,不要使用 __del__。

字符串表示:__str__ 与 __repr__

这两个方法控制对象被转换成字符串时的行为。其中, __str__ 面向用户,__repr__ 面向开发者。

__str__:友好的用户输出

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__:精确的调试输出

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()。

可调用对象:__call__

正常情况下,函数和类实例可以调用: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。


序列协议:__getitem__、__setitem__、__delitem__

这三个方法让对象支持 [] 操作:取值、赋值、删除。配合它们,你的类可以表现得像 list 或 dict。

__getitem__:支持下标访问和切片

该内置方法能够完整支持对象的下标取值访问模式,同时也兼容常规的切片截取操作,借助这一方法开发者可以像操作列表、元组这类内置序列类型一样,通过方括号书写下标数字精准获取对应位置的单个元素,也能按照起始索引、终止索引与步长的组合规则完成区间切片提取工作。例如:

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。

__setitem__ 与 __delitem__:写和删

主要作用是让自定义类的实例对象能够像字典、列表这类内置容器一样,支持通过方括号[]执行赋值操作与元素删除操作。例如:

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 更省事,但手写魔法方法有助于理解底层机制。

上下文管理器:__enter__ 与 __exit__

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 设计哲学的体现:"如果它走起路来像鸭子,叫起来像鸭子,那它就是鸭子。"

说说我的看法
全部评论(
没有评论
关于
本网站专注于 Java、数据库(MySQL、Oracle)、Linux、软件架构及大数据等多领域技术知识分享。涵盖丰富的原创与精选技术文章,助力技术传播与交流。无论是技术新手渴望入门,还是资深开发者寻求进阶,这里都能为您提供深度见解与实用经验,让复杂编码变得轻松易懂,携手共赴技术提升新高度。如有侵权,请来信告知:hxstrive@outlook.com
其他应用
公众号