__getitem__ 必须同时处理 int 和 slice:对 int 调用 _normalize_index 转负索引,对 slice 用 _normalize_slice 标准化边界并返回新序列实例。

Python 中 __getitem__ 必须同时处理 int 和 slice
自定义序列类要支持负索引和切片,核心是正确实现 __getitem__。它不能只假设传入的是 int——当写 obj[1:4] 或 obj[-2:] 时,Python 会传入一个 slice 对象,不是整数。如果没做类型分发,直接对 slice 做算术运算会抛 TypeError: unsupported operand type(s) for +: 'slice' and 'int'。
实操建议:
- 在
__getitem__开头用isinstance(key, slice)分支处理切片逻辑 - 对
int类型的key,先用self._normalize_index(key)统一转为非负索引(避免重复写负数逻辑) - 不要在
__getitem__里手动调用range()或拼列表——切片可能很大,应返回新序列实例或惰性对象(如自定义迭代器)
负索引转换必须基于当前长度动态计算
负索引(如 -1)的意义依赖于序列当前长度,不能硬编码偏移量。比如 myseq[-1] 应等价于 myseq[len(myseq)-1],而 len(myseq) 可能随内部数据变化。
实操建议:
- 实现
__len__(self)并确保它返回真实、及时的长度 - 写一个私有方法
_normalize_index(self, i),内部用i % len(self)或i + len(self) if i 转换;注意前者对i = -0和边界值更鲁棒 - 在
__getitem__中对整数索引统一调用该方法,切片的.start/.stop/.step也要分别归一化(slice.indices(len)是标准解法)
切片返回值类型需与原序列一致
用户期望 mylist[1:3] 返回 list,mytuple[::2] 返回 tuple。同理,你的自定义类 MySeq 的切片也应返回 MySeq 实例,而不是 list 或 tuple。否则链式操作(如 obj[1:5][::-1])会意外降级为内置类型,丢失自定义行为。
实操建议:
- 在切片分支中,用
type(self)(new_data)构造新实例,而非list(...) - 如果内部数据不可直接复用(比如是懒加载的),切片应封装参数(起止、步长),延迟到后续访问时才计算
- 特别注意空切片:当
start >= stop(正向)或start (负步长)时,应返回空实例,而非报错
__len__ 和内部存储不一致会导致切片越界静默失败
常见坑:内部用 list 存数据,但 __len__ 返回了错误值(比如缓存未更新)。这时 slice.indices(len) 计算出的索引范围会错,导致取到错误元素、越界、或漏掉末尾项,且不报错。
实操建议:
- 所有修改内部数据的方法(如
append、pop)必须同步更新长度相关状态,或干脆每次__len__都实时计算(只要代价可控) - 测试时重点覆盖边界场景:
obj[-1]、obj[:-1]、obj[100:](越界)、obj[::2]、obj[::-1] - 别依赖
collections.abc.Sequence的默认__contains__或index——它们底层会调用__getitem__,如果切片/索引逻辑有缺陷,这些方法也会出错
负索引和切片看似简单,真正难的是让所有边界情况下的行为和内置类型完全对齐——尤其是空序列、全负切片、超大步长这些场景,很容易被忽略。










