面向读者:Python 3.10 + 开发者(进阶 / 高级)核心价值:系统梳理 Python 所有 80 + 魔术方法的原理、用法、边界、工程化实践,结合 200 + 可运行代码示例,解决 “不知道魔术方法能做什么”“什么时候用魔术方法”“怎么用魔术方法写出优雅的代码” 三大痛点。
引言:你真的懂 “魔术方法” 吗?
我曾在一个 Python 技术沙龙上问过 30 位 Python 开发者:“你能说出 5 个以上的魔术方法吗?”结果只有 12 位能说出__init__/__str__/__repr__,只有 3 位能说出__getattr__/__setattr__,没人能说出__slots__/__call__/__enter__等 “高级魔术方法” 的具体用法。
这是 Python 开发者的普遍现状:只知道魔术方法 “存在”,但不知道 “能做什么”“怎么用”“为什么用”。
实际上,魔术方法是Python 面向对象的 “灵魂”—— 它们是 Python 解释器在特定场景下自动调用的方法,让你可以重写 Python 的默认行为(如对象的创建、比较、运算、上下文管理等),写出 “Pythonic” 的优雅代码。
一、魔术方法的基础认知
1.1 什么是魔术方法?
魔术方法(Magic Methods)又称 “双下划线方法”(Dunder Methods),是指以两个下划线开头和结尾的方法(如__init__/__str__/__add__)。
Python 解释器会在特定场景下自动调用这些方法,例如:
- 创建对象时自动调用
__init__; - 打印对象时自动调用
__str__; - 对象相加时自动调用
__add__; - 使用
with语句时自动调用__enter__和__exit__。
1.2 魔术方法的命名规则
所有魔术方法都遵循 **“双下划线开头 + 方法名 + 双下划线结尾”** 的命名规则,例如:
__init__:初始化方法;__str__:字符串表示方法;__add__:加法运算方法;__enter__/__exit__:上下文管理方法。
注意:不要自定义新的魔术方法——Python 可能会在未来版本中使用这些方法,导致冲突。
1.3 魔术方法的调用方式
魔术方法不能直接调用(虽然语法上允许,但不推荐),必须由 Python 解释器自动调用,例如:
# 错误:直接调用魔术方法
obj.__str__()
# 正确:由Python解释器自动调用
print(obj) # 自动调用obj.__str__()
二、核心魔术方法:对象生命周期与表示
这部分包含15 + 核心魔术方法,覆盖对象的创建、初始化、销毁、字符串表示等基础场景。
2.1 对象创建与初始化:__new__ vs __init__
很多开发者认为__init__是 “构造方法”,但实际上 **__new__才是真正的构造方法 **—— 它负责创建对象并返回对象实例,而__init__负责初始化对象的属性。
2.1.1 __new__:构造对象
class MyClass:
def __new__(cls, *args, **kwargs):
print(f"__new__ called with cls={cls}, args={args}, kwargs={kwargs}")
# 必须调用父类的__new__方法创建对象
instance = super().__new__(cls)
return instance
def __init__(self, name):
print(f"__init__ called with self={self}, name={name}")
self.name = name
# 测试
obj = MyClass("张三")
# 输出:
# __new__ called with cls=<class '__main__.MyClass'>, args=('张三',), kwargs={}
# __init__ called with self=<__main__.MyClass object at 0x000001>, name=张三
关键点:
__new__是静态方法(无需装饰器),第一个参数是cls(类本身);__new__必须返回一个对象实例,否则__init__不会被调用;__new__的主要用途:单例模式、不可变对象的继承、元类。
2.1.2 单例模式示例(用__new__)
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.name = "单例实例"
return cls._instance
# 测试
obj1 = Singleton()
obj2 = Singleton()
print(obj1 is obj2) # 输出True,确实是同一个实例
2.1.3 __init__:初始化对象
class Person:
def __init__(self, name, age):
self.name = name # 初始化姓名
self.age = age # 初始化年龄
def __str__(self):
return f"Person(name={self.name}, age={self.age})"
# 测试
person = Person("张三", 20)
print(person) # 输出Person(name=张三, age=20)
关键点:
__init__是实例方法,第一个参数是self(对象实例);__init__没有返回值(必须是 None);__init__的主要用途:初始化对象的属性。
2.2 对象销毁:__del__
__del__是析构方法,在对象被垃圾回收时自动调用,主要用于资源清理(如关闭文件、释放数据库连接等)。
class File:
def __init__(self, filename):
self.filename = filename
self.f = open(filename, "w")
print(f"打开文件:{filename}")
def __del__(self):
self.f.close()
print(f"关闭文件:{self.filename}")
# 测试
file = File("test.txt")
# 输出:打开文件:test.txt
del file # 手动删除对象,触发__del__
# 输出:关闭文件:test.txt
注意事项:
__del__的调用时间不可预测(取决于 Python 的垃圾回收机制);- 不要在
__del__中依赖外部资源(如网络、数据库),因为它们可能已经被清理; - 推荐使用上下文管理器(
__enter__/__exit__)替代__del__进行资源清理。
2.3 对象表示:__str__ vs __repr__
这两个方法都用于返回对象的字符串表示,但应用场景不同:
__str__:用于用户友好的字符串表示(如print()/str());__repr__:用于开发者友好的字符串表示(如repr()/ 调试器)。
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return f"姓名:{self.name},年龄:{self.age}" # 用户友好
def __repr__(self):
return f"Person(name={self.name!r}, age={self.age!r})" # 开发者友好(!r表示自动添加引号)
# 测试
person = Person("张三", 20)
print(person) # 输出:姓名:张三,年龄:20 → 调用__str__
print(repr(person)) # 输出:Person(name='张三', age=20) → 调用__repr__
关键点:
__repr__应该尽可能准确地反映对象的构造方式(如Person(name='张三', age=20));- 如果只定义了
__repr__,而没有定义__str__,那么str()也会调用__repr__; - 推荐同时定义
__str__和__repr__,以提高代码的可读性。
三、属性访问魔术方法:控制属性的读写
这部分包含10 + 属性访问魔术方法,覆盖属性的读取、修改、删除、不存在属性的处理等场景。
3.1 基本属性访问:__getattr__/__setattr__/__delattr__
3.1.1 __getattr__:当访问不存在的属性时调用
class Person:
def __init__(self, name):
self.name = name # 只定义了name属性
def __getattr__(self, attr):
print(f"__getattr__ called with attr={attr}")
# 可以返回默认值
if attr == "age":
return 18
# 如果没有默认值,必须抛出AttributeError
raise AttributeError(f"'Person' object has no attribute '{attr}'")
# 测试
person = Person("张三")
print(person.name) # 输出:张三 → 存在的属性,直接返回
print(person.age) # 输出:__getattr__ called with attr=age → 18 → 不存在的属性,调用__getattr__
# print(person.gender) # 输出:AttributeError: 'Person' object has no attribute 'gender'
3.1.2 __setattr__:当设置任何属性时调用
class Person:
def __init__(self, name):
# 注意:不能直接用self.name = name,否则会无限递归(因为设置self.name会调用__setattr__)
# 正确:用super().__setattr__()
super().__setattr__("name", name)
def __setattr__(self, attr, value):
print(f"__setattr__ called with attr={attr}, value={value}")
# 验证属性
if attr == "age":
if not isinstance(value, int) or value < 0 or value > 120:
raise ValueError("年龄必须是0-120之间的整数")
# 用super().__setattr__()设置属性,避免无限递归
super().__setattr__(attr, value)
# 测试
person = Person("张三")
person.age = 20 # 输出:__setattr__ called with attr=age, value=20
# person.age = 150 # 输出:ValueError: 年龄必须是0-120之间的整数
关键点:
- 在
__setattr__中不能直接用self.attr = value,否则会无限递归; - 必须用 **
super().__setattr__()或self.__dict__[attr] = value** 来设置属性。
3.1.3 __delattr__:当删除任何属性时调用
class Person:
def __init__(self, name):
self.name = name
self.age = 20
def __delattr__(self, attr):
print(f"__delattr__ called with attr={attr}")
# 禁止删除name属性
if attr == "name":
raise AttributeError("禁止删除name属性")
# 用super().__delattr__()删除属性,避免无限递归
super().__delattr__(attr)
# 测试
person = Person("张三")
del person.age # 输出:__delattr__ called with attr=age
# del person.name # 输出:AttributeError: 禁止删除name属性
3.2 高级属性访问:__getattribute__/__slots__
3.2.1 __getattribute__:当访问任何属性时调用(无论属性是否存在)
class Person:
def __init__(self, name):
self.name = name
self.age = 20
def __getattribute__(self, attr):
print(f"__getattribute__ called with attr={attr}")
# 必须用super().__getattribute__()获取属性,否则会无限递归
return super().__getattribute__(attr)
# 测试
person = Person("张三")
print(person.name) # 输出:__getattribute__ called with attr=name → 张三
print(person.age) # 输出:__getattribute__ called with attr=age → 20
注意事项:
__getattribute__的优先级高于__getattr__—— 只有当__getattribute__抛出AttributeError时,才会调用__getattr__;- 在
__getattribute__中不能直接用self.attr,否则会无限递归; - 必须用 **
super().__getattribute__()或object.__getattribute__(self, attr)** 来获取属性。
3.2.2 __slots__:限制对象的属性
__slots__是类属性,用于限制对象只能拥有指定的属性,主要用于内存优化(避免为每个对象创建__dict__)。
class Person:
__slots__ = ("name", "age") # 限制只能拥有name和age属性
def __init__(self, name, age):
self.name = name
self.age = age
# 测试
person = Person("张三", 20)
# person.gender = "男" # 输出:AttributeError: 'Person' object has no attribute 'gender'
关键点:
__slots__的属性不包含__dict__,所以不能动态添加属性;- 如果需要动态添加属性,可以在
__slots__中包含"__dict__":__slots__ = ("name", "age", "__dict__"); __slots__只对当前类的实例有效,对子类无效(子类需要单独定义__slots__)。
四、运算魔术方法:重写 Python 的默认运算
这部分包含30 + 运算魔术方法,覆盖算术运算、比较运算、位运算等场景。
4.1 算术运算:__add__/__sub__/__mul__/__div__
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
# 加法运算:p1 + p2
def __add__(self, other):
if isinstance(other, Point):
return Point(self.x + other.x, self.y + other.y)
raise TypeError("必须和Point类型相加")
# 反向加法运算:2 + p1 → 自动调用other.__radd__(self)
def __radd__(self, other):
# 这里假设other是数值类型
return Point(self.x + other, self.y + other)
# 乘法运算:p1 * 2
def __mul__(self, scalar):
if isinstance(scalar, (int, float)):
return Point(self.x * scalar, self.y * scalar)
raise TypeError("必须和数值类型相乘")
# 反向乘法运算:2 * p1
def __rmul__(self, scalar):
return self.__mul__(scalar)
def __str__(self):
return f"Point(x={self.x}, y={self.y})"
# 测试
p1 = Point(1, 2)
p2 = Point(3, 4)
print(p1 + p2) # 输出:Point(x=4, y=6) → __add__
print(5 + p1) # 输出:Point(x=6, y=7) → __radd__
print(p1 * 2) # 输出:Point(x=2, y=4) → __mul__
print(2 * p1) # 输出:Point(x=2, y=4) → __rmul__
关键点:
- 普通运算方法(如
__add__)用于 “左操作数是当前类实例” 的场景; - 反向运算方法(如
__radd__)用于 “右操作数是当前类实例” 的场景; - 运算方法必须返回一个新的对象,不要修改当前对象(保持不可变性)。
4.2 比较运算:__eq__/__ne__/__lt__/__gt__/__le__/__ge__
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
# 相等比较:==
def __eq__(self, other):
if isinstance(other, Person):
return self.name == other.name and self.age == other.age
return False
# 小于比较:<
def __lt__(self, other):
if isinstance(other, Person):
return self.age < other.age
raise TypeError("必须和Person类型比较")
# 小于等于比较:<= → 如果没有定义,Python会自动用__lt__和__eq__实现
def __le__(self, other):
return self.__lt__(other) or self.__eq__(other)
def __str__(self):
return f"Person(name={self.name}, age={self.age})"
# 测试
p1 = Person("张三", 20)
p2 = Person("张三", 20)
p3 = Person("李四", 25)
print(p1 == p2) # 输出:True → __eq__
print(p1 < p3) # 输出:True → __lt__
print(p1 <= p2) # 输出:True → __le__
关键点:
- 必须同时定义
__eq__和__hash__(如果要将对象存入集合或作为字典的键),否则会导致不可预测的行为; - 比较运算方法必须返回布尔值;
- 推荐只定义
__eq__、__lt__和__le__,其他比较运算(__gt__、__ge__、__ne__)会由 Python 自动实现。
4.3 位运算:__and__/__or__/__xor__/__invert__
class BitSet:
def __init__(self, value):
self.value = value # 用整数表示位集合
# 位与运算:&
def __and__(self, other):
if isinstance(other, BitSet):
return BitSet(self.value & other.value)
return BitSet(self.value & other)
# 位或运算:|
def __or__(self, other):
if isinstance(other, BitSet):
return BitSet(self.value | other.value)
return BitSet(self.value | other)
# 位取反运算:~
def __invert__(self):
return BitSet(~self.value)
def __str__(self):
return f"BitSet({bin(self.value)})"
# 测试
b1 = BitSet(0b1010) # 1010
b2 = BitSet(0b1100) # 1100
print(b1 & b2) # 输出:BitSet(0b1000) → 1000
print(b1 | b2) # 输出:BitSet(0b1110) → 1110
print(~b1) # 输出:BitSet(-0b1011) → 取反(补码)
五、容器与迭代魔术方法:自定义容器
这部分包含20 + 容器与迭代魔术方法,覆盖序列、映射、迭代器等场景。
5.1 序列容器:__len__/__getitem__/__setitem__/__delitem__
要实现一个类似列表的序列容器,需要定义以下魔术方法:
class MyList:
def __init__(self):
self._data = [] # 用列表存储数据
# 获取长度:len(list)
def __len__(self):
return len(self._data)
# 获取元素:list[index]
def __getitem__(self, index):
return self._data[index]
# 设置元素:list[index] = value
def __setitem__(self, index, value):
self._data[index] = value
# 删除元素:del list[index]
def __delitem__(self, index):
del self._data[index]
# 追加元素
def append(self, value):
self._data.append(value)
# 测试
my_list = MyList()
my_list.append(1)
my_list.append(2)
my_list.append(3)
print(len(my_list)) # 输出:3 → __len__
print(my_list[0]) # 输出:1 → __getitem__
my_list[1] = 4 # __setitem__
print(my_list[1]) # 输出:4
del my_list[2] # __delitem__
print(len(my_list)) # 输出:2
for item in my_list: # 自动调用__getitem__和__len__实现迭代
print(item) # 输出:1、4
关键点:
- 实现
__len__和__getitem__后,容器就支持迭代(for循环、列表推导等); __getitem__支持切片:如果index是slice类型,可以返回切片后的新容器;
支持切片的__getitem__示例:
def __getitem__(self, index):
if isinstance(index, slice):
# 切片:返回新的MyList
new_list = MyList()
new_list._data = self._data[index]
return new_list
# 普通索引
return self._data[index]
# 测试
my_list = MyList()
my_list.append(1)
my_list.append(2)
my_list.append(3)
print(my_list[1:]) # 输出:<__main__.MyList object at 0x000001> → 可以进一步实现__str__来美化输出
5.2 映射容器:__contains__/__keys__/__values__/__items__
要实现一个类似字典的映射容器,需要在序列容器的基础上定义以下魔术方法:
class MyDict:
def __init__(self):
self._data = {} # 用字典存储数据
# 检查键是否存在:key in dict
def __contains__(self, key):
return key in self._data
# 获取键:dict[key]
def __getitem__(self, key):
return self._data[key]
# 设置键:dict[key] = value
def __setitem__(self, key, value):
self._data[key] = value
# 删除键:del dict[key]
def __delitem__(self, key):
del self._data[key]
# 获取所有键:dict.keys()
def keys(self):
return self._data.keys()
# 获取所有值:dict.values()
def values(self):
return self._data.values()
# 获取所有键值对:dict.items()
def items(self):
return self._data.items()
# 测试
my_dict = MyDict()
my_dict["name"] = "张三"
my_dict["age"] = 20
print("name" in my_dict) # 输出:True → __contains__
print(my_dict["name"]) # 输出:张三 → __getitem__
print(list(my_dict.keys())) # 输出:['name', 'age'] → keys()
print(list(my_dict.items())) # 输出:[('name', '张三'), ('age', 20)] → items()
5.3 迭代器:__iter__/__next__
要实现一个迭代器,需要定义__iter__(返回迭代器本身)和__next__(返回下一个元素,没有元素时抛出StopIteration)。
class MyIterator:
def __init__(self, data):
self._data = data
self._index = 0
# 返回迭代器本身
def __iter__(self):
return self
# 返回下一个元素
def __next__(self):
if self._index < len(self._data):
item = self._data[self._index]
self._index += 1
return item
# 没有元素时抛出StopIteration
raise StopIteration
# 测试
my_iter = MyIterator([1,2,3])
for item in my_iter:
print(item) # 输出:1、2、3
关键点:
- 迭代器是一次性的—— 一旦耗尽,就不能再使用;
- 可以用
iter()函数将容器转换为迭代器:iter(my_list); - 可以用
next()函数获取迭代器的下一个元素:next(my_iter)。
六、上下文管理魔术方法:__enter__/__exit__
上下文管理魔术方法用于自动管理资源(如文件、数据库连接等),是with语句的底层实现。
6.1 基础用法
class File:
def __init__(self, filename, mode):
self.filename = filename
self.mode = mode
# 进入上下文:with语句开始时调用
def __enter__(self):
self.f = open(self.filename, self.mode)
return self.f # 可以将资源对象返回给with语句
# 退出上下文:with语句结束时调用,无论是否有异常
def __exit__(self, exc_type, exc_val, exc_tb):
self.f.close()
# 如果返回True,会抑制异常;返回None或False,会传播异常
return False
# 测试
with File("test.txt", "w") as f:
f.write("Hello, World!") # 自动调用__enter__获取f,结束时自动调用__exit__关闭文件
__exit__的参数:
exc_type:异常类型(如果没有异常,为 None);exc_val:异常值(如果没有异常,为 None);exc_tb:异常栈跟踪(如果没有异常,为 None);
6.2 抑制异常示例
class File:
def __exit__(self, exc_type, exc_val, exc_tb):
self.f.close()
if exc_type is IOError:
# 抑制IOError异常
print(f"处理IOError:{exc_val}")
return True
return False
# 测试
with File("not_exist.txt", "r") as f:
f.read() # 抛出IOError,但被抑制
# 输出:处理IOError:[Errno 2] No such file or directory: 'not_exist.txt'
6.3 实用示例:数据库连接上下文管理器
import sqlite3
class Database:
def __init__(self, db_name):
self.db_name = db_name
def __enter__(self):
self.conn = sqlite3.connect(self.db_name)
self.cursor = self.conn.cursor()
return self.cursor # 返回游标
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type:
# 有异常,回滚
self.conn.rollback()
print(f"数据库回滚:{exc_val}")
else:
# 无异常,提交
self.conn.commit()
# 关闭连接
self.cursor.close()
self.conn.close()
# 测试
with Database("test.db") as cursor:
# 创建表
cursor.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)")
# 插入数据
cursor.execute("INSERT INTO users (name) VALUES ('张三')")
# 自动提交并关闭连接
七、魔术方法的工程化实践
7.1 单例模式(用__new__)
class Singleton:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
# 可以在__new__中初始化属性
cls._instance.init(*args, **kwargs)
return cls._instance
def init(self, name):
self.name = name
# 测试
s1 = Singleton("单例1")
s2 = Singleton("单例2") # 第二次调用不会初始化
print(s1.name) # 输出:单例1 → 因为第二次调用被忽略
print(s2.name) # 输出:单例1 → 确实是同一个实例
7.2 惰性加载(用__getattr__)
class LazyLoad:
def __init__(self):
self._data = None
def __getattr__(self, attr):
if attr == "data":
# 惰性加载:只有当访问data时才会加载
print("开始加载data...")
self._data = [1,2,3,4,5]
# 将data添加到__dict__中,下次访问时直接返回,不再调用__getattr__
self.__dict__["data"] = self._data
return self._data
raise AttributeError(f"'LazyLoad' object has no attribute '{attr}'")
# 测试
lazy = LazyLoad()
print(lazy.data) # 输出:开始加载data... → [1,2,3,4,5]
print(lazy.data) # 输出:[1,2,3,4,5] → 直接返回,不再加载
7.3 不可变对象(用__setattr__/__delattr__)
class Immutable:
def __init__(self, name, age):
self.__dict__["name"] = name
self.__dict__["age"] = age
def __setattr__(self, attr, value):
raise AttributeError("不可变对象,不能修改属性")
def __delattr__(self, attr):
raise AttributeError("不可变对象,不能删除属性")
# 测试
immutable = Immutable("张三", 20)
print(immutable.name) # 输出:张三
# immutable.age = 21 # 输出:AttributeError: 不可变对象,不能修改属性
# del immutable.name # 输出:AttributeError: 不可变对象,不能删除属性
八、50000 字总结
8.1 魔术方法的分类
| 分类 | 主要方法 |
|---|---|
| 对象生命周期 | __new__/__init__/__del__ |
| 对象表示 | __str__/__repr__/__format__ |
| 属性访问 | __getattr__/__setattr__/__delattr__/__getattribute__/__slots__ |
| 运算 | __add__/__sub__/__mul__/__div__/__eq__/__lt__/__le__ |
| 容器与迭代 | __len__/__getitem__/__setitem__/__delitem__/__iter__/__next__ |
| 上下文管理 | __enter__/__exit__ |
| 其他高级 | __call__/__hash__/__instancecheck__/__subclasscheck__ |
8.2 核心原则
- 不要滥用魔术方法:只在需要重写 Python 默认行为时使用;
- 遵循 Pythonic 原则:写出符合 Python 风格的代码;
- 保持向后兼容:确保魔术方法的实现与 Python 的版本兼容;
- 文档化:为魔术方法添加文档字符串,说明用途和参数;
- 测试:为魔术方法编写测试用例,确保功能正确。
8.3 工程化建议
- 单例模式:用
__new__实现; - 资源管理:用上下文管理器(
__enter__/__exit__)实现; - 属性控制:用
__getattr__/__setattr__实现惰性加载、属性验证; - 自定义容器:用
__len__/__getitem__实现; - 对象表示:同时定义
__str__和__repr__。
九、结语
魔术方法是 Python 最强大的特性之一,它让你可以深入 Python 的底层,重写默认行为,写出优雅的代码。但要注意:魔术方法不是 “魔法”,它只是 Python 解释器自动调用的普通方法—— 只有理解了它的原理和用法,才能真正发挥它的作用。
希望这篇 15000 字的指南能帮助你从 “知道” 魔术方法到 “熟练使用” 魔术方法,写出真正 “Pythonic” 的代码。


被折叠的 条评论
为什么被折叠?



