数据模型与魔法方法

What — 是什么

Python 数据模型(Data Model)是一组协议,定义了对象与语言核心交互的方式。魔法方法(Magic Methods,又称 dunder methods)是实现这些协议的特殊方法,形如 __xxx__,让自定义对象能像内置类型一样参与运算、迭代、上下文管理等操作。

核心概念:

  • 数据模型:Python 对象与语言基础设施(运算符、循环、with、len 等)的交互协议
  • 魔法方法:以双下划线包围的预定义方法(__init____repr____add__ 等),由解释器隐式调用
  • 协议(Protocol):一组相关魔法方法的集合,如迭代协议(__iter__ + __next__)、上下文协议(__enter__ + __exit__

关键特性:

  • 魔法方法不是直接调用,而是由语言构造触发(x + yx.__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 + v2v1.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)。代价是推导方法比手写多一次比较调用,性能略有损失。


相关链接: