编程后端开发工程师

如何在Python中实现序列接口(Sequence)?实现特殊方法__getitem__、__len__的目的是什么,以及有什么潜在问题?

用 Hintsage AI 助手通过面试

回答。

问题背景:

序列是Python中最古老和最基本的概念之一。经典示例包括列表(list)、字符串(str)、元组(tuple)。为了与序列进行交互,定义了一个特殊的协议:需要实现__getitem____len__方法。这使得对象可以表现得像"序列",支持索引、切片、循环及某些标准函数。

问题:

如果没有正确实现这些方法,用户自定义类将无法进行索引操作、使用for循环或如len()这样的函数。初学者往往只实现其中一个方法,不处理异常情况,不支持切片(slice),导致出现不正确或意外的行为。

解决方案:

需要实现__getitem__(self, key)方法以支持索引和切片,以及__len__(self)方法以支持len()函数和正确的可迭代性。为了支持切片,需要在__getitem__中区分key的类型并正确处理slice对象。

示例代码:

def is_even(n): return n % 2 == 0 class EvenSequence: def __init__(self, size): self.size = size def __getitem__(self, index): if isinstance(index, slice): return [x for x in range(self.size)[index] if is_even(x)] if index < 0 or index >= self.size: raise IndexError('索引超出范围') return index if is_even(index) else None def __len__(self): return self.size

关键特性:

  • 如果实现了这两个方法,大多数Python函数和语法都可以正常工作。
  • 为了支持切片,必须在__getitem__中处理slice类型的对象。
  • 如果不实现__len__,对象将无法与len()及某些标准函数的完整列表正常工作。

陷阱问题。

只实现__getitem__,对象能否作为序列工作?

部分可以。如果只实现__getitem__,则可以通过for进行迭代和索引元素,但len()将无法使用。

示例代码:

class SeqOnlyGetitem: def __getitem__(self, index): if index >= 10: raise IndexError return index * 2 s = SeqOnlyGetitem() for x in s: print(x) # 工作正常(可迭代) # print(len(s)) # 错误 TypeError

如何处理负索引,若不考虑其后果会怎样?

负索引是Python序列的关键特性。如果不处理,对象的行为会让用户感到意外。

class NegIndex: def __init__(self, data): self.data = data def __getitem__(self, index): if index < 0: index += len(self.data) return self.data[index]

在自己的Sequence类中需要实现__contains__或__iter__吗?

不一定。如果实现了__getitem____len__,则in操作会正常工作,for会使用它们进行迭代。通常不需要直接实现这些方法,但可能会提高性能。

常见错误和反模式

  • 不检查索引范围,导致IndexError或意外结果。
  • 不实现切片(slice)支持,因此obj[2:10:2]会引发错误。
  • 忘记负索引(obj[-1])。

现实案例

负面案例

开发者仅为自己的类实现__getitem__,仅支持正索引。模块通过了一部分单元测试,但在实际使用中,试图获取不存在的索引或负索引时出现了问题。

优点:

  • 快速的初始实现。

缺点:

  • 实际使用中出现意外错误。
  • 使用不便(无法通过负索引获取最后的元素,不支持切片,不支持len())。

正面案例

团队实现了两个方法(getitem__和__len),考虑了切片、负索引,抛出了正确的异常。最终类在Python中的所有标准场景下工作良好。

优点:

  • 从Python API的角度看,行为可预测。
  • 操作便捷,错误最小。

缺点:

  • 代码稍多,设计时需要注意细节。