__getitem__ 必须同时处理负索引和切片,因为Python调用 x[i] 时无论 i 是 int 还是 slice 都只触发该方法,不会自动归一化负索引或解析切片;若未支持,会抛 TypeError 或 IndexError。

为什么 __getitem__ 必须同时处理负索引和切片
Python 在调用 x[i] 时,不管 i 是 int 还是 slice,都只走 __getitem__;它不会自动帮你转负索引或拆解切片。如果你只支持正整数下标,遇到 obj[-1] 或 obj[1:4] 就会抛 TypeError: 'MyList' object is not subscriptable 或更隐蔽的 IndexError。
实操建议:
- 在
__getitem__开头用isinstance(key, slice)分支处理切片,否则直接当索引处理 - 对整数索引,先用
len(self)归一化负索引:if key ,再校验范围 - 切片对象的
.start、.stop、.step都可能是None,别直接算 —— 用slice.indices(len(self))安全展开
如何让 in 操作符正常工作
in 默认调用 __contains__;如果没定义,Python 会退回到遍历 __iter__(再 fallback 到 __getitem__ 从 0 开始试),但这种 fallback 效率极低,且不支持自定义逻辑(比如按哈希查、或跳过某些字段)。
实操建议:
- 显式实现
__contains__(self, item),内部用你已有的数据结构做查找(如item in self._data) - 如果底层是 list,直接委托即可;如果是 dict 或 set,注意语义一致(
in查的是键还是值?) - 避免在
__contains__里触发完整遍历 —— 比如你的容器实际是稀疏数组,就别用for x in self: if x == item: return True
切片返回类型必须是同类实例
用户写 obj[2:5] 期望得到一个同类型的对象,而不是 list。否则链式操作会断掉:obj[2:5].append(x) 失败,或 type(obj[2:5]) is not type(obj) 导致类型检查报错。
实操建议:
- 在
__getitem__的 slice 分支中,构造并返回self.__class__(sub_data),而非sub_data本身 - 确保你的类构造函数能接受可迭代对象(如
list或tuple)初始化内部存储 - 如果切片步长不是 1(如
[::2]),注意新实例的长度可能不等于(stop - start) // step,要按实际提取结果构造
容易被忽略的边界:空容器与 step=-1
空容器的 __len__ 返回 0,但 slice.indices(0) 仍会返回 (0, 0, 1) —— 这没问题;真正容易崩的是 step=-1 切片(如 [::-1]),它要求你的数据结构支持反向访问,且返回的新实例必须保持行为一致。
实操建议:
- 在切片处理分支中,拿到
start, stop, step = s.indices(len(self))后,别手动range(start, stop, step)—— 要用self._data[start:stop:step](如果底层是 list)或等价逻辑 - 测试用例至少覆盖:
MyList([])[0:]、MyList([1,2,3])[::-1]、MyList([1,2,3])[-100:100] - 如果底层不是 list(比如是链表),
__getitem__对切片的实现成本可能很高,这时要考虑是否真要支持任意切片,还是只支持常见模式
真正的难点不在写通,而在让所有边界组合(空 + 负索引 + 反向切片 + in 查找)都不意外崩掉。多数人卡在 slice.indices() 的返回值没接住,或者忘了 contains 不是可选的。










