当前位置:首页>python>Python魔术方法——`__len__` 与 `__getitem__`:让对象像序列一样工作

Python魔术方法——`__len__` 与 `__getitem__`:让对象像序列一样工作

  • 2026-09-02 18:51:21
Python魔术方法——`__len__` 与 `__getitem__`:让对象像序列一样工作

一、什么是 __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 类型)。
  • • 返回值应该大于等于 0(负数没有意义)。
  • • 如果容器中的元素数量很大,__len__ 应该高效(例如直接返回内部存储的长度)。

三、__getitem__ 详解

__getitem__ 方法允许对象通过方括号 [] 访问元素。它接收一个参数 key,这个参数可以是:

  • • 整数索引:访问单个元素(如 obj[2])。
  • • 切片对象(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__ 后,你的类就拥有了序列类型的大部分能力:

  • • len(obj) —— 获取长度
  • • obj[index] —— 索引访问
  • • obj[start:stransform: translateY(step] —— 切片
  • • for item in obj —— 迭代
  • • item in obj —— 成员检查(需要 __contains__ 或 fallback)
  • • list(obj) —— 转换为列表
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__ 时,应正确处理整数索引、负数索引和切片。
  • • 索引越界时抛出 IndexError。

通过实现 __len__ 和 __getitem__,你可以让自己的类像内置的列表、字符串一样方便使用,这是 Python 中创建自定义容器类型的核心技巧。

最新文章

随机文章