Furthe77oad
Furthe77oad
发布于 2026-09-15 / 0 阅读
0
0

Python 迭代器与生成器

写在最前

我们每天都在使用 Python 的迭代机制:遍历列表、逐行读取文件、处理查询结果,甚至在推导式中生成新数据。看起来简单的 for 循环,背后其实由“可迭代对象”和“迭代器”共同完成。

生成器则是在这套机制上提供的一种便捷写法。它可以按需产生数据,而不必一次把全部结果存进内存。

理解这几个概念之后,很多 Python 代码都会变得更加自然。

1. 先认识三个概念

在正式写代码前,先区分三个容易混淆的概念。

1.1 可迭代对象

能够被 for 循环遍历的对象,叫作可迭代对象(iterable)。

常见的可迭代对象包括:

  • 列表;
  • 元组;
  • 字符串;
  • 字典;
  • 集合;
  • range 对象;
  • 文件对象。

例如,列表可以直接交给 for 循环:

languages = ["Python", "Rust", "Go"]

for language in languages:
    print(language)

输出:

Python
Rust
Go

1.2 迭代器

迭代器(iterator)是负责“记住当前位置,并返回下一个值”的对象。

它支持两个关键操作:

iter(object)   # 获得迭代器
next(iterator) # 取得下一个值

1.3 生成器

生成器(generator)是一种特殊的迭代器。它通常由包含 yield 的生成器函数,或者生成器表达式创建。

三者之间的关系可以概括为:

可迭代对象
    │
    │ iter()
    ▼
  迭代器
    │
    ├── next() ──> 下一个值
    ├── next() ──> 下一个值
    └── next() ──> StopIteration

生成器是迭代器的一种便捷实现

2. 使用 iter() 和 next()

下面不使用 for 循环,手动遍历一个播放列表:

playlist = ["Track A", "Track B", "Track C"]
player = iter(playlist)

print(next(player))
print(next(player))
print(next(player))

输出:

Track A
Track B
Track C

player 会记住已经读取到哪里。每调用一次 next(),位置就向前移动一次。

如果所有元素都已经取完,再次调用 next():

print(next(player))

程序会抛出异常:

StopIteration

StopIteration 并不表示程序出现了意外故障,它是迭代器发出的正常结束信号。

3. for 循环背后做了什么

我们平时写的代码是:

colors = ["blue", "white", "black"]

for color in colors:
    print(color)

从迭代协议的角度看,它大致相当于:

colors = ["blue", "white", "black"]
iterator = iter(colors)

while True:
    try:
        color = next(iterator)
        print(color)
    except StopIteration:
        break

for 循环替我们完成了四件事:

  1. 调用 iter() 获取迭代器;
  2. 反复调用 next();
  3. 把得到的值赋给循环变量;
  4. 捕获 StopIteration 并结束循环。

所以日常代码中没有必要手动捕获 StopIteration,直接使用 for 通常更加清晰。

4. 可迭代对象不一定是迭代器

列表是可迭代对象,但列表本身不是迭代器:

numbers = [10, 20, 30]

print(iter(numbers) is numbers)

输出:

False

iter(numbers) 创建了一个单独的列表迭代器。

而迭代器调用 iter() 时,通常会返回自己:

numbers = [10, 20, 30]
iterator = iter(numbers)

print(iter(iterator) is iterator)

输出:

True

这个差别非常重要:列表可以反复开始新的遍历,而一个已经消耗过的迭代器不会自动回到开头。

numbers = [10, 20, 30]
iterator = iter(numbers)

print(list(iterator))
print(list(iterator))

输出:

[10, 20, 30]
[]

第一次转换已经取完所有元素,因此第二次只能得到空列表。

5. 创建自己的迭代器

一个对象想成为迭代器,需要实现两个特殊方法:

  • __iter__():返回迭代器对象;
  • __next__():返回下一个值,并在结束时抛出 StopIteration。

下面创建一个按照指定步长递增的迭代器:

class StepCounter:
    def __init__(self, start, stop, step):
        self.current = start
        self.stop = stop
        self.step = step

    def __iter__(self):
        return self

    def __next__(self):
        if self.current >= self.stop:
            raise StopIteration

        value = self.current
        self.current += self.step
        return value


counter = StepCounter(start=2, stop=11, step=3)

for number in counter:
    print(number)

输出:

2
5
8

执行过程如下:

  1. for 调用 counter.__iter__();
  2. 每轮调用一次 counter.__next__();
  3. 当前值达到 stop 后抛出 StopIteration;
  4. for 循环收到结束信号并停止。

手动实现迭代器能帮助我们理解原理,但很多时候使用生成器会更简单。

6. 使用 yield 创建生成器

只要函数体中出现 yield,这个函数就成为生成器函数。

下面用生成器逐个产生分页编号:

def page_numbers(total_pages):
    page = 1

    while page <= total_pages:
        yield page
        page += 1


pages = page_numbers(4)
print(pages)

输出类似:

<generator object page_numbers at 0x...>

调用生成器函数时,函数体不会立刻完整执行,而是先返回一个生成器对象。

可以使用 next() 逐个取值:

pages = page_numbers(4)

print(next(pages))
print(next(pages))

输出:

1
2

也可以直接使用 for:

for page in page_numbers(4):
    print(page)

输出:

1
2
3
4

7. yield 为什么能够“暂停”函数

普通函数遇到 return 后会结束,并丢弃内部状态:

def get_status():
    return "ready"

生成器函数遇到 yield 时,会完成三件事:

  1. 返回 yield 后面的值;
  2. 暂停在当前位置;
  3. 保存局部变量和执行状态。

下一次调用 next() 时,它会从暂停的位置继续运行。

def process_steps():
    print("开始准备")
    yield "prepare"

    print("开始执行")
    yield "run"

    print("开始收尾")
    yield "finish"


steps = process_steps()

print(next(steps))
print(next(steps))
print(next(steps))

输出:

开始准备
prepare
开始执行
run
开始收尾
finish

每次 next() 只会让函数运行到下一个 yield,而不是从头重新执行。

当函数执行到末尾,或者遇到不带值的 return,Python 会自动产生 StopIteration。

8. 生成器为什么节省内存

假设需要处理一百万个整数。列表推导式会先创建并保存全部结果:

squares_list = [number ** 2 for number in range(1_000_000)]

生成器表达式则只描述计算规则:

squares_generator = (number ** 2 for number in range(1_000_000))

只有在取出某个值时,它才进行对应计算:

print(next(squares_generator))
print(next(squares_generator))
print(next(squares_generator))

输出:

0
1
4

两种方式的差别可以简单理解为:

列表:先把所有结果准备好,再交给使用者
生成器:使用者需要一个,就现场生成一个

生成器通常更节省内存,但它也有代价:数据一般只能顺序读取一次,不能像列表一样随时通过下标访问。

9. 生成器表达式

生成器表达式的写法与列表推导式很相似,只是把方括号换成圆括号:

# 列表推导式
even_list = [number for number in range(10) if number % 2 == 0]

# 生成器表达式
even_generator = (number for number in range(10) if number % 2 == 0)

查看结果:

print(even_list)
print(list(even_generator))

输出:

[0, 2, 4, 6, 8]
[0, 2, 4, 6, 8]

最终数值相同,但产生结果的时机不同。

生成器表达式作为函数的唯一参数时,可以省略额外的一层圆括号:

total = sum(number ** 2 for number in range(1, 6))
print(total)

输出:

55

10. 一个实际例子:分批处理数据

假设系统中有一组任务,我们希望每次只取固定数量,分批交给后续程序处理:

def split_batches(items, batch_size):
    for start in range(0, len(items), batch_size):
        end = start + batch_size
        yield items[start:end]


tasks = ["task-1", "task-2", "task-3", "task-4", "task-5"]

for batch in split_batches(tasks, 2):
    print(batch)

输出:

['task-1', 'task-2']
['task-3', 'task-4']
['task-5']

这里没有一次性构造“所有批次组成的列表”。每轮循环需要一批数据时,生成器才通过 yield 返回一批。

这种思路常用于:

  • 分页读取接口数据;
  • 分批写入数据库;
  • 逐行处理大型文件;
  • 持续接收日志或消息;
  • 构建数据处理流水线。

11. 使用 yield from 委托迭代

如果一个生成器需要依次产出多个可迭代对象中的元素,可以使用 yield from:

def all_tags():
    python_tags = ["python", "iterator"]
    web_tags = ["html", "css"]

    yield from python_tags
    yield from web_tags


print(list(all_tags()))

输出:

['python', 'iterator', 'html', 'css']

它相当于:

def all_tags():
    python_tags = ["python", "iterator"]
    web_tags = ["html", "css"]

    for tag in python_tags:
        yield tag

    for tag in web_tags:
        yield tag

在简单场景中,可以把 yield from 理解为“把另一个可迭代对象中的值逐个转交出去”。

12. 常见误区

12.1 生成器不是元组

下面创建的是生成器表达式:

values = (number * 10 for number in range(3))

如果想得到元组,需要显式转换:

values = tuple(number * 10 for number in range(3))
print(values)

输出:

(0, 10, 20)

12.2 生成器通常只能完整消费一次

names = (name.upper() for name in ["Ada", "Linus", "Guido"])

print(list(names))
print(list(names))

输出:

['ADA', 'LINUS', 'GUIDO']
[]

如果需要反复遍历,可以保存原始数据,并在每次使用前重新创建生成器。

12.3 创建生成器不会立即执行函数体

def demo():
    print("函数开始执行")
    yield 1


generator = demo()
print("生成器已经创建")
print(next(generator))

输出:

生成器已经创建
函数开始执行
1

直到第一次调用 next(),生成器函数才真正开始执行。

12.4 不要无条件创建无限生成器

生成器可以无限地产生数据:

def sequence():
    number = 1

    while True:
        yield number
        number += 1

这种写法本身没有问题,但使用方必须知道何时停止,例如配合条件判断或 itertools.islice(),否则循环永远不会结束。

13. 应该选择列表还是生成器

适合使用列表的情况:

  • 数据量较小;
  • 结果需要反复使用;
  • 需要通过下标随机访问;
  • 需要调用 len() 或直接查看全部内容。

适合使用生成器的情况:

  • 数据量很大,甚至没有明确上限;
  • 只需要顺序处理一次;
  • 希望尽快得到第一个结果;
  • 数据来自文件、网络或持续的数据流;
  • 希望把多个处理步骤连接成流水线。

生成器并不是列表的替代品。选择哪一种,取决于数据规模、访问方式和代码可读性。

14. 总结

可以用下面几句话记住本文的核心:

可迭代对象:能够被 for 遍历
迭代器:记住遍历位置,通过 next() 逐个返回值
StopIteration:表示迭代已经结束
生成器:一种按需产生数据的特殊迭代器
yield:返回一个值并暂停函数,下一次从原位置继续

for 循环替我们隐藏了迭代协议的细节,而生成器帮助我们用更少的代码实现迭代器。对于大型数据和一次性顺序处理任务,生成器能够在保持代码简洁的同时显著减少内存占用。

参考资料


评论