直接继承list不算真正“自定义容器”,因为它未显式实现容器协议要求的__len__和__getitem__等方法,无法控制切片返回类型、拦截负索引或注入校验逻辑;必须手动实现这两个核心方法才能被len()、索引和切片等内置操作正确识别和调用。

为什么直接继承 list 不能算“自定义容器”
很多人第一反应是 class MyList(list): pass,但这只是包装,不是实现。Python 的容器协议(container protocol)要求你显式支持 __len__、__getitem__ 等方法,才能被内置函数和语法(如 len()、[1:5])正确识别和调用。继承 list 虽然能用,但掩盖了底层行为,比如你无法控制切片返回类型、无法拦截负索引逻辑、也无法在 len() 调用时插入校验或缓存逻辑。
必须实现的两个核心方法:__len__ 和 __getitem__
要让实例响应 len(obj) 和 obj[...](包括切片),至少得实现:
-
__len__(self):返回非负整数。注意不能返回 float 或 None,否则len()会报TypeError: 'float' object cannot be interpreted as an integer -
__getitem__(self, key):key 可能是int(单个索引)或slice对象(如slice(1, 5, 2))。必须自己判断并分别处理,不能只写self._data[key]就完事——因为_data可能不支持切片(比如是 dict 或生成器)
示例片段:
def __getitem__(self, key):
if isinstance(key, slice):
# 手动处理 start/stop/step,避免越界或负索引未归一化
start, stop, step = key.indices(len(self))
return [self[i] for i in range(start, stop, step)]
elif isinstance(key, int):
if key < 0:
key += len(self)
if not 0 <= key < len(self):
raise IndexError("index out of range")
return self._data[key]
else:
raise TypeError(f"index must be int or slice, not {type(key).__name__}")
切片返回什么类型?别默认返回 list
当你写 myobj[1:4],Python 期望返回一个同类型容器(比如 str[1:4] 返回 str,bytes[1:4] 返回 bytes),而不是硬编码成 list。否则会破坏接口一致性,下游代码用 isinstance(res, type(myobj)) 判断时就失效。
立即学习“Python免费学习笔记(深入)”;
- 如果内部用
list存储,切片应返回self.__class__(...)实例,而非list(...) - 如果内部是不可变结构(如 tuple),构造新实例时需确保构造函数接受可迭代对象
- 若切片逻辑复杂(比如带过滤或映射),更要主动控制返回类型,避免意外暴露底层数据结构
容易被忽略的边界:负索引、空切片、step=0
slice.indices(n) 是关键工具,它自动把 [::2]、[-3:]、[100:200] 这类表达式归一为合法的 (start, stop, step) 元组。不调用它,自己手动算负索引很容易出错:
- 直接用
key.start or 0忽略了None在切片中的语义(表示“从头/到尾”) - 没处理
step=0会导致ValueError: slice step cannot be zero,但这个错误应由slice.indices()抛出,不是你手动检查 - 空切片(如
[5:3])经.indices(n)后会得到(5, 3, 1),遍历时自然不执行,无需额外判断
所以务必依赖 key.indices(len(self)),别自己重造轮子。
真正麻烦的是当你的容器逻辑本身不连续(比如稀疏数组、懒加载序列),这时 __len__ 和 __getitem__ 的语义需要严格对齐——长度不能是“已加载项数”,而应是“逻辑总长”。这点一旦错位,切片和循环就会行为异常。


















