Python 从入门到精通
第 15 / 36 篇
从环境安装和基础语法出发,逐步学习工程实践、自动化、数据处理与 Web 开发。
-
01
Python 怎么装、怎么用 -
02
变量、数字和字符串 -
03
列表:把一组数据放在一起 -
04
元组、集合和字典 -
05
条件判断:让程序做选择 -
06
循环:重复的事交给程序 -
07
函数:把代码整理成可复用的块 -
08
模块与包:拆分你的程序 -
09
输入、输出与字符串格式化 -
10
文件读写:保存程序的数据 -
11
异常处理:程序出错时怎么办 -
12
基础阶段练习:命令行记账本 -
13
类和对象:面向对象入门 -
14
继承、组合与特殊方法 -
15
迭代器与生成器正在阅读 -
16
列表推导式与生成器表达式 -
17
装饰器:给函数增加能力 -
18
上下文管理器与 with -
19
类型标注与 dataclass -
20
正则表达式:从文本中找规律 -
21
日期、时间与时区 -
22
日志与调试 -
23
虚拟环境与依赖管理 -
24
测试:让修改不再提心吊胆 -
25
网络请求:用 Python 调用 API -
26
网页解析与合规采集 -
27
操作 Excel、CSV 与批量文件 -
28
SQLite:给程序加一个数据库 -
29
数据分析入门:NumPy 与 Pandas -
30
画图:把数据变得直观 -
31
Flask 入门:做一个小网站 -
32
异步编程:同时处理多项任务 -
33
线程、进程与并发选择 -
34
性能分析与优化 -
35
项目结构、配置与发布 -
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是工具箱
练习
- 写一个生成器
even_numbers(limit),产出 0 到 limit 的所有偶数 - 用生成器读一个大文件的前 5 行就停(提示:for 循环里计数 break)
- 思考:
range(10_000_000)为什么内存占用极小?它返回的是列表吗?提示:试试type(range(10))