6.8 KiB
doc_type, full_text
| doc_type | full_text |
|---|---|
| short | sources/01_Iteration_protocol.md |
01_Iteration_protocol 总结
本文介绍 Python 中无处不在的 Python迭代协议,解释 for 循环背后的底层机制,并通过 Portfolio 示例说明如何让自定义对象表现得像标准容器。
核心内容
1. 迭代无处不在
Python 中许多对象都支持迭代,包括:
- 字符串:逐字符迭代
- 字典:默认逐键迭代
- 列表、元组:逐元素迭代
- 文件对象:逐行迭代
示例:
for x in obj:
...
这种统一的使用方式来自 Python 的迭代协议。
2. for 循环背后的机制
for 语句本质上会执行以下步骤:
_iter = obj.__iter__()
while True:
try:
x = _iter.__next__()
# statements
except StopIteration:
break
关键点:
obj.__iter__()返回一个迭代器对象。- 迭代器通过
__next__()逐个返回元素。 - 当没有更多元素时,抛出
StopIteration。 for循环会自动捕获StopIteration并结束循环。
这说明所有能用于 for 循环的对象都实现了底层的 Python迭代协议。
3. 手动迭代
文档通过列表演示了手动调用迭代器:
a = [1, 9, 4, 25, 16]
i = a.__iter__()
i.__next__()
连续调用 __next__() 会依次得到列表元素,直到列表耗尽并抛出 StopIteration。
Python 内置函数 next() 是调用迭代器 __next__() 方法的简写:
next(i)
文件对象也是迭代器的一种典型示例。对文件调用 next(f) 会逐行读取内容;文件读到末尾时同样抛出 StopIteration。
4. 让自定义对象支持迭代
如果自定义对象内部包装了列表或其他可迭代对象,可以通过实现 __iter__() 让它支持 for 循环。
示例:
class Portfolio:
def __init__(self, holdings):
self._holdings = holdings
def __iter__(self):
return self._holdings.__iter__()
这里 Portfolio 是对持仓列表的封装。通过把迭代行为委托给内部列表 _holdings,Portfolio 实例就可以像列表一样被遍历:
for s in portfolio:
...
这体现了 对象封装 与 Python特殊方法 的结合:对象可以隐藏内部数据结构,同时暴露符合 Python 习惯的操作接口。
5. Portfolio 示例:从列表封装到可迭代容器
文档要求创建 portfolio.py,定义 Portfolio 类:
class Portfolio:
def __init__(self, holdings):
self._holdings = holdings
@property
def total_cost(self):
return sum([s.shares * s.price for s in self._holdings])
def tabulate_shares(self):
from collections import Counter
total_shares = Counter()
for s in self._holdings:
total_shares[s.name] += s.shares
return total_shares
随后修改 report.py 中的 read_portfolio(),让它返回 Portfolio 实例,而不是普通列表。
问题在于:原有程序可能依赖对投资组合的遍历。如果 Portfolio 没有实现 __iter__(),程序会崩溃,因为它不再是可迭代对象。
修复方式是添加:
def __iter__(self):
return self._holdings.__iter__()
这样既保留了封装,又兼容原来依赖迭代的代码。
6. 使用属性表达聚合行为
Portfolio 类还提供了 total_cost 属性,用于计算总成本:
@property
def total_cost(self):
return sum([s.shares * s.price for s in self._holdings])
这使 pcost.py 可以简化为:
def portfolio_cost(filename):
portfolio = report.read_portfolio(filename)
return portfolio.total_cost
这是一种更面向对象的设计:成本计算逻辑属于 Portfolio,而不是散落在外部函数中。
7. 构造更完整的容器对象
除了迭代,一个更“像 Python 容器”的类通常还应支持:
len(obj):通过__len__()- 索引访问:通过
__getitem__() - 切片访问:同样由
__getitem__()支持 - 成员测试:通过
__contains__()
完整示例:
class Portfolio:
def __init__(self, holdings):
self._holdings = holdings
def __iter__(self):
return self._holdings.__iter__()
def __len__(self):
return len(self._holdings)
def __getitem__(self, index):
return self._holdings[index]
def __contains__(self, name):
return any([s.name == name for s in self._holdings])
@property
def total_cost(self):
return sum([s.shares * s.price for s in self._holdings])
def tabulate_shares(self):
from collections import Counter
total_shares = Counter()
for s in self._holdings:
total_shares[s.name] += s.shares
return total_shares
支持这些特殊方法后,可以进行如下操作:
len(portfolio)
portfolio[0]
portfolio[0:3]
'IBM' in portfolio
这些行为共同构成了 Python容器协议 的重要部分。
8. Pythonic 设计思想
本文最后强调:所谓 “Pythonic” 的代码,往往意味着对象能够使用 Python 生态中通用的表达方式。
对于容器对象来说,重要的不只是保存数据,还要支持 Python 用户熟悉的操作:
- 可迭代
- 可索引
- 可切片
- 可求长度
- 可进行成员测试
通过实现 __iter__()、__len__()、__getitem__()、__contains__() 等特殊方法,自定义类可以自然融入 Python 语言环境,而无需调用笨重的专用方法。
关键概念
- Python迭代协议:
__iter__()、__next__()与StopIteration共同定义迭代机制。 - Python特殊方法:通过双下划线方法让对象支持语言内置语法。
- Python容器协议:容器对象通常应支持迭代、长度、索引和成员测试。
- 对象封装:
Portfolio封装内部列表,同时暴露更高级的业务接口。 - Pythonic设计:让自定义对象遵循 Python 既有词汇和操作习惯。
主要收获
for循环依赖__iter__()和__next__()。- 迭代结束通过
StopIteration表示。 next()是调用迭代器__next__()的内置快捷方式。- 自定义类只要实现
__iter__(),就可以支持for循环。 - 封装列表时,可以把迭代、索引、长度等操作委托给内部列表。
- 实现常见特殊方法能让自定义对象更符合 Pythonic 风格。