第 13 章 魔术方法(双下方法)全景
魔术方法(dunder methods)是 Python 的数据模型核心,让自定义类可以像内置类型一样使用各种运算符和语法。
13.1 对象表示:__repr__、__str__、__format__
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
"""开发者看的表示,尽量可以 eval 回原对象"""
return f"Point({self.x}, {self.y})"
def __str__(self):
"""用户看的表示,更友好"""
return f"({self.x}, {self.y})"
def __format__(self, spec):
"""支持 format() 和 f-string 格式规范"""
if spec == "r":
return f"({self.x:.2f}, {self.y:.2f})"
return str(self)
p = Point(3, 4)
print(repr(p)) # Point(3, 4) — 调用 __repr__
print(str(p)) # (3, 4) — 调用 __str__
print(p) # (3, 4) — print 调用 __str__
print(f"{p}") # (3, 4) — f-string 调用 __format__
print(f"{p:r}") # (3.00, 4.00) — 带格式规范
# 在容器中,元素用 __repr__ 显示
print([p, Point(1, 2)]) # [Point(3, 4), Point(1, 2)]
规则:
- 总是实现
__repr__,它是最基本的表示 __str__没实现时会回退到__repr____repr__应该尽量是无歧义的,最好可以eval()回原对象
13.2 比较运算:__eq__、__lt__、__hash__
from functools import total_ordering
@total_ordering # 只需定义 __eq__ 和 __lt__,自动补全其他比较方法
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
def __eq__(self, other):
if not isinstance(other, Temperature):
return NotImplemented # 让 Python 尝试反向比较
return self.celsius == other.celsius
def __lt__(self, other):
if not isinstance(other, Temperature):
return NotImplemented
return self.celsius < other.celsius
def __hash__(self):
return hash(self.celsius)
def __repr__(self):
return f"Temperature({self.celsius})"
t1 = Temperature(20)
t2 = Temperature(30)
t3 = Temperature(20)
print(t1 == t3) # True
print(t1 < t2) # True
print(t1 >= t2) # False — @total_ordering 自动实现
print(t2 > t1) # True
# 可哈希 → 可作字典键和集合元素
temps = {t1: "舒适", t2: "炎热"}
print(Temperature(20) in temps) # True
__eq__ 和 __hash__ 的关系:
- 定义了
__eq__但没定义__hash__,对象变成不可哈希(不能放入 set/dict) - 可变对象不应该实现
__hash__ __hash__必须保证:a == b→hash(a) == hash(b)
13.3 算术运算:__add__、__radd__、__iadd__
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Vector({self.x}, {self.y})"
# 正向运算: self + other
def __add__(self, other):
if isinstance(other, Vector):
return Vector(self.x + other.x, self.y + other.y)
if isinstance(other, (int, float)):
return Vector(self.x + other, self.y + other)
return NotImplemented
# 反向运算: other + self(当 other 不知道如何 + Vector 时调用)
def __radd__(self, other):
return self.__add__(other)
# 就地运算: self += other
def __iadd__(self, other):
if isinstance(other, Vector):
self.x += other.x
self.y += other.y
return self
return NotImplemented
# 一元运算
def __neg__(self): # -v
return Vector(-self.x, -self.y)
def __abs__(self): # abs(v)
return (self.x ** 2 + self.y ** 2) ** 0.5
# 标量乘法
def __mul__(self, scalar): # v * 3
return Vector(self.x * scalar, self.y * scalar)
def __rmul__(self, scalar): # 3 * v
return self.__mul__(scalar)
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2) # Vector(4, 6)
print(v1 + 10) # Vector(11, 12)
print(10 + v1) # Vector(11, 12) — 触发 __radd__
print(v1 * 3) # Vector(3, 6)
print(3 * v1) # Vector(3, 6) — 触发 __rmul__
print(-v1) # Vector(-1, -2)
print(abs(Vector(3, 4))) # 5.0
完整的算术方法表:
| 运算 | 正向 | 反向 | 就地 |
|---|---|---|---|
+ | __add__ | __radd__ | __iadd__ |
- | __sub__ | __rsub__ | __isub__ |
* | __mul__ | __rmul__ | __imul__ |
/ | __truediv__ | __rtruediv__ | __itruediv__ |
// | __floordiv__ | __rfloordiv__ | __ifloordiv__ |
% | __mod__ | __rmod__ | __imod__ |
** | __pow__ | __rpow__ | __ipow__ |
13.4 容器协议:__len__、__getitem__、__setitem__、__contains__
class Deck:
ranks = [str(n) for n in range(2, 11)] + list("JQKA")
suits = "♠♥♦♣"
def __init__(self):
self._cards = [f"{r}{s}" for s in self.suits for r in self.ranks]
def __len__(self):
return len(self._cards)
def __getitem__(self, index):
return self._cards[index]
def __setitem__(self, index, value):
self._cards[index] = value
def __delitem__(self, index):
del self._cards[index]
def __contains__(self, item):
return item in self._cards
def __iter__(self):
return iter(self._cards)
deck = Deck()
print(len(deck)) # 52
print(deck[0]) # 2♠
print(deck[-1]) # A♣
print(deck[0:3]) # ['2♠', '3♠', '4♠'] — 切片也能用!
print("A♠" in deck) # True
# __getitem__ 让对象可迭代(即使没有 __iter__)
for card in deck:
pass
# 还能用 random
import random
print(random.choice(deck)) # 随机一张牌
13.5 可调用对象:__call__
实现 __call__ 让实例像函数一样被调用:
class Adder:
def __init__(self, n):
self.n = n
def __call__(self, x):
return x + self.n
add5 = Adder(5)
print(add5(10)) # 15
print(add5(20)) # 25
# 检查对象是否可调用
print(callable(add5)) # True
print(callable(42)) # False
实用场景:带状态的函数:
class RateLimiter:
def __init__(self, max_calls, period):
self.max_calls = max_calls
self.period = period
self.calls = []
def __call__(self, func):
from functools import wraps
import time
@wraps(func)
def wrapper(*args, **kwargs):
now = time.time()
self.calls = [t for t in self.calls if now - t < self.period]
if len(self.calls) >= self.max_calls:
raise RuntimeError("调用频率超限")
self.calls.append(now)
return func(*args, **kwargs)
return wrapper
@RateLimiter(max_calls=3, period=60)
def api_request():
return "response"
13.6 上下文管理:__enter__、__exit__
class Timer:
def __enter__(self):
import time
self.start = time.perf_counter()
return self # with ... as xxx 中的 xxx
def __exit__(self, exc_type, exc_val, exc_tb):
import time
self.elapsed = time.perf_counter() - self.start
print(f"耗时: {self.elapsed:.4f}s")
return False # False: 不抑制异常; True: 抑制异常
with Timer() as t:
sum(range(1_000_000))
print(f"实际耗时: {t.elapsed:.4f}s")
class ManagedFile:
def __init__(self, filename, mode="r"):
self.filename = filename
self.mode = mode
self.file = None
def __enter__(self):
self.file = open(self.filename, self.mode)
return self.file
def __exit__(self, exc_type, exc_val, exc_tb):
if self.file:
self.file.close()
# 如果想抑制某种异常
if exc_type is FileNotFoundError:
print(f"文件 {self.filename} 不存在")
return True # 抑制异常
return False
with ManagedFile("test.txt", "w") as f:
f.write("hello")
13.7 属性访问:__getattr__、__getattribute__、__setattr__、__delattr__
class DynamicAttrs:
def __init__(self):
self._data = {}
def __getattr__(self, name):
"""访问不存在的属性时调用(最后的手段)"""
if name in self._data:
return self._data[name]
raise AttributeError(f"没有属性 '{name}'")
def __setattr__(self, name, value):
"""任何属性赋值都会调用"""
if name.startswith("_"):
super().__setattr__(name, value) # 内部属性正常处理
else:
self._data[name] = value
def __delattr__(self, name):
"""del obj.attr 时调用"""
if name in self._data:
del self._data[name]
else:
super().__delattr__(name)
obj = DynamicAttrs()
obj.name = "Alice" # 触发 __setattr__ → 存入 _data
print(obj.name) # 触发 __getattr__ → 从 _data 取
del obj.name # 触发 __delattr__
__getattr__ vs __getattribute__:
__getattribute__:每次属性访问都调用(包括存在的属性),慎用__getattr__:只在正常查找失败后调用(更安全、更常用)
class Logged:
def __getattribute__(self, name):
print(f"访问属性: {name}")
return super().__getattribute__(name) # 必须调 super(),否则无限递归
obj = Logged()
obj.x = 10
obj.x # 打印 "访问属性: x",返回 10
13.8 __slots__ 节省内存
默认情况下,Python 对象用 __dict__ 字典存储属性,灵活但占内存:
class RegularPoint:
def __init__(self, x, y):
self.x = x
self.y = y
class SlottedPoint:
__slots__ = ("x", "y") # 用固定结构代替 __dict__
def __init__(self, x, y):
self.x = x
self.y = y
import sys
r = RegularPoint(1, 2)
s = SlottedPoint(1, 2)
print(sys.getsizeof(r) + sys.getsizeof(r.__dict__)) # ~200 字节
print(sys.getsizeof(s)) # ~56 字节
# __slots__ 的限制
s.z = 3 # AttributeError! 不能添加未声明的属性
# RegularPoint 有 __dict__
print(r.__dict__) # {'x': 1, 'y': 2}
# SlottedPoint 没有 __dict__
# print(s.__dict__) # AttributeError
何时使用 __slots__:创建大量实例时(如数据点、数据库记录)。
13.9 __init_subclass__ 与 __set_name__
__init_subclass__ — 在子类创建时触发
class Plugin:
_registry = {}
def __init_subclass__(cls, plugin_name=None, **kwargs):
super().__init_subclass__(**kwargs)
name = plugin_name or cls.__name__.lower()
Plugin._registry[name] = cls
print(f"注册插件: {name}")
class ImagePlugin(Plugin, plugin_name="image"):
pass # 注册插件: image
class VideoPlugin(Plugin, plugin_name="video"):
pass # 注册插件: video
class AudioPlugin(Plugin):
pass # 注册插件: audioplugin
print(Plugin._registry)
# {'image': <class 'ImagePlugin'>, 'video': <class 'VideoPlugin'>, 'audioplugin': <class 'AudioPlugin'>}
__set_name__ — 描述符获知自己的属性名
class Validator:
def __set_name__(self, owner, name):
self.public_name = name
self.private_name = f"_{name}"
def __get__(self, obj, objtype=None):
return getattr(obj, self.private_name, None)
def __set__(self, obj, value):
self.validate(value)
setattr(obj, self.private_name, value)
def validate(self, value):
pass
class PositiveNumber(Validator):
def validate(self, value):
if value < 0:
raise ValueError(f"{self.public_name} 必须为正数,得到 {value}")
class Product:
price = PositiveNumber() # __set_name__ 自动获得 "price"
quantity = PositiveNumber() # __set_name__ 自动获得 "quantity"
def __init__(self, name, price, quantity):
self.name = name
self.price = price
self.quantity = quantity
p = Product("Widget", 9.99, 100)
# p.price = -1 # ValueError: price 必须为正数,得到 -1
本章小结:魔术方法是 Python 数据模型的精髓。通过实现不同的协议,自定义类可以完美融入 Python 的语法体系——支持运算符、迭代、上下文管理、属性访问等。记住
NotImplemented的用法、__repr__vs__str__的区别、__slots__的适用场景,这些是面试和实战中的高频知识点。