Python 数据模型
Python 有一个核心设计哲学:一致性。当你用了一段时间 Python 后,会发现这门语言的行为非常 predictable——列表、字符串、字典这些内置类型在很多场景下的表现是相似的。这种一致性不是巧合,而是源于 Python 的数据模型(Data Model)。
数据模型定义了 Python 内置类型的公共接口。比如 len(obj)、obj[key]、for x in obj 这些操作,背后都有一组以双下划线开头和结尾的特殊方法(special method)在支撑。当你在自己的类中实现这些特殊方法时,你的对象就能表现得像内置类型一样自然。
什么是特殊方法
特殊方法的名字格式是 __xxx__,比如 __len__、__getitem__、__repr__。Python 解释器在遇到特定语法时,会自动调用对应的特殊方法,而不是让你显式地调用它们。
例如:
len(obj)→ 调用obj.__len__()obj[key]→ 调用obj.__getitem__(key)str(obj)→ 调用obj.__str__()
特殊方法也叫 dunder method(dunder 是 "double under" 的缩写)。
示例:实现一个社团成员名单
假设你在管理一个社团的成员信息,想实现一个 TeamRoster 类,让它能像 Python 列表一样被使用:可以用 len() 查人数、用 [] 取成员、可以迭代、可以判断某个成员是否在名单中。
from collections import namedtuple
Member = namedtuple('Member', ['name', 'role'])
class TeamRoster:
def __init__(self):
self._members = []
def add(self, name, role):
self._members.append(Member(name, role))
def __len__(self):
return len(self._members)
def __getitem__(self, position):
return self._members[position]
只实现了 __len__ 和 __getitem__ 两个方法,这个类就已经支持了很多 Python 核心功能:
>>> team = TeamRoster()
>>> team.add("Alice", "算法")
>>> team.add("Bob", "前端")
>>> team.add("Carol", "后端")
>>> len(team)
3
>>> team[0]
Member(name='Alice', role='算法')
>>> team[-1]
Member(name='Carol', role='后端')
随机选取一个成员:
>>> from random import choice
>>> choice(team)
Member(name='Bob', role='前端')
切片也没问题:
>>> team[:2]
[Member(name='Alice', role='算法'), Member(name='Bob', role='前端')]
迭代和成员判断:
>>> for member in team:
... print(f"{member.name} ({member.role})")
Alice (算法)
Bob (前端)
Carol (后端)
>>> Member("Alice", "算法") in team
True
>>> Member("David", "设计") in team
False
TeamRoster 隐式继承了 object,但上面的功能不是靠继承来的,而是靠数据模型——通过实现 __len__ 和 __getitem__,Python 就知道这个类是一个序列,自动赋予了它迭代、切片、in 判断等能力。
如何使用特殊方法
基本原则
特殊方法是给 Python 解释器调用的,不是给你直接调用的。也就是说,你应该写 len(obj) 而不是 obj.__len__()。使用内置函数(len、str、iter 等)的好处是:
- 它们会为内置类型走捷径(比如
len(list)直接读取 C 结构体里的长度,比方法调用更快) - 它们提供了一致的接口,用户不需要记各种
.size()、.length()等方法名
不要自己发明新的特殊方法(比如 __foo__),Python 未来可能会用到这些名字。
模拟数值类型
假设我们要实现一个二维点 Point2D,支持 + 加法、* 标量乘法、abs() 求距离:
from math import hypot
class Point2D:
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def __repr__(self):
return f'Point2D({self.x!r}, {self.y!r})'
def __abs__(self):
return hypot(self.x, self.y)
def __bool__(self):
return bool(abs(self))
def __add__(self, other):
return Point2D(self.x + other.x, self.y + other.y)
def __mul__(self, scalar):
return Point2D(self.x * scalar, self.y * scalar)
用法:
>>> p1 = Point2D(3, 4)
>>> p2 = Point2D(1, 2)
>>> p1 + p2
Point2D(4, 6)
>>> abs(p1)
5.0
>>> p1 * 3
Point2D(9, 12)
>>> abs(p1 * 3)
15.0
字符串表示形式
__repr__ 返回的字符串应该准确、无歧义,最好能让人看了就知道怎么重建这个对象。上面的 Point2D 类,__repr__ 返回的是类似构造函数调用的形式,这是好习惯。
__str__ 则是给终端用户看的,应该更友好。如果只实现一个,优先实现 __repr__,因为当 __str__ 不存在时,Python 会回退到 __repr__。
>>> p = Point2D(3, 4)
>>> repr(p)
'Point2D(3, 4)'
>>> str(p)
'Point2D(3, 4)' # 因为没定义 __str__,回退到 __repr__
算术运算符
__add__ 和 __mul__ 实现了 + 和 * 运算符。注意中缀运算符的原则:不改变操作对象,而是返回新对象。p1 + p2 不会修改 p1 或 p2,而是创建一个新的 Point2D。
上面的实现只支持 point * 3(点乘标量),不支持 3 * point。要实现交换律,需要额外定义 __rmul__:
def __rmul__(self, scalar):
return self * scalar
自定义布尔值
默认情况下,自定义类的实例总被认为是 True。但你可以通过 __bool__ 来控制:
def __bool__(self):
return bool(abs(self))
如果类没有定义 __bool__,Python 会尝试调用 __len__,返回 0 则为 False,否则为 True。
一个更高效的实现(避免开方运算):
def __bool__(self):
return bool(self.x or self.y)
常用特殊方法一览
| 类别 | 方法名 | 说明 |
|---|---|---|
| 字符串表示 | __repr__、__str__、__format__、__bytes__ | 对象的字符串形式 |
| 数值转换 | __abs__、__bool__、__int__、__float__、__hash__ | 转为数值或布尔值 |
| 集合模拟 | __len__、__getitem__、__setitem__、__delitem__、__contains__ | 序列/映射行为 |
| 迭代 | __iter__、__reversed__、__next__ | for 循环、reversed() |
| 可调用 | __call__ | 让对象像函数一样被调用 |
| 上下文管理 | __enter__、__exit__ | with 语句 |
| 实例生命周期 | __new__、__init__、__del__ | 创建和销毁 |
| 属性管理 | __getattr__、__getattribute__、__setattr__、__delattr__ | 属性访问 |
| 运算符重载 | __add__、__sub__、__mul__、__truediv__ 等 | +、-、*、/ 等 |
| 比较运算 | __eq__、__ne__、__lt__、__le__、__gt__、__ge__ | ==、!=、<、<=、>、>= |
| 反向运算 | __radd__、__rsub__、__rmul__ 等 | 操作数交换位置时调用 |
| 增量赋值 | __iadd__、__isub__、__imul__ 等 | +=、-=、*= 等 |
为什么 len 不是方法
你可能会问:为什么 Python 用 len(obj) 而不是 obj.len()?
答案是效率。对于 str、list、bytes 等内置类型,CPython 会直接从 C 结构体中读取长度,完全不调用方法。len() 是一个极其常见的操作,必须足够快。
同时,得益于特殊方法的设计,你自定义的类也能用 len(),而不必实现 .len() 或 .size() 等方法。这种处理方式在内置类型的效率和语言的一致性之间找到了平衡。
小结
- 特殊方法(
__xxx__)让自定义对象融入 Python 的数据模型 - 通过实现少量特殊方法,你的类就能支持迭代、切片、运算符、
len()、str()等核心功能 - 使用内置函数(
len、str、iter)而不是直接调用特殊方法 - 优先实现
__repr__,它同时服务于调试和用户展示 - 中缀运算符应该返回新对象,不要修改操作数