Leodanis Pozo Ramos
当你编写计算机程序时,经常需要多次重复执行某段代码。为此,你可以采用以下方法之一:
- 顺序重复目标代码所需次数
- 将目标代码放入一个循环中,按需运行多次
第一种方法存在一些缺点,最麻烦的问题是重复代码本身难以维护且不可扩展。例如,以下代码会在屏幕上打印三次问候语:
# greeting.py
print("Hello!")
print("Hello!")
print("Hello!")
运行此脚本后,你会看到 'Hello!' 被打印三次。这段代码虽然有效,但如果你决定将消息更新为 'Hello, World!',就必须修改三行代码,这带来了维护负担。
想象一下,如果面对的是更大、更复杂的代码块,维护工作可能会变成一场噩梦。
使用循环是解决该问题并避免可维护性问题的更好方式。循环允许你按需多次运行一段代码。例如,使用 while 循环重写上述示例:
>>> times = 0
>>> while times < 3:
... print("Hello!")
... times += 1
...
Hello!
Hello!
Hello!
这个 while 循环会持续运行,只要循环继续条件(times < 3)为真。每次迭代中,循环都会打印问候消息并递增控制变量 times。现在,如果你要更新消息,只需修改一行代码,大大提升了可维护性。
Python 的 while 循环支持所谓的不定次迭代(indefinite iteration),即反复执行同一代码块,可能执行次数不确定。
你还可能遇到另一种类似但不同的迭代类型——定次迭代(definite iteration),即按预定义次数遍历相同代码。当你需要逐个遍历数据流中的项目时,这种迭代特别有用。
在 Python 中,通常使用 for 循环来实现此类迭代:
>>> numbers = [1, 2, 3, 4, 5]
>>> for number in numbers:
... print(number)
...
1
2
3
4
5
在此示例中,numbers 列表代表你的数据流,你通常将其泛称为可迭代对象(iterable),因为你可以在其上进行迭代(稍后会详细介绍)。
当你使用 while 或 for 循环多次重复执行代码时,实际上就是在运行迭代(iteration)。这是对该过程本身的命名。
认识 Python 迭代器
迭代器通过 PEP 234 在 Python 2.2 中引入。它们是对语言的重要补充,因为它们统一了迭代过程,并将其从容器数据类型的实现细节中抽象出来。这种抽象使得即使对无序集合(如集合 set)也能确保每个元素恰好被访问一次。
什么是 Python 迭代器?
在 Python 中,迭代器是一个允许你遍历数据集合(如列表、元组、字典和集合)的对象。
Python 迭代器实现了迭代器设计模式,允许你遍历容器并访问其元素。该模式将迭代算法与容器数据结构解耦。
迭代器主要负责两项操作:
- 一次返回数据流或容器中的一个项目
- 跟踪当前项和已访问项
简而言之,迭代器会在维护迭代状态所需的所有内部记录的同时,从集合或数据流中逐个生成每个项目或值。
Python 迭代器必须实现一个广为人知的内部结构——迭代器协议。
什么是 Python 迭代器协议?
当一个 Python 对象实现了两个特殊方法(合称迭代器协议)时,它就被视为迭代器。这两个方法使 Python 迭代器得以工作。因此,如果你想创建自定义迭代器类,就必须实现以下方法:
| 方法 | 描述 |
|---|---|
.__iter__() |
用于初始化迭代器,必须返回一个迭代器对象。 |
.__next__() |
用于迭代,必须返回数据流中的下一个值。 |
迭代器的 .__iter__() 方法通常返回 self(即当前对象:迭代器本身)。该方法的实现通常如下:
def __iter__(self):
return self
.__iter__() 的唯一职责是返回一个迭代器对象,因此通常直接返回 self(前提是该实例定义了 .__next__() 方法)。
.__next__() 方法则根据需求更复杂一些。它必须返回数据流中的下一个项目,并在数据流中没有更多项目时抛出 StopIteration 异常。该异常将终止迭代过程。没错,迭代器使用异常进行流程控制。
何时在 Python 中使用迭代器?
Python 迭代器最常见的用例是允许遍历数据流或容器数据结构。Python 在底层使用迭代器来支持所有需要迭代的操作,包括 for 循环、推导式、可迭代解包等。因此,你其实一直在无意识地使用迭代器。
在日常编程中,当你需要遍历未知数量或海量数据集时,迭代器非常有用。这些数据可能来自本地磁盘、数据库或网络。
在这些情况下,迭代器允许你一次只处理一个项目,而不会耗尽系统内存资源——这是迭代器最吸引人的特性之一。
创建不同类型的迭代器
通过在类中使用构成迭代器协议的两个方法,你可以编写至少三种不同类型的自定义迭代器:
- 接收数据流,并按原样逐个生成数据项
- 接收数据流,转换每个项目,然后生成转换后的项目
- 不接收输入数据,而是通过某种计算生成新数据,最终生成这些数据项
第一种迭代器是你所说的经典迭代器,因为它实现了原始的迭代器模式。第二和第三种则通过新增功能进一步拓展了该模式的能力。
注意:第二和第三种迭代器可能让你联想到函数式编程中的映射(mapping)和过滤(filtering)操作。
生成原始数据
作为第一个示例,你将编写一个名为 SequenceIterator 的经典迭代器。它接收一个序列类型作为参数,并按需生成其项目。
# sequence_iter.py
class SequenceIterator:
def __init__(self, sequence):
self._sequence = sequence
self._index = 0
def __iter__(self):
return self
def __next__(self):
if self._index < len(self._sequence):
item = self._sequence[self._index]
self._index += 1
return item
else:
raise StopIteration
SequenceIterator 在实例化时接收一个值序列。.__init__() 方法负责创建适当的实例属性,包括输入序列和 _index 属性(用于通过索引遍历序列)。
.__iter__() 方法仅返回当前对象 self(即迭代器本身,它必须有 .__next__() 方法)。
.__next__() 方法中,先检查当前索引是否小于输入序列的项目数。如果是,则获取当前项,递增 _index,并返回该项;否则抛出 StopIteration 异常以结束迭代。
使用示例:
>>> from sequence_iter import SequenceIterator
>>> for item in SequenceIterator([1, 2, 3, 4]):
... print(item)
...
1
2
3
4
注意:你可以创建一个不定义
.__iter__()方法的迭代器,此时.__next__()仍可工作。但若想在for循环中使用,必须实现.__iter__(),因为循环总会调用它来初始化迭代器。
转换输入数据
假设你想编写一个迭代器,接收数字序列,计算每个数字的平方值,并按需生成这些平方值:
# square_iter.py
class SquareIterator:
def __init__(self, sequence):
self._sequence = sequence
self._index = 0
def __iter__(self):
return self
def __next__(self):
if self._index < len(self._sequence):
square = self._sequence[self._index] ** 2
self._index += 1
return square
else:
raise StopIteration
使用示例:
>>> from square_iter import SquareIterator
>>> for square in SquareIterator([1, 2, 3, 4, 5]):
... print(square)
...
1
4
9
16
25
这种数据转换功能非常高效:它不会一次性将所有结果存入内存(如创建新列表),而是按需计算,节省大量内存。
生成新数据
你还可以创建不依赖输入数据流、而是通过计算生成新数据的迭代器。例如,生成斐波那契数列:
# fib_iter.py
class FibonacciIterator:
def __init__(self, stop=10):
self._stop = stop
self._index = 0
self._current = 0
self._next = 1
def __iter__(self):
return self
def __next__(self):
if self._index < self._stop:
self._index += 1
fib_number = self._current
self._current, self._next = self._next, self._current + self._next
return fib_number
else:
raise StopIteration
使用示例(默认生成10个斐波那契数):
>>> from fib_iter import FibonacciIterator
>>> for fib_number in FibonacciIterator():
... print(fib_number)
...
0
1
1
2
3
5
8
13
21
34
编写潜在无限迭代器
Python 迭代器可以处理潜在无限的数据流。只需省略 StopIteration 即可:
# inf_fib.py
class FibonacciInfIterator:
def __init__(self):
self._current = 0
self._next = 1
def __iter__(self):
return self
def __next__(self):
self._current, self._next = self._next, self._current + self._next
return self._current
警告:无限循环会导致程序挂起!在 REPL 中可按
Ctrl+C终止。
从 collections.abc.Iterator 继承
collections.abc 模块包含一个名为 Iterator 的抽象基类(ABC)。继承它可快速创建自定义迭代器:
from collections.abc import Iterator
class SequenceIterator(Iterator):
def __init__(self, sequence):
self._sequence = sequence
self._index = 0
def __next__(self):
if self._index < len(self._sequence):
item = self._sequence[self._index]
self._index += 1
return item
else:
raise StopIteration
这样就不必手动实现 .__iter__() 方法了。
创建生成器迭代器
生成器函数是一种特殊函数,允许你以函数式风格创建迭代器。与普通函数不同,生成器函数使用 yield 语句返回一个生成器迭代器,可逐个生成数据流。
注意:Python 中“生成器”一词通常指两个概念:
- 生成器函数:使用
yield定义的函数- 生成器迭代器:该函数返回的对象
创建生成器函数
>>> def sequence_generator(sequence):
... for item in sequence:
... yield item
...
>>> for number in sequence_generator([1, 2, 3, 4]):
... print(number)
...
1
2
3
4
生成器函数比基于类的迭代器更简洁易懂。
使用生成器表达式
生成器表达式的语法与列表推导式几乎相同,只需将方括号 [] 改为圆括号 ():
>>> (item for item in [1, 2, 3, 4])
<generator object <genexpr> at 0x...>
>>> gen = (item for item in [1, 2, 3, 4])
>>> for item in gen:
... print(item)
...
1
2
3
4
不同类型的生成器迭代器
生成器也可用于:
- 按原样生成输入数据
- 转换输入并生成新数据流
- 通过计算生成全新数据流(无需输入)
示例1:平方生成器
>>> def square_generator(sequence):
... for item in sequence:
... yield item ** 2
...
>>> list(square_generator([1, 2, 3, 4, 5]))
[1, 4, 9, 16, 25]
示例2:斐波那契生成器
>>> def fibonacci_generator(stop=10):
... current, next_val = 0, 1
... for _ in range(stop):
... yield current
... current, next_val = next_val, current + next_val
...
>>> list(fibonacci_generator(5))
[0, 1, 1, 2, 3]
使用迭代器进行内存高效的数据处理
迭代器和生成器比普通函数、容器类型和推导式更节省内存,因为它们不会同时将所有数据存储在内存中,而是按需生成。
返回迭代器而非容器类型
普通函数通常创建列表等容器存储结果:
>>> def square_list(numbers):
... return [n**2 for n in numbers]
...
>>> square_list([1,2,3,4])
[1, 4, 9, 16]
而生成器只在需要时计算单个值,大幅降低内存占用。
构建数据处理管道
可将多个生成器函数组合成内存高效的数据处理管道:
# math_pipeline.py
def to_square(numbers): return (n**2 for n in numbers)
def to_even(numbers): return (n for n in numbers if n % 2 == 0)
def to_string(numbers): return (str(n) for n in numbers)
# 使用管道
>>> import math_pipeline as mpl
>>> list(mpl.to_string(mpl.to_square(mpl.to_even(range(10)))))
['0', '4', '16', '36', '64']
Python 迭代器的一些限制
尽管迭代器功能强大,但也存在一些约束:
- 不可重复迭代:迭代器一旦耗尽,无法再次使用
- 无法重置:耗尽后必须创建新迭代器
- 只能前进:不支持反向遍历(无
.__previous__()) - 无法获取长度:不知道总共有多少项
- 不支持索引/切片:不能使用
obj[2]或obj[1:3]
解决方案:可创建支持多次迭代的类(如
ReusableRange)。
使用内置 next() 函数
next() 函数用于从迭代器获取下一项(内部调用 .__next__()):
>>> numbers = iter([1, 2, 3])
>>> next(numbers)
1
>>> next(numbers, 0) # 提供默认值
2
>>> next(numbers, 0)
3
>>> next(numbers, 0) # 耗尽后返回默认值
0
常见用途:跳过 CSV 文件的标题行
with open("data.csv") as f:
next(f) # 跳过首行
for line in f:
process(line)
认识 Python 可迭代对象(Iterables)
可迭代对象是可以被迭代的对象(如列表、元组、字符串等)。关键区别:
| 特性 | 迭代器 | 可迭代对象 |
|---|---|---|
可直接用于 for 循环 |
✅ | ✅ |
| 可多次迭代 | ❌ | ✅ |
支持 next() |
✅ | ❌ |
| 保存迭代状态 | ✅ | ❌ |
| 内存高效 | ✅ | ❌ |
可迭代协议
对象只要实现 .__iter__() 方法(返回迭代器)或 .__getitem__() 方法(支持索引),就是可迭代的。
示例:使栈可迭代
class Stack:
def __init__(self):
self._items = []
def push(self, item):
self._items.append(item)
def __iter__(self):
return iter(self._items) # 或使用 yield / yield from
异步迭代器(Async Iterators)
Python 3.7+ 支持异步迭代,使用 async for 循环:
# async_rand.py
import asyncio
from random import randint
class AsyncIterable:
def __init__(self, stop):
self._stop = stop
self._index = 0
def __aiter__(self):
return self
async def __anext__(self):
if self._index >= self._stop:
raise StopAsyncIteration
await asyncio.sleep(value := randint(1, 3))
self._index += 1
return value
# 使用
>>> async def main():
... async for n in AsyncIterable(3):
... print(n)
...
>>> asyncio.run(main())
2
1
3
异步迭代器协议:
.__aiter__()→ 返回自身.__anext__()→ 返回可等待对象,耗尽时抛出StopAsyncIteration
总结
通过本教程,你已掌握:
- 如何使用迭代器协议创建自定义迭代器
- 迭代器与可迭代对象的区别及使用场景
- 如何用生成器函数和表达式创建生成器迭代器
- 如何构建内存高效的数据处理管道
- 如何编写异步迭代器
现在,你已能根据需求选择合适的工具,在代码中充分发挥迭代器与可迭代对象的强大能力!