
📖 开篇导读
在前面的课程中,我们经常使用for循环遍历列表、字符串、字典等容器。你是否想过,for循环底层是如何工作的?为什么有些对象可以被遍历,而有些不能?是否有一种方式,可以让我们按需生成数据,而不是一次性把所有数据都放在内存里?
这些问题都指向Python中的迭代器(Iterator)和生成器(Generator)。迭代器是支持for循环的底层机制;生成器是一种特殊的迭代器,可以用更简洁的语法实现惰性求值,在大数据处理、流式处理中非常有用。
💡 工作场景:
- 读取超大文件(如几十GB的日志)时,不能一次读入内存,需要逐行读取,这就是迭代器的应用。
- 生成斐波那契数列无限序列,不可能提前存储所有值,而生成器可以无限生成。
- 数据管道中,将多个处理步骤串联起来,每个步骤使用生成器实现惰性计算,节省内存。
本课将深入讲解:
- 迭代器协议(
__iter__和__next__) - 可迭代对象 vs 迭代器的区别
- 生成器函数(
yield)的原理与用法 - 生成器表达式
- 生成器的进阶方法(
send、close、throw) - 实战:读取大文件、无限序列、管道处理
学完本课,你将掌握Python中高效处理数据流的核心技术。
🎯 学习目标
| 目标编号 | 具体掌握内容 | 对应面试/工作价值 |
|---|---|---|
| 1️⃣ | 理解迭代器协议,能够自定义迭代器类 | 深入理解for循环本质 |
| 2️⃣ | 区分可迭代对象和迭代器 | 面试常问区别 |
| 3️⃣ | 掌握生成器函数(yield),能编写生成器 | 处理大数据序列 |
| 4️⃣ | 掌握生成器表达式,与列表推导式对比 | 节省内存的写法 |
| 5️⃣ | 了解生成器的惰性求值特性及内存优势 | 优化程序性能 |
| 6️⃣ | 掌握生成器的进阶方法(send、close、throw) | 协程基础 |
🔥 面试考点:“
yield的作用是什么?”“生成器和迭代器的区别?”“如何判断一个对象是否可迭代?”“range和list的区别(迭代器角度)?”
📚 知识点理论精讲
一、迭代器协议
在Python中,可迭代对象(Iterable)是指实现了__iter__方法的对象,该方法返回一个迭代器(Iterator)。迭代器实现了__iter__和__next__方法,__next__方法返回下一个元素,并在没有元素时抛出StopIteration异常。
1.1 可迭代对象与迭代器的区别
| 概念 | 特点 | 示例 |
|---|---|---|
| 可迭代对象 | 实现了__iter__,可以用于for循环 | list, tuple, str, dict, range |
| 迭代器 | 实现了__iter__和__next__,记录迭代位置 | iter(list)返回的迭代器对象 |
# 可迭代对象
lst = [1,2,3]
print(hasattr(lst, '__iter__')) # True
print(hasattr(lst, '__next__')) # False
# 获取迭代器
it = iter(lst)
print(hasattr(it, '__iter__')) # True
print(hasattr(it, '__next__')) # True
# 手动迭代
print(next(it)) # 1
print(next(it)) # 2
print(next(it)) # 3
# print(next(it)) # StopIteration
1.2 for循环的本质
# for 循环等价于:
it = iter(iterable)
while True:
try:
value = next(it)
# 循环体
except StopIteration:
break
1.3 自定义迭代器
实现一个迭代器类,需要实现__iter__(返回自身)和__next__。
class MyRange:
def __init__(self, start, end):
self.start = start
self.end = end
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current >= self.end:
raise StopIteration
value = self.current
self.current += 1
return value
for i in MyRange(0, 5):
print(i) # 0 1 2 3 4
二、生成器函数与yield
生成器是一种简化迭代器创建的方式。定义一个函数,使用yield而不是return,这个函数就是生成器函数。调用生成器函数会返回一个生成器对象(属于迭代器)。
2.1 基本用法
def count_down(n):
while n > 0:
yield n
n -= 1
gen = count_down(3)
print(next(gen)) # 3
print(next(gen)) # 2
print(next(gen)) # 1
# print(next(gen)) # StopIteration
for i in count_down(5):
print(i) # 5 4 3 2 1
2.2 yield的原理
当函数执行到yield时,会返回一个值,并暂停函数的执行,保存当前状态(局部变量、指令指针等)。下次调用next()时,从暂停处继续执行。
🧠 类比:
return就像跳下公交车,再也回不来;yield就像到站停车,你可以下去办事,然后回来继续坐车。
2.3 生成器与普通函数的对比
| 特性 | 普通函数 | 生成器函数 |
|---|---|---|
| 返回值 | return | yield |
| 执行方式 | 一次执行完毕 | 逐次执行,可暂停 |
| 内存占用 | 可能很大 | 很小,惰性生成 |
| 多次调用 | 每次重新执行 | 生成器对象有状态 |
2.4 生成器应用场景
- 生成无限序列(如斐波那契数列、自然数)
- 处理大文件逐行读取
- 数据流式处理(管道)
三、生成器表达式
类似于列表推导式,但使用圆括号而不是方括号。返回一个生成器对象,惰性计算。
# 列表推导式(立即计算,占用内存)
squares_list = [x**2 for x in range(1000000)] # 占用大量内存
# 生成器表达式(惰性计算,不占用内存)
squares_gen = (x**2 for x in range(1000000)) # 几乎不占内存
# 使用
for s in squares_gen:
if s > 100:
break
print(s)
生成器表达式可以用在函数调用中,作为参数时括号可以省略:
sum(x**2 for x in range(10)) # 而不是 sum([x**2 for x in range(10)])
四、生成器的高级方法
生成器对象除了__next__()(等价于next()),还有send()、throw()、close()方法。
4.1 send(value)
send可以向生成器发送一个值,该值会成为yield表达式的返回值。send也会使生成器继续执行到下一个yield。
def accumulator():
total = 0
while True:
value = yield total
if value is None:
break
total += value
acc = accumulator()
next(acc) # 启动生成器,执行到第一个yield,返回0
print(acc.send(10)) # 发送10,total=10,yield返回10
print(acc.send(20)) # 发送20,total=30,yield返回30
acc.close()
注意:必须先调用next()或send(None)启动生成器。
4.2 throw(type, value, traceback)
在生成器内部引发一个异常。
def gen():
try:
yield 1
yield 2
except ValueError:
print("收到ValueError")
yield 3
g = gen()
print(next(g)) # 1
g.throw(ValueError) # 输出"收到ValueError",然后yield 3
4.3 close()
关闭生成器,在生成器内部引发GeneratorExit异常。生成器可以捕获该异常进行清理。
def gen():
try:
yield 1
finally:
print("cleanup")
g = gen()
print(next(g))
g.close() # 输出"cleanup"
五、迭代器与生成器的性能优势
- 节省内存:不需要一次性存储所有数据,而是按需生成。
- 减少延迟:可以处理无限序列,每次只计算一个值。
- 提高响应性:在流式处理中,可以边生成边处理。
对比:读取大文件,使用readlines()会一次加载所有行到内存;而直接迭代文件对象是逐行生成,内存友好。
# 坏方式:一次性读取所有行
with open('large.log') as f:
lines = f.readlines() # 如果文件有几GB,内存爆炸
# 好方式:逐行迭代(文件对象本身就是迭代器)
with open('large.log') as f:
for line in f:
process(line)
💻 代码案例实操
案例1:自定义迭代器——斐波那契数列
"""
fibonacci_iterator.py
实现一个迭代器版本的斐波那契数列
"""
class Fibonacci:
def __init__(self, max_count):
self.max_count = max_count
self.count = 0
self.a = 0
self.b = 1
def __iter__(self):
return self
def __next__(self):
if self.count >= self.max_count:
raise StopIteration
self.count += 1
if self.count == 1:
return self.a
elif self.count == 2:
return self.b
else:
self.a, self.b = self.b, self.a + self.b
return self.b
fib = Fibonacci(10)
for num in fib:
print(num, end=' ') # 0 1 1 2 3 5 8 13 21 34
案例2:生成器实现无限自然数
"""
infinite_numbers.py
使用生成器产生无限的自然数序列
"""
def natural_numbers(start=0):
n = start
while True:
yield n
n += 1
# 取前10个自然数
nums = natural_numbers()
for _ in range(10):
print(next(nums), end=' ') # 0 1 2 3 4 5 6 7 8 9
# 使用 itertools.islice 截取有限个
import itertools
first_10 = list(itertools.islice(natural_numbers(), 10))
print(first_10)
案例3:生成器读取大文件(按行过滤)
"""
large_file_filter.py
逐行读取大文件,过滤包含特定关键字的行,返回生成器
"""
def filter_lines(filepath, keyword):
"""生成器函数:逐行读取文件,yield包含关键字的行"""
with open(filepath, 'r', encoding='utf-8') as f:
for line in f:
if keyword in line:
yield line.rstrip('\n')
# 模拟大文件
with open('test_large.txt', 'w') as f:
for i in range(1000):
f.write(f"line {i}: this is a test\n")
# 使用生成器,不会一次加载所有行
for line in filter_lines('test_large.txt', '500'):
print(line) # 只输出包含500的行
案例4:生成器管道处理数据流
"""
generator_pipeline.py
多个生成器串联形成数据处理管道
"""
def read_file(filepath):
"""读取文件,yield每一行"""
with open(filepath, 'r', encoding='utf-8') as f:
for line in f:
yield line
def filter_lines(lines, keyword):
"""过滤包含关键词的行"""
for line in lines:
if keyword in line:
yield line
def convert_to_upper(lines):
"""将行转为大写"""
for line in lines:
yield line.upper()
def take(lines, n):
"""取前n个元素"""
for i, line in enumerate(lines):
if i >= n:
break
yield line
# 构建管道:读取 -> 过滤 -> 转大写 -> 取前5
with open('test_large.txt', 'w') as f:
for i in range(100):
f.write(f"log: event {i}\n")
pipeline = take(
convert_to_upper(
filter_lines(
read_file('test_large.txt'),
'event 5'
)
), 5
)
for item in pipeline:
print(item, end='')
案例5:生成器实现惰性求值——素数生成
"""
prime_generator.py
生成无限素数序列
"""
def is_prime(n):
if n < 2:
return False
for i in range(2, int(n**0.5)+1):
if n % i == 0:
return False
return True
def prime_generator():
"""生成无限素数序列"""
n = 2
while True:
if is_prime(n):
yield n
n += 1
# 获取前20个素数
import itertools
primes = prime_generator()
first_20 = list(itertools.islice(primes, 20))
print(first_20)
# 还可以继续使用原来的生成器获取后面的素数
next_10 = list(itertools.islice(primes, 10))
print(next_10)
案例6:yield与send交互——可控制的累加器
"""
yield_send_demo.py
使用send方法向生成器发送数据,实现交互式累加
"""
def interactive_accumulator():
total = 0
while True:
value = yield total # 返回当前total,并接收外部send的值
if value is None:
break
total += value
return total # 生成器结束后,StopIteration的value可以捕获
acc = interactive_accumulator()
# 启动生成器,必须先调用next或send(None)
next(acc) # 相当于 acc.send(None),返回0
print(acc.send(10)) # 返回10
print(acc.send(20)) # 返回30
print(acc.send(5)) # 返回35
try:
acc.send(None) # 触发break,生成器结束,抛出StopIteration,返回值35
except StopIteration as e:
print(f"最终累加和: {e.value}")
案例7:yield from语法糖
yield from可以将迭代器中的元素逐个yield出来,简化嵌套生成器。
"""
yield_from_demo.py
演示yield from的用法
"""
def sub_gen():
yield 1
yield 2
yield 3
def main_gen():
# 普通方式
for x in sub_gen():
yield x
# 等价于 yield from sub_gen()
def chain(*iterables):
"""使用yield from连接多个可迭代对象"""
for it in iterables:
yield from it
# 使用
for x in chain([1,2], "ab", range(3)):
print(x, end=' ') # 1 2 a b 0 1 2
# 递归生成器(遍历嵌套列表)
def flatten(nested):
"""扁平化嵌套列表"""
for item in nested:
if isinstance(item, list):
yield from flatten(item)
else:
yield item
nested_list = [1, [2, [3, 4], 5], 6]
flat = list(flatten(nested_list))
print(flat) # [1,2,3,4,5,6]
⚠️ 易错点避坑总结
| 序号 | 坑点描述 | 后果 | 解决方案 |
|---|---|---|---|
| 1 | 混淆可迭代对象和迭代器 | 试图对非迭代器调用next()报错 | 使用iter()获取迭代器 |
| 2 | 生成器函数中既用return又用yield | return会触发StopIteration并返回 | 仅在需要提前终止时使用return |
| 3 | 忘记启动生成器就直接send | TypeError: can't send non-None value to a just-started generator | 先调用next(g)或g.send(None) |
| 4 | 生成器表达式与列表推导式混淆 | 误以为生成器表达式也支持所有列表方法(如索引) | 生成器只能迭代一次,不支持索引 |
| 5 | 多次迭代同一个生成器 | 第二次迭代得到空结果 | 需要重新创建生成器对象 |
| 6 | 在迭代生成器时修改被迭代的容器 | 可能引发异常或行为不确定 | 不要在迭代过程中修改 |
| 7 | yield from后没有正确传播异常 | 异常无法被生成器外部捕获 | yield from会自动处理(子生成器的异常会传给调用者) |
| 8 | 在__next__中抛出StopIteration之前没有保存状态 | 迭代器不可恢复 | 正常实现即可 |
| 9 | 生成器中无限循环没有终止条件 | 会导致无限生成 | 确保有退出条件或由调用方控制 |
| 10 | 将生成器视为线程(试图实现异步) | 生成器只是协程基础,但不是操作系统线程 | 使用asyncio或真正的多线程 |
📝 课后实战练习题
第1题:自定义迭代器
实现一个EvenNumbers迭代器,接收start和end,迭代产生之间的所有偶数。示例:EvenNumbers(1,10)产生2,4,6,8。
第2题:生成器实现range功能
编写生成器函数my_range(start, stop=None, step=1),模仿内置range的行为,支持正负步长。使用yield实现。
第3题:逐行读取并统计
给定一个大文件(模拟),编写生成器read_by_chunk(filepath, chunk_size=1024),每次读取固定字节数(而不是行)。然后用它统计文件总字节数。
第4题:无限序列累积求和
创建生成器cumulative_sum(iterable),接收一个可迭代对象,产出累积和。例如输入[1,2,3,4]输出1,3,6,10。测试用无限生成器作为输入。
第5题:生成器管道重构
将案例4的管道使用yield from或函数组合方式重写,使代码更简洁。
第6题:send实现带状态的计数器
实现生成器counter(start=0),可以通过send重置计数:send('reset')重置为初始值,send('increment')增加1。正常next返回当前值并递增。
第7题:扁平化任意深度嵌套
扩展flatten函数,能够处理嵌套的元组、列表等可迭代对象(但不包含字符串递归展开)。使用isinstance(item, (list, tuple))判断。
🔜 下节课预告
迭代器和生成器是惰性求值的基础。下一节课我们将学习装饰器——Python中另一个强大的函数式编程特性,它可以不修改函数本身而增强其功能。
第32课:装饰器入门与进阶:无参装饰器、有参装饰器底层实战
内容包括:
- 闭包回顾与装饰器本质
- 无参装饰器的实现与使用
- 带参数的装饰器(两层嵌套)
- 多个装饰器叠加顺序
functools.wraps保留元信息- 类装饰器
- 实战:计时器、权限校验、日志记录
装饰器是Python面试和实际项目中的高频知识点,学完你将能写出优雅的增强代码。
🌟 学习鼓励:迭代器和生成器是Python高效处理数据的秘诀。理解它们,你就能写出既内存友好又代码优雅的解决方案。请多动手写生成器,特别是无限序列和数据管道,你会感受到惰性求值的强大之处。
🔗《50节课 Python 从入门到精通》系列课程导航
🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~
1856

被折叠的 条评论
为什么被折叠?



