系列文章

Python 从入门到精通

第 15 / 36 篇

从环境安装和基础语法出发,逐步学习工程实践、自动化、数据处理与 Web 开发。

  1. 01
    Python 怎么装、怎么用
  2. 02
    变量、数字和字符串
  3. 03
    列表:把一组数据放在一起
  4. 04
    元组、集合和字典
  5. 05
    条件判断:让程序做选择
  6. 06
    循环:重复的事交给程序
  7. 07
    函数:把代码整理成可复用的块
  8. 08
    模块与包:拆分你的程序
  9. 09
    输入、输出与字符串格式化
  10. 10
    文件读写:保存程序的数据
  11. 11
    异常处理:程序出错时怎么办
  12. 12
    基础阶段练习:命令行记账本
  13. 13
    类和对象:面向对象入门
  14. 14
    继承、组合与特殊方法
  15. 15
    迭代器与生成器正在阅读
  16. 16
    列表推导式与生成器表达式
  17. 17
    装饰器:给函数增加能力
  18. 18
    上下文管理器与 with
  19. 19
    类型标注与 dataclass
  20. 20
    正则表达式:从文本中找规律
  21. 21
    日期、时间与时区
  22. 22
    日志与调试
  23. 23
    虚拟环境与依赖管理
  24. 24
    测试:让修改不再提心吊胆
  25. 25
    网络请求:用 Python 调用 API
  26. 26
    网页解析与合规采集
  27. 27
    操作 Excel、CSV 与批量文件
  28. 28
    SQLite:给程序加一个数据库
  29. 29
    数据分析入门:NumPy 与 Pandas
  30. 30
    画图:把数据变得直观
  31. 31
    Flask 入门:做一个小网站
  32. 32
    异步编程:同时处理多项任务
  33. 33
    线程、进程与并发选择
  34. 34
    性能分析与优化
  35. 35
    项目结构、配置与发布
  36. 36
    综合项目:从需求到上线

查看整个系列 →

你有没有想过,for x in 列表 到底是怎么工作的?为什么字符串、文件都能 for?这一篇揭开底层的秘密:迭代器(iterator)和生成器(generator)。搞懂它们,你写的代码能省一大半内存。

可迭代对象 vs 迭代器

两个容易混的词:

  • 可迭代对象(iterable):能被 for 遍历的东西,比如列表、字符串、字典、文件。它们能通过 iter() 变成迭代器
  • 迭代器(iterator):能一个一个往外吐东西的对象,用 next() 取下一个
nums = [1, 2, 3]
it = iter(nums)          # 列表 → 迭代器
print(next(it))          # 1
print(next(it))          # 2
print(next(it))          # 3
# print(next(it))        # StopIteration!吐完了

for 循环的本质就是:不断 next(),直到遇到 StopIteration 异常就结束。所以下面两种写法等价:

# 写法 1:for(日常用这个)
for n in nums:
    print(n)

# 写法 2:手动迭代(理解原理用)
it = iter(nums)
while True:
    try:
        n = next(it)
        print(n)
    except StopIteration:
        break

关键区别:迭代器是一次性的——吐完就没了,不能回头。列表可以反复遍历,迭代器不行:

it = iter([1, 2, 3])
print(list(it))   # [1, 2, 3]
print(list(it))   # [] 空了!迭代器不能重来

生成器:懒加载的迭代器

生成器是用函数写的迭代器,核心就一个词:yield

def count_down(n):
    while n > 0:
        yield n          # 关键:yield 暂停并返回
        n -= 1

for x in count_down(3):
    print(x)             # 3 2 1

和普通函数的区别:普通函数 return 一次就结束;含 yield 的函数每次调用 next()执行到 yield 就暂停,下次 next 从暂停处继续。函数体不是一次性跑完,而是”挤牙膏”式地往外吐。

看个更直观的例子:

def gen():
    print("第一次被取")
    yield 1
    print("第二次被取")
    yield 2
    print("第三次被取")

g = gen()
print(next(g))   # 第一次被取 / 1
print(next(g))   # 第二次被取 / 2
print(next(g))   # 第三次被取 / StopIteration

注意:调用 gen() 时函数体一行都没执行,直到第一次 next() 才开始。

生成器为什么省内存

这是生成器最大的价值。比如要处理 1 亿个数字:

# 坏:一次性造出 1 亿个元素的列表,内存爆炸
nums = [x * 2 for x in range(100_000_000)]

# 好:生成器边算边吐,内存几乎不变
nums = (x * 2 for x in range(100_000_000))   # 生成器表达式,注意是圆括号
for n in nums:
    if n > 100:
        break
    print(n)

列表推导式是”全部算好放内存”,生成器是”用的时候才算”。处理大文件、大日志时,生成器是救命的:

def read_big_file(path):
    with open(path, "r", encoding="utf-8") as f:
        for line in f:          # 逐行读,不会把整个文件装进内存
            yield line.strip()

生成器表达式

生成器也有推导式语法,把列表推导式的方括号换成圆括号:

squares = (x * x for x in range(10))
print(sum(squares))        # 285(生成器可以直接喂给 sum)
# 注意:sum 消费完后生成器就空了
print(list(squares))       # [] 空了

什么时候用列表推导式、什么时候用生成器表达式?只需要遍历一次,用生成器;要反复用、要索引、要长度,用列表

无限序列:生成器的主场

列表装不下无限的东西,生成器可以:

def fibonacci():
    a, b = 0, 1
    while True:              # 无限循环,但不会卡死
        yield a
        a, b = b, a + b

fib = fibonacci()
for _ in range(10):
    print(next(fib), end=" ")   # 0 1 1 2 3 5 8 13 21 34

因为生成器是”要一个给一个”,无限序列也能优雅处理——只要你别傻乎乎地 list() 它。

itertools:迭代器工具箱

标准库 itertools 提供一堆实用的迭代器工具:

import itertools

# 无限计数
for i in itertools.count(1):
    if i > 3:
        break
    print(i)              # 1 2 3

# 无限循环
for x in itertools.cycle(["红", "绿"]):
    ...  # 红绿红绿...(记得 break)

# 排列组合
print(list(itertools.permutations([1, 2, 3], 2)))  # 排列
print(list(itertools.combinations([1, 2, 3], 2)))  # 组合

# 按条件分组
for key, group in itertools.groupby("AAABBBCC"):
    print(key, len(list(group)))   # A 3 / B 3 / C 2

新手坑

坑 1:生成器只能遍历一次。遍历完就空。需要多次用就转成列表存起来。

坑 2:yield 和 return 混用。函数里同时出现 yield 和 return 可以(return 直接结束),但新手容易写出”yield 之后 return 值”,那个 return 的值永远不会被 next() 拿到,只会引发 StopIteration。别依赖它。

坑 3:把生成器当列表用。不能 gen[0]、不能 len(gen)。要下标就老老实实用列表。

小结

  • 迭代器:iter() 得到,next() 一个一个取,取完 StopIteration,一次性
  • for 循环本质 = 反复 next() 直到 StopIteration
  • 生成器:函数里用 yield,边算边吐,天然省内存
  • 生成器表达式:圆括号版推导式,只遍历一次时用
  • 无限序列、大文件处理是生成器的主场;itertools 是工具箱

练习

  1. 写一个生成器 even_numbers(limit),产出 0 到 limit 的所有偶数
  2. 用生成器读一个大文件的前 5 行就停(提示:for 循环里计数 break)
  3. 思考:range(10_000_000) 为什么内存占用极小?它返回的是列表吗?提示:试试 type(range(10))