17370845950

如何实现一个支持负索引和切片的自定义序列类
__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] 返回 listmytuple[::2] 返回 tuple。同理,你的自定义类 MySeq 的切片也应返回 MySeq 实例,而不是 listtuple。否则链式操作(如 obj[1:5][::-1])会意外降级为内置类型,丢失自定义行为。

实操建议:

  • 在切片分支中,用 type(self)(new_data) 构造新实例,而非 list(...)
  • 如果内部数据不可直接复用(比如是懒加载的),切片应封装参数(起止、步长),延迟到后续访问时才计算
  • 特别注意空切片:当 start >= stop(正向)或 start (负步长)时,应返回空实例,而非报错

__len__ 和内部存储不一致会导致切片越界静默失败

常见坑:内部用 list 存数据,但 __len__ 返回了错误值(比如缓存未更新)。这时 slice.indices(len) 计算出的索引范围会

错,导致取到错误元素、越界、或漏掉末尾项,且不报错。

实操建议:

  • 所有修改内部数据的方法(如 appendpop)必须同步更新长度相关状态,或干脆每次 __len__ 都实时计算(只要代价可控)
  • 测试时重点覆盖边界场景:obj[-1]obj[:-1]obj[100:](越界)、obj[::2]obj[::-1]
  • 别依赖 collections.abc.Sequence 的默认 __contains__index——它们底层会调用 __getitem__,如果切片/索引逻辑有缺陷,这些方法也会出错

负索引和切片看似简单,真正难的是让所有边界情况下的行为和内置类型完全对齐——尤其是空序列、全负切片、超大步长这些场景,很容易被忽略。