怎么通过__getitem__和__len__自定义Python序列类型

桃夭
桃夭
发布时间:2026-11-09 | 11人浏览过
原创

本文详细讲解如何在Python中通过实现__len__和__getitem__魔术方法自定义不可变序列类型。包含序列协议底层原理、负索引与切片计算规范、代码示例以及与单方法实现的差异对比。

怎么通过__getitem__和__len__自定义Python序列类型
自定义 Python 序列类型概述:通过 __len__ 与 __getitem__ 构建完整序列对象

在 Python 中,让一个自定义类表现得像内置列表(list)或元组(tuple)那样支持下标访问、求长度与循环遍历,核心方法就是实现 __len__ 与 __getitem__ 两个特殊魔术方法。得益于 Python 的“鸭子类型”(Duck Typing)机制,你的类无需继承任何标准基类,只要按规范交付这两个方法,解释器就会将其认定为合法的不可变序列(Immutable Sequence),自动赋予索引查找、切片操作、for 循环迭代、in 包含检测以及 reversed() 逆序处理等全部语言特权。

一、Python 序列协议的运作原理

在 Python 数据模型(Data Model)中,序列协议是由一组约定好的特殊方法构成的非正式接口。要实现一个最小可用的不可变序列类型,必须满足两项核心契约:

  • __len__(self) 契约:必须返回一个非负整数,代表序列中元素的总数。该方法直接响应外部的 len(obj) 调用。若序列未显式定义 __bool__,Python 会在真值测试中调用 __len__,当长度大于 0 时判定为真,为 0 时判定为假。
  • __getitem__(self, index) 契约:必须能够根据传入的 index 返回对应的序列项。如果传入的索引超出合法范围,必须抛出标准的 IndexError;如果传入的索引类型既不是整数也不是切片对象,必须抛出 TypeError。
怎么通过__getitem__和__len__自定义Python序列类型
图 1:Python 序列语法糖与底层魔术方法映射架构

如上图所示,当在代码中使用 for item in seq 遍历一个没有实现 __iter__ 方法的对象时,Python 解释器会自动触发序列回退机制:从整数索引 0 开始,依次调用 __getitem__(0)、__getitem__(1)……直到 __getitem__ 抛出 IndexError,解释器捕获此异常并将其转化为 StopIteration,平稳结束循环。这也是为什么越界时必须抛出 IndexError 而非返回 None 的原因——如果返回 None,for 循环将无法感知终止边界,导致陷入死循环。

二、编写最小可用序列:代理内置容器

最直接的自定义序列方式是在内部持有一个内置列表或元组,并将长度与元素访问请求委托给内部容器。以下是一个保存扑克牌组的不可变序列示例:

import collections

Card = collections.namedtuple('Card', ['rank', 'suit'])

class FrenchDeck:
    ranks = [str(n) for n in range(2, 11)] + list('JQKA')
    suits = ['黑桃', '红心', '梅花', '方块']

    def __init__(self):
        # 在内存中构造扑克牌列表
        self._cards = [Card(rank, suit) for suit in self.suits
                                        for rank in self.ranks]

    def __len__(self):
        # 满足序列协议:返回元素总数
        return len(self._cards)

    def __getitem__(self, position):
        # 委托给内部 list:自动继承负索引与切片能力
        return self._cards[position]

在这个例子中,只需编写两个特殊方法,该类立刻具备了丰富的语言特性:

# 实例化扑克牌序列
deck = FrenchDeck()

# 1. 响应 len()
print(f"牌组总数: {len(deck)}")

# 2. 支持正负索引与切片
print(f"第一张牌: {deck[0]}")
print(f"最后一张: {deck[-1]}")
print(f"前三张牌: {deck[:3]}")

# 3. 支持 for 循环与 random.choice
import random
print(f"随机抽牌: {random.choice(deck)}")
print(f"包含检测: {Card('A', '黑桃') in deck}")
示例运行输出(示例)
牌组总数: 52
第一张牌: Card(rank='2', suit='黑桃')
最后一张: Card(rank='A', suit='方块')
前三张牌: [Card(rank='2', suit='黑桃'), Card(rank='3', suit='黑桃'), Card(rank='4', suit='黑桃')]
随机抽牌: Card(rank='7', suit='红心')
包含检测: True

三、深层把控:严谨处理负索引、切片与类型校验

当自定义序列并非简单代理已有列表,而是按需动态生成数据(例如惰性数学序列、磁盘分页数据或矩阵结构)时,我们不能直接依赖外部列表的切片机制。此时,必须在 __getitem__ 内部显式解析传入的参数。

怎么通过__getitem__和__len__自定义Python序列类型
图 2:__getitem__(self, index) 的参数解析与边界分发步骤

在处理更严谨的自定义序列时,需要遵循以下 3 个关键步骤:

  1. 区分整数与切片类型:使用 isinstance(index, int) 与 isinstance(index, slice) 进行类型判断。如果传入既非整数也非切片,主动抛出 TypeError。
  2. 整数索引规整与越界检查:如果 index < 0,必须将其转换为非负索引:index += len(self)。若计算后仍小于 0 或大于等于序列总长度,立即抛出 IndexError("序列索引越界")。
  3. 切片对象标准化(slice.indices()):Python 内置的 slice 对象提供了一个原生方法 slice.indices(len)。它能自动处理负步长、省略的 None 以及超大边界,计算出完全符合 Python 规范的 (start, stop, step) 三元组。

以下是一个动态计算等差数列的完整实现,展示了如何在不依赖内置列表切片的情况下完整支撑标准序列操作:

class ArithmeticSequence:
    """无需存储全部数据的只读等差数列自定义序列"""
    def __init__(self, start, step, length):
        if length < 0:
            raise ValueError("序列长度不能为负数")
        self._start = start
        self._step = step
        self._len = length

    def __len__(self):
        return self._len

    def __getitem__(self, index):
        if isinstance(index, int):
            # 1. 负索引规整化
            if index < 0:
                index += self._len
            # 2. 边界合规校验
            if index < 0 or index >= self._len:
                raise IndexError("ArithmeticSequence 索引超出范围")
            # 动态即时计算元素
            return self._start + index * self._step

        elif isinstance(index, slice):
            # 3. 使用 slice.indices 标准化切片区间
            start, stop, step = index.indices(self._len)
            # 按照切片参数动态构造子序列并返回本类实例
            sliced_items = [self[i] for i in range(start, stop, step)]
            return sliced_items

        else:
            raise TypeError(
                f"序列索引必须是整数或切片,不能是 {type(index).__name__}"
            )

    def __repr__(self):
        return f"ArithmeticSequence(start={self._start}, step={self._step}, len={self._len})"
重要注意事项:切片操作的返回值类型设计
在设计自定义序列时,切片返回什么对象取决于应用场景:若希望保持封闭性与链式操作能力,切片应返回原序列类的全新实例(如 self.__class__(...));若为了通用性与轻量化,也可以像上述代码一样返回 Python 原生列表。切忌直接返回内部未受保护的数据引用。

四、关键差异分析:仅实现 __getitem__ 对比同时实现两者

不少初学者会发现,仅仅实现 __getitem__ 且不定义 __len__ 时,对象依然能够执行下标索引和 for 循环。但在严谨的工程实践中,这两种做法在行为一致性与语义契约上存在本质差异。

怎么通过__getitem__和__len__自定义Python序列类型
图 3:仅实现 __getitem__ 与同时实现 __getitem__ + __len__ 的关键差异
特性行为 仅实现 __getitem__ 同时实现 __getitem__ 与 __len__
协议归属 仅构成可迭代回退对象(Iterable Fallback) 正式契合不可变序列协议(Immutable Sequence)
len(obj) 求长 直接崩溃:抛出 TypeError: object has no len() 原生支持:O(1) 获取当前序列真实大小
反向迭代 reversed() 崩溃:报 TypeError: '...' object is not reversible 原生支持:解释器从 len - 1 递减逐个取项
布尔真值测试 bool(obj) 恒为 True(即便序列内部没有任何元素) 依据 len(obj) > 0 判定,空序列正确判定为 False
标准库工具协作 random.choice(obj) 无法运行,抛出错误 与标准库所有序列工具与泛型算法无缝兼容
抽象基类检测 无法被认定为序列结构 满足 collections.abc.Sequence 契约要求

这种差异之所以关键,是因为缺少 __len__ 会在隐式逻辑中埋下隐患。例如,很多业务代码依赖 if not seq: 来判断序列是否为空;若只实现了 __getitem__,哪怕序列是空的,该条件判断也永远无法成立。因此,定义严谨的序列类型时,必须成对提供 __len__ 与 __getitem__。

五、进阶实践:继承 collections.abc.Sequence

虽然通过鸭子类型我们已经拥有了序列的核心功能,但在生产环境中,更推荐的做法是继承标准库的抽象基类 collections.abc.Sequence。

from collections.abc import Sequence

class ColorPalette(Sequence):
    """继承 Sequence 抽象基类的调色板不可变序列"""
    def __init__(self, colors):
        self._colors = tuple(colors)

    def __len__(self):
        return len(self._colors)

    def __getitem__(self, index):
        return self._colors[index]

继承 Sequence 抽象基类的价值主要体现在两点:

  • 接口强制保证:如果忘记编写 __len__ 或 __getitem__,在类实例化时就会直接报错,防止残缺对象流入运行时。
  • 免费混入方法(Mixin Methods):抽象基类会自动基于这两个方法提供高度优化的 count()、index()、__contains__() 以及 __reversed__() 方法,无需手动实现即可直接调用。

六、知识梳理与核心要点总结

为便于随时查阅与复用,下方的总结图谱系统性地归纳了自定义序列的四层结构:

怎么通过__getitem__和__len__自定义Python序列类型
图 4:自定义 Python 序列实现要点与体系知识总结

通过本文的讲解与实践,可以得出自定义 Python 序列的清晰路径:

  1. 确立协议:始终同时实现 __len__ 与 __getitem__,保证对象在取长、索引、切片、反转及真值测试中的一致性。
  2. 防御边界:索引越界时务必严格抛出 IndexError,这是驱动 for 循环正常终止的信号基石;切片计算推荐使用 slice.indices()。
  3. 进阶选型:只读序列优先继承 collections.abc.Sequence 获得现成的常用算法;如需支持元素修改与删除(可变序列),则进一步补全 __setitem__ 与 __delitem__。
相关专题:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表