订阅更新

yield和return有什么区别?——生成器和迭代器

Python手册 第 8 题 约 7 分钟读完

本文目录
  1. 先说结论
  2. return vs yield——核心区别
  3. 生成器的执行流程——暂停和恢复
  4. 生成器表达式——列表推导式的惰性版本
  5. 大数据场景:生成器处理大文件
  6. 实战经验:生成器导出5000万行数据
  7. yield from——生成器的组合
  8. 面试高频追问
  9. 总结

先说结论

生成器是一种特殊的迭代器——函数中有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年大数据老兵,只讲大数据面试干货

刷题进度保存在当前浏览器