Python 数据模型的根基是「一切皆对象」加协议驱动:对象由身份、类型、值三要素构成,序列按容器/扁平 × 可变/不可变两轴分类,切片、解包、for 遍历全部建立在这套协议之上。

对象三要素

数字、函数、类本身都是对象,绝大多数类型最终锚定在 Python 关键字 object 上。每个对象有三要素:

  • 身份(identity):唯一标识,id() 可读,CPython 中即内存地址;is 比较的就是它,终身不变
  • 类型(type):决定对象支持的操作与取值范围,type() 可读,不可变
  • 值(value):对象承载的数据;能否原地修改由类型决定

身份与类型生来不变,值是否可变因类型而异——这条 mutable / immutable 分界线贯穿赋值、传参与拷贝语义(引用与可变性)。

内建序列两轴分类

容器序列(存对象引用)扁平序列(存值,连续内存)
可变listbytearray、array.array、memoryview
不可变tuplestr、bytes
  • 容器序列存引用:嵌套结构的子对象可被共享,浅拷贝因此埋雷
  • 扁平序列一段连续内存只放标量:紧凑省空间,但类型受限
  • collections.deque 属容器可变,双端 O(1)(collections)

序列协议:len 与 getitem

对象无需继承任何基类,实现协议即可被当作序列(特殊方法 协议成员):

class Sentence:
    def __init__(self, text):
        self.words = text.split()
 
    def __len__(self):
        return len(self.words)
 
    def __getitem__(self, i):
        return self.words[i]
 
s = Sentence("hello fluent python")
len(s)      # 3
s[0]        # 'hello'
for w in s: ...   # 甚至不写 __iter__ 就能迭代

解释器按协议分发:len() 走 __len__,下标、切片、in、for 全部经由 __getitem__。只实现 __getitem__ 的对象也能被 for 遍历——解释器从 0 逐个索引直到 IndexError,完整机制见 迭代器协议。这是 duck typing 的核心:行为可辨识即类型,继承只是复用手段。

切片语义

seq[a:b:c] 在运行时构造 slice(a, b, c) 对象传给 __getitem__,自定义序列只需处理 slice 类型参数即免费获得切片支持:

s = "abcdefghij"
s[2:8:2]   # 'cegi',等价于 s[slice(2, 8, 2)]
s[::-1]    # 'jihgfedcba',负步长反转;步长为负时 start/stop 缺省方向反转
s[99:]     # 越界不报错——切片宽容,索引 s[99] 才 IndexError
  • 切片产生新对象、不动原序列;lst[1:3] = [...] 切片赋值才是原地修改,且长度可伸缩
  • 多维下标 a[i, j] 实际传入 tuple (i, j);...(Ellipsis)不是内建序列的合法索引,是 NumPy 等库约定的「其余维度」占位符

序列解包与星号赋值

a, b = b, a                  # 交换,无临时变量
first, *rest = [1, 2, 3, 4]  # first=1, rest=[2, 3, 4]
*init, last = range(5)       # init=[0, 1, 2, 3], last=4
(a, (b, c)) = (1, (2, 3))    # 嵌套解包按结构匹配
for i, (x, y) in enumerate(points): ...   # 解包直接用于 for 目标
  • 带 * 的名字收集剩余元素为 list(无论原序列什么类型),一条赋值语句最多一个
  • 函数侧 *args 收集、调用侧 f(*seq) 展开,是同一思想在函数协议上的延伸

协议优先于继承,是数据模型的品味所在:实现 __len__/__getitem__,len()、切片、in、for 全部免费;再实现 __iter__/__next__,整个惰性世界接入。写自定义容器先问「接上哪个协议」,而不是「继承哪个类」。