跳到主要内容

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__()。使用内置函数(lenstriter 等)的好处是:

  1. 它们会为内置类型走捷径(比如 len(list) 直接读取 C 结构体里的长度,比方法调用更快)
  2. 它们提供了一致的接口,用户不需要记各种 .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 不会修改 p1p2,而是创建一个新的 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()

答案是效率。对于 strlistbytes 等内置类型,CPython 会直接从 C 结构体中读取长度,完全不调用方法。len() 是一个极其常见的操作,必须足够快。

同时,得益于特殊方法的设计,你自定义的类也能用 len(),而不必实现 .len().size() 等方法。这种处理方式在内置类型的效率语言的一致性之间找到了平衡。

小结

  • 特殊方法(__xxx__)让自定义对象融入 Python 的数据模型
  • 通过实现少量特殊方法,你的类就能支持迭代、切片、运算符、len()str() 等核心功能
  • 使用内置函数(lenstriter)而不是直接调用特殊方法
  • 优先实现 __repr__,它同时服务于调试和用户展示
  • 中缀运算符应该返回新对象,不要修改操作数