一、什么是 __len__ 和 __getitem__?
__len__ 和 __getitem__ 是 Python 中用于实现**容器类型(Container)**的两个核心魔术方法。通过实现它们,你可以让自定义类的对象表现得像内置的列表、元组、字符串一样,支持 len() 函数、索引访问、切片操作,甚至可以被 for 循环遍历。
| | |
__len__(self) | | len(obj) |
__getitem__(self, key) | | obj[index]、obj[start:end]、for item in obj |
classMyList:
def__init__(self, items):
self._items = list(items)
def__len__(self):
returnlen(self._items)
def__getitem__(self, index):
returnself._items[index]
ml = MyList([10, 20, 30, 40])
print(len(ml)) # 4
print(ml[1]) # 20
print(ml[1:3]) # [20, 30]
for item in ml:
print(item) # 10 20 30 40
二、__len__ 详解
__len__ 方法应该返回一个非负整数,表示容器中元素的数量。它被内置函数 len() 调用。
2.1 基本用法
classBookCollection:
def__init__(self):
self._books = []
defadd(self, book):
self._books.append(book)
def__len__(self):
returnlen(self._books)
collection = BookCollection()
collection.add("Python入门")
collection.add("数据结构")
print(len(collection)) # 2
2.2 注意事项
- •
__len__ 必须返回一个整数(通常是 int 类型)。 - • 如果容器中的元素数量很大,
__len__ 应该高效(例如直接返回内部存储的长度)。
三、__getitem__ 详解
__getitem__ 方法允许对象通过方括号 [] 访问元素。它接收一个参数 key,这个参数可以是:
- • 切片对象(slice):访问子序列(如
obj[1:5:2])。 - • 其他类型:例如字典的键(但字典有自己的实现)。
3.1 支持整数索引
classMyArray:
def__init__(self, data):
self._data = list(data)
def__getitem__(self, index):
returnself._data[index]
arr = MyArray([5, 10, 15, 20])
print(arr[0]) # 5
print(arr[2]) # 15
print(arr[-1]) # 20(负数索引也支持)
3.2 支持切片操作
当使用切片时,key 参数会是一个 slice 对象。slice 对象有三个属性:start、stop、step。
classMyArray:
def__init__(self, data):
self._data = list(data)
def__getitem__(self, key):
ifisinstance(key, slice):
# 处理切片,返回一个新的 MyArray 实例
return MyArray(self._data[key])
else:
# 处理整数索引
returnself._data[key]
arr = MyArray([0, 1, 2, 3, 4, 5])
print(arr[2:5]) # MyArray 实例,包含 [2, 3, 4]
print(arr[1:5:2]) # [1, 3]
3.3 支持遍历(可迭代)
如果一个类实现了 __getitem__,并且能够处理从 0 开始的连续整数索引,那么它就可以被 for 循环遍历(当索引超出范围时,应该抛出 IndexError)。
classCountdown:
def__init__(self, start):
self.start = start
def__getitem__(self, index):
value = self.start - index
if value < 0:
raise IndexError("超出范围")
return value
c = Countdown(5)
for num in c:
print(num) # 5, 4, 3, 2, 1
四、组合使用:实现完整的序列协议
实现 __len__ 和 __getitem__ 后,你的类就拥有了序列类型的大部分能力:
- •
obj[start:stransform: translateY(step] —— 切片 - •
item in obj —— 成员检查(需要 __contains__ 或 fallback)
classSequenceExample:
def__init__(self, data):
self._data = list(data)
def__len__(self):
returnlen(self._data)
def__getitem__(self, key):
returnself._data[key]
seq = SequenceExample([10, 20, 30, 40])
print(10in seq) # True(通过 __getitem__ 逐个比较)
print(50in seq) # False
print(list(seq)) # [10, 20, 30, 40]
注意:in 运算符在没有 __contains__ 时会使用 __getitem__ 逐个检查,时间复杂度 O(n)。如果经常需要成员检查,建议额外实现 __contains__。
五、实战案例
5.1 简单的可迭代范围类
classMyRange:
def__init__(self, start, end, step=1):
self.start = start
self.end = end
self.step = step
def__len__(self):
# 计算元素个数
ifself.step > 0:
returnmax(0, (self.end - self.start + self.step - 1) // self.step)
else:
returnmax(0, (self.start - self.end - self.step - 1) // -self.step)
def__getitem__(self, index):
if index < 0:
# 支持负数索引,从后往前数
index = len(self) + index
if index < 0or index >= len(self):
raise IndexError("索引超出范围")
returnself.start + index * self.step
r = MyRange(2, 10, 2)
print(len(r)) # 4
print(r[0]) # 2
print(r[3]) # 8
print(r[-1]) # 8
for i in r:
print(i, end=' ') # 2 4 6 8
5.2 卡片牌组类
classCard:
def__init__(self, rank, suit):
self.rank = rank
self.suit = suit
def__repr__(self):
returnf"{self.rank}{self.suit}"
classDeck:
ranks = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"]
suits = ["♠", "♥", "♣", "♦"]
def__init__(self):
self._cards = [Card(rank, suit) for suit inself.suits for rank inself.ranks]
def__len__(self):
returnlen(self._cards)
def__getitem__(self, position):
returnself._cards[position]
deck = Deck()
print(len(deck)) # 52
print(deck[0]) # A♠
print(deck[-1]) # K♦
print(deck[10:15]) # 切片返回列表
for card in deck[:5]: # 遍历前5张
print(card, end=' ')
5.3 实现二维矩阵类
classMatrix:
def__init__(self, rows, cols, data=None):
self.rows = rows
self.cols = cols
if data:
self._data = list(data)
else:
self._data = [0] * (rows * cols)
def__getitem__(self, key):
ifisinstance(key, tuple):
row, col = key
returnself._data[row * self.cols + col]
else:
# 返回一行(简化处理)
row = key
return [self._data[row * self.cols + col] for col inrange(self.cols)]
def__setitem__(self, key, value):
ifisinstance(key, tuple):
row, col = key
self._data[row * self.cols + col] = value
else:
raise TypeError("需要使用 (row, col) 元组作为索引")
def__len__(self):
returnself.rows * self.cols
m = Matrix(3, 3)
m[0, 0] = 1
m[1, 1] = 2
m[2, 2] = 3
print(m[0, 0]) # 1
print(m[1]) # [0, 2, 0]
六、注意事项
6.1 索引越界
在 __getitem__ 中,当索引超出范围时,应该抛出 IndexError(而不是 ValueError 或其他异常)。
def__getitem__(self, index):
if index < 0or index >= len(self):
raise IndexError("索引超出范围")
returnself._data[index]
6.2 处理负数索引
通常应该支持负数索引(-1 表示最后一个元素)。可以这样做:
def__getitem__(self, index):
if index < 0:
index = len(self) + index
# 继续处理...
6.3 __getitem__ 的返回值类型
当 key 是整数时,返回单个元素;当 key 是切片时,通常返回一个新的同类型容器(或其子序列)。这取决于你的设计。
6.4 __len__ 的性能
__len__ 应该是一个 O(1) 的操作。如果需要计算长度,最好在内部维护一个长度变量,而不是每次都重新计算。
6.5 缺少 __iter__ 时的 fallback
如果类实现了 __getitem__ 但没有实现 __iter__,Python 会使用一个后备迭代器,从索引 0 开始依次调用 __getitem__,直到抛出 IndexError。这提供了基本的可迭代性,但效率不如显式实现 __iter__。
七、总结
- •
__len__:返回容器中元素的数量,被 len() 调用。 - •
__getitem__:支持索引访问和切片操作,被 obj[key] 调用。 - • 实现这两个方法后,自定义类可以获得类似序列的行为:支持
len()、索引、切片、迭代、成员检查等。 - • 实现
__getitem__ 时,应正确处理整数索引、负数索引和切片。
通过实现 __len__ 和 __getitem__,你可以让自己的类像内置的列表、字符串一样方便使用,这是 Python 中创建自定义容器类型的核心技巧。