数据模型与魔法方法
What — 是什么
Python 数据模型(Data Model)是一组协议,定义了对象与语言核心交互的方式。魔法方法(Magic Methods,又称 dunder methods)是实现这些协议的特殊方法,形如
__xxx__,让自定义对象能像内置类型一样参与运算、迭代、上下文管理等操作。
核心概念:
- 数据模型:Python 对象与语言基础设施(运算符、循环、with、len 等)的交互协议
- 魔法方法:以双下划线包围的预定义方法(
__init__、__repr__、__add__等),由解释器隐式调用 - 协议(Protocol):一组相关魔法方法的集合,如迭代协议(
__iter__+__next__)、上下文协议(__enter__+__exit__)
关键特性:
- 魔法方法不是直接调用,而是由语言构造触发(
x + y→x.__add__(y)) - 合理实现魔法方法可以让自定义类拥有与内置类型一致的行为
__new__与__init__分离:__new__控制实例创建,__init__控制实例初始化__slots__禁用__dict__,节省内存并限制动态属性- 描述符协议(
__get__/__set__/__delete__)是 property、classmethod 的底层机制
运行机制:
- 内存模型:普通对象通过
__dict__存储属性;__slots__对象使用预分配的属性槽,无__dict__ - 执行模型:运算符/内置函数触发魔法方法查找,按 MRO 顺序在类中查找
- 查找机制:
obj.attr先查找数据描述符(类中定义了__get__+__set__的属性),再查实例__dict__,再查非数据描述符
类型系统:
- 魔法方法定义了类型的”行为接口”,相当于 Python 的 ad-hoc 多态
collections.abc模块用抽象方法声明协议要求(如Sequence要求__getitem__+__len__)- 类型提示中可用
Protocol定义结构化子类型
Why — 为什么
适用场景:
- 让自定义类支持运算符(
+、[]、==等) - 让对象可迭代、可上下文管理
- 控制对象创建过程(单例、不可变对象)
- 自定义属性访问行为(验证、惰性计算)
- 优化内存使用(
__slots__)
常用魔法方法分类:
| 分类 | 方法 | 触发方式 |
|---|---|---|
| 对象创建 | __new__、__init__、__del__ | ClassName()、del obj |
| 字符串表示 | __repr__、__str__、__format__ | repr()、str()、f"{obj}" |
| 比较运算 | __eq__、__lt__、__gt__、__le__、__ge__、__ne__ | ==、<、> 等 |
| 算术运算 | __add__、__sub__、__mul__、__truediv__ | +、-、*、/ |
| 反向算术 | __radd__、__rsub__ 等 | 右操作数优先 |
| 增量赋值 | __iadd__、__isub__ 等 | +=、-= 等 |
| 容器协议 | __len__、__getitem__、__setitem__、__contains__ | len()、[]、in |
| 迭代协议 | __iter__、__next__ | for x in obj |
| 上下文管理 | __enter__、__exit__ | with obj: |
| 属性访问 | __getattr__、__setattr__、__getattribute__、__delattr__ | obj.attr |
| 描述符 | __get__、__set__、__delete__ | 类属性访问 |
| 可调用 | __call__ | obj() |
优缺点:
- ✅ 优点:
- 让自定义类融入语言,代码更 Pythonic
- 运算符重载提升代码可读性(
v1 + v2比v1.add(v2)更直观) - 协议化设计,鸭子类型天然支持
- ❌ 缺点:
- 方法名不直观,新手难以发现和记忆
- 过度使用运算符重载降低代码可读性
__getattribute__容易造成无限递归
How — 怎么用
快速上手:自定义向量类
from math import hypot
class Vector:
__slots__ = ('_x', '_y') # 节省内存,禁止动态属性
def __init__(self, x=0, y=0):
self._x = x
self._y = y
def __repr__(self):
return f"Vector({self._x}, {self._y})"
def __abs__(self):
return hypot(self._x, self._y)
def __add__(self, other):
return Vector(self._x + other._x, self._y + other._y)
def __mul__(self, scalar):
return Vector(self._x * scalar, self._y * scalar)
def __eq__(self, other):
return (self._x, self._y) == (other._x, other._y)
def __bool__(self):
return bool(self._x or self._y)
v1 = Vector(3, 4)
v2 = Vector(1, 2)
print(v1 + v2) # Vector(4, 6)
print(abs(v1)) # 5.0
print(v1 * 3) # Vector(9, 12)
print(v1 == v2) # False
print(bool(v1)) # True
代码示例1:__new__ 与 __init__ 的区别 — 实现单例与不可变对象
# 单例模式:用 __new__ 控制实例创建
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, value):
self.value = value # 注意:每次调用都会重新初始化
s1 = Singleton(1)
s2 = Singleton(2)
print(s1 is s2) # True
print(s1.value) # 2(被第二次 __init__ 覆盖)
# 不可变对象:用 __new__ + __slots__ 实现
class ImmutablePoint:
__slots__ = ('_x', '_y')
def __new__(cls, x, y):
instance = super().__new__(cls)
object.__setattr__(instance, '_x', x)
object.__setattr__(instance, '_y', y)
return instance
def __setattr__(self, name, value):
raise AttributeError("ImmutablePoint is immutable")
@property
def x(self): return self._x
@property
def y(self): return self._y
def __repr__(self):
return f"Point({self._x}, {self._y})"
p = ImmutablePoint(1, 2)
# p.x = 5 # AttributeError: ImmutablePoint is immutable
代码示例2:描述符协议 — 实现类型验证属性
class Validated:
"""描述符:自动类型验证的属性"""
def __init__(self, name, expected_type, min_val=None, max_val=None):
self.name = name
self.expected_type = expected_type
self.min_val = min_val
self.max_val = max_val
def __set_name__(self, owner, name):
self.private_name = f'_{name}'
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.private_name, None)
def __set__(self, obj, value):
if not isinstance(value, self.expected_type):
raise TypeError(
f"{self.name} 期望 {self.expected_type.__name__},"
f"收到 {type(value).__name__}"
)
if self.min_val is not None and value < self.min_val:
raise ValueError(f"{self.name} 不能小于 {self.min_val}")
if self.max_val is not None and value > self.max_val:
raise ValueError(f"{self.name} 不能大于 {self.max_val}")
setattr(obj, self.private_name, value)
class Student:
name = Validated("name", str)
age = Validated("age", int, min_val=0, max_val=150)
score = Validated("score", (int, float), min_val=0, max_val=100)
def __init__(self, name, age, score):
self.name = name
self.age = age
self.score = score
s = Student("Alice", 20, 95)
# Student("Bob", -5, 80) # ValueError: age 不能小于 0
# Student("Eve", 25, 150) # ValueError: score 不能大于 100
代码示例3:上下文管理器与属性代理
# 自定义上下文管理器:计时器
import time
class Timer:
def __enter__(self):
self.start = time.perf_counter()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.elapsed = time.perf_counter() - self.start
print(f"耗时: {self.elapsed:.4f}s")
return False # 不吞异常
with Timer():
time.sleep(0.1)
# 耗时: 0.100Xs
# 属性代理:动态委托属性访问
class Proxy:
"""代理模式:拦截并转发属性访问"""
def __init__(self, obj):
self._obj = obj
def __getattr__(self, name):
"""只在正常属性查找失败时调用"""
return getattr(self._obj, name)
def __setattr__(self, name, value):
if name.startswith('_'):
super().__setattr__(name, value)
else:
setattr(self._obj, name, value)
def __repr__(self):
return f"Proxy({self._obj!r})"
class Real:
x = 10
def greet(self): return "hello"
proxy = Proxy(Real())
print(proxy.x) # 10(转发到 Real.x)
print(proxy.greet()) # hello(转发到 Real.greet)
常见问题与踩坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
__init__ 被重复调用 | __new__ 返回已有实例时 __init__ 仍执行 | 单例中用标记位避免重复初始化 |
__repr__ 缺失 | 只定义了 __str__,repr() 输出不友好 | 始终定义 __repr__,__str__ 可选 |
__eq__ 不加 __hash__ | 定义 __eq__ 后 Python 自动设 __hash__=None | 需要可哈希则显式定义 __hash__ |
__getattr__ 与 __getattribute__ 混淆 | 前者仅在查找失败时调用,后者每次都调用 | 需要拦截所有访问用后者,但要避免递归 |
__slots__ 继承问题 | 子类不继承父类 __slots__,子类仍有 __dict__ | 子类也定义 __slots__(可以为空) |
| 描述符不生效 | 描述符定义在实例上而非类上 | 描述符必须作为类属性,不能作为实例属性 |
| 运算符不对称 | 1 + obj 调用 int.__add__,返回 NotImplemented | 实现 __radd__ 处理反向运算 |
最佳实践
- 始终定义
__repr__,确保eval(repr(obj))可还原(或至少信息充分) - 实现
__eq__时同时实现__hash__,或显式设__hash__ = None - 优先用
@dataclass自动生成__init__/__repr__/__eq__等 __setattr__中修改属性用object.__setattr__(self, name, value)避免递归- 描述符优先用
__set_name__(Python 3.6+)自动获取属性名 __slots__在大量实例场景下节省内存(百万级实例可节省 40-50% 内存)
面试题
Q1: __new__ 和 __init__ 有什么区别?
__new__是类方法(虽未加 @classmethod),负责创建实例并返回,在__init__之前调用。__init__是实例方法,负责初始化已创建的实例,无返回值。__new__控制创建过程(可返回已有实例实现单例),__init__控制初始化过程。如果__new__不返回当前类的实例,__init__不会被调用。
Q2: __getattr__ 和 __getattribute__ 有什么区别?
__getattribute__在每次属性访问时都会调用(无条件拦截),__getattr__只在常规属性查找失败时才调用(兜底处理)。__getattribute__中访问self.xxx会造成无限递归,必须用object.__getattribute__(self, name)访问。绝大多数情况应优先用__getattr__。
Q3: 什么是描述符?property 的底层原理是什么?
描述符是实现了
__get__/__set__/__delete__中任一方法的类属性对象。分两种:数据描述符(实现了__set__或__delete__)优先级高于实例__dict__;非数据描述符(只有__get__)优先级低于实例__dict__。property正是数据描述符的实现:fget/fset/fdel分别映射到__get__/__set__/__delete__。
Q4: __slots__ 的作用和限制是什么?
__slots__声明类的固定属性集合,禁用__dict__和__weakref__,节省内存(每个实例少一个 dict 对象)并阻止动态添加属性。限制:1) 子类不受约束(除非子类也定义__slots__);2) 无法动态添加未声明的属性;3) 多重继承时所有父类__slots__的属性名不能重复;4) 不方便调试(没有__dict__可查看所有属性)。
Q5: 如何让自定义对象支持 for ... in 循环?
实现迭代协议:定义
__iter__返回迭代器对象(通常是 self),迭代器再定义__next__返回下一个值,抛出StopIteration终止。或者只实现__getitem__(从 0 开始按索引访问),Python 会自动创建迭代器。前者是标准迭代器模式,后者是序列协议的兼容方案。
Q6: 实现了 __eq__ 后为什么对象变得不可哈希?
Python 3 中,如果类定义了
__eq__但没有定义__hash__,解释器会自动将__hash__设为None,使对象不可哈希。原因是:相等的对象应该有相同的哈希值,如果__eq__的语义变了,默认的基于 id 的哈希就不再正确。如果确定对象是不可变的,可以手动定义__hash__。
Q7: 什么是数据描述符和非数据描述符?属性查找的优先级是什么?
数据描述符实现了
__set__或__delete__,非数据描述符只实现了__get__。属性查找优先级:数据描述符 > 实例__dict__> 非数据描述符 > 类__dict__。这解释了为什么 property(数据描述符)能覆盖实例属性,而普通方法(非数据描述符)不能。
Q8: functools.total_ordering 的作用是什么?
@total_ordering装饰器只需定义__eq__和一个比较方法(__lt__/__le__/__gt__/__ge__之一),就能自动推导出其余比较方法。例如只定义__eq__+__lt__,它自动生成__le__(not (a < b) and not (a != b))、__gt__(not a <= b)、__ge__(not a < b)。代价是推导方法比手写多一次比较调用,性能略有损失。
相关链接: