yield和return有什么区别?——生成器和迭代器
本文目录
先说结论
生成器是一种特殊的迭代器——函数中有yield关键字就变成了生成器函数。调用生成器函数不立即执行,而是返回一个生成器对象。每次调用next()执行到yield暂停并返回值,下次next()从暂停处继续执行。好处是惰性求值(Lazy Evaluation)——按需产出数据,不需要一次性把所有结果放在内存中。
先把这段讲顺;面试官追问时,再展开下面的原理和细节。
详细讲解
return vs yield——核心区别
# return:一次性返回全部结果,函数结束
def get_all_numbers(n):
result = []
for i in range(n):
result.append(i * 2)
return result # 一次性返回整个列表
nums = get_all_numbers(10_000_000) # 列表占约80MB内存!
# yield:逐个产出结果,函数暂停/恢复
def gen_numbers(n):
for i in range(n):
yield i * 2 # 产出一个值,暂停,等下次next()
nums = gen_numbers(10_000_000) # 生成器对象,几乎不占内存!
# 只有在遍历时才逐个计算
for num in nums:
process(num) # 每次只有一个值在内存中
| 维度 | return(列表) | yield(生成器) |
|---|---|---|
| 内存 | 全部结果存在内存 | 只有当前值在内存 |
| 计算时机 | 调用时全部计算 | 遍历时按需计算 |
| 可重复遍历 | 可以(列表还在内存) | 不可以(遍历一次就耗尽) |
| 适用场景 | 结果集小、需要多次访问 | 结果集大、只遍历一次 |
生成器的执行流程——暂停和恢复
def countdown(n):
print(f'Starting countdown from {n}')
while n > 0:
yield n # 暂停,返回n
n -= 1 # 下次next()从这里继续
print('Done!')
gen = countdown(3)
# 此时函数还没执行!只是创建了生成器对象
print(next(gen)) # 打印'Starting countdown from 3',返回3
print(next(gen)) # 返回2(从n -= 1继续执行)
print(next(gen)) # 返回1
print(next(gen)) # 打印'Done!',抛出StopIteration(结束)
# for循环自动处理StopIteration
for x in countdown(3):
print(x) # 3, 2, 1
生成器表达式——列表推导式的惰性版本
# 列表推导式(立即计算,全部在内存)
squares = [x**2 for x in range(10_000_000)] # 约80MB内存
# 生成器表达式(惰性计算,按需产出)
squares = (x**2 for x in range(10_000_000)) # 几乎不占内存
# 注意:圆括号而不是方括号!
# 对于只遍历一次的场景,用生成器表达式更省内存
# 比如传给sum/max/min等聚合函数
total = sum(x**2 for x in range(10_000_000)) # 不需要额外括号
# 比 sum([x**2 for x in range(10_000_000)]) 省80MB内存
大数据场景:生成器处理大文件
# 场景:逐行处理一个10GB的日志文件
# 错误方式:一次性读入内存
lines = open('huge.log').readlines() # MemoryError!
# 正确方式:文件对象本身就是迭代器(逐行产出)
with open('huge.log') as f:
for line in f: # 逐行读取,内存中只有一行
if 'ERROR' in line:
process_error(line)
# 更复杂的场景:生成器函数做逐批处理
def read_in_batches(filepath, batch_size=10000):
batch = []
with open(filepath) as f:
for line in f:
batch.append(line.strip())
if len(batch) >= batch_size:
yield batch # 产出一批数据
batch = [] # 清空准备下一批
if batch: # 最后不满一批的数据
yield batch
# 使用:逐批处理,内存始终可控
for batch in read_in_batches('huge.log', batch_size=50000):
results = process_batch(batch) # 每批5万行
write_to_db(results)
实战经验:生成器导出5000万行数据
在某知名出海硬件品牌,需要把5000万行设备数据从数据库导出到CSV。如果一次性SELECT全部数据到内存再写文件,需要10GB+内存(数据库驱动会缓存全部结果)。
# 用生成器逐批读取+逐批写入
def fetch_in_batches(cursor, batch_size=100000):
while True:
rows = cursor.fetchmany(batch_size)
if not rows:
break
yield rows
cursor = conn.cursor()
cursor.execute('SELECT * FROM devices')
with open('output.csv', 'w') as f:
writer = csv.writer(f)
writer.writerow(header)
for batch in fetch_in_batches(cursor, 100000):
writer.writerows(batch)
# 5000万行数据,内存始终稳定在200MB以内
# 因为同一时间内存中最多只有10万行(一个batch)
效果:导出5000万行只用了200MB内存(不用生成器需要10GB+),总耗时15分钟。内存稳定意味着脚本可以在普通的8GB内存的机器上运行,不需要专门申请大内存实例。
yield from——生成器的组合
# yield from用于把一个生成器的所有值转发出去
def read_multiple_files(file_list):
for filepath in file_list:
yield from read_lines(filepath) # 转发每个文件的所有行
# 等价于:
def read_multiple_files(file_list):
for filepath in file_list:
for line in read_lines(filepath):
yield line
# yield from更简洁,而且性能稍好(少了一层循环)
面试高频追问
追问一:生成器能重复使用吗?
不能。生成器遍历完一次就耗尽了(StopIteration),不能重置。如果需要重复遍历,要么用列表,要么重新创建生成器对象。这是生成器和列表最大的区别——列表可以多次遍历,生成器只能单次。
追问二:send()和next()有什么区别?
next()只是获取下一个值。send(value)可以把value发送给生成器——yield表达式的返回值就是send的参数。这让生成器可以接收外部输入,实现协程(coroutine)的效果。但在大数据场景中send()用得很少。
追问三:Python的range()是生成器吗?
不是。range()是一个特殊的不可变序列对象——它支持随机访问(range(10)[5]可以直接得到5)、支持in判断(5 in range(10)是O(1)而不是O(N))、支持重复遍历。range()比生成器更强大但实现方式不同。range()只能产出等差数列,生成器可以产出任意序列。
总结
生成器面试讲清楚三层:① yield vs return(yield暂停/恢复+惰性求值,return一次性返回+内存占满);② 生成器表达式(圆括号替代方括号,传给sum/max等聚合函数时省内存);③ 大数据场景(逐行处理大文件、逐批读数据库、内存可控的流式处理)。能讲出5000万行数据用生成器导出内存稳定200MB的实际案例,面试官会觉得你善用Python特性解决工程问题。
下一篇聊Python的可变性和拷贝:浅拷贝和深拷贝踩了多少坑?函数参数传递到底是传值还是传引用?
你们用过生成器处理大数据吗?评论区聊聊~
关注「Wood聊大数据」| 9年大数据老兵,只讲大数据面试干货