36. 生成器与惰性迭代
在第 35 章中,我们通过可迭代对象(iterable)与迭代器(iterator)学习了 Python 中的迭代(iteration)如何工作。我们看到,迭代器会在被请求时一次返回一个值,这让 Python 能够在不一次性把所有内容加载到内存中的情况下处理序列。现在我们将探索生成器(generator)——Python 创建迭代器最优雅、最实用的方式。
生成器是可以暂停并恢复执行的函数(function),它会在被请求时一次产生一个值,而不是预先计算所有值并将它们存入内存。这种方法——称为惰性求值(lazy evaluation)——意味着值只在需要时才生成,使其成为 Python 编写内存高效代码最强大的特性之一。
36.1) 生成器是什么,以及为什么它们有用
36.1.1) 创建大型列表的问题
让我们先理解生成器要解决的问题。假设你需要处理一百万个数字的序列。下面是使用列表(list)的传统做法:
# 创建一个包含一百万个平方数的列表
def get_squares_list(n):
"""Return a list of squares from 0 to n-1."""
squares = []
for i in range(n):
squares.append(i * i)
return squares
# 这会在内存中创建一个包含 1,000,000 个数字的列表
numbers = get_squares_list(1_000_000)
print(f"First five squares: {numbers[:5]}") # Output: First five squares: [0, 1, 4, 9, 16]这种做法有一个显著问题:它会一次性在内存中创建并存储全部一百万个数字,即使你只需要一次处理一个数字。对于更大的数据集或更复杂的计算,这可能会消耗巨量内存,甚至导致程序崩溃。
36.1.2) 引入生成器:按需计算值
生成器(generator)是一种特殊类型的函数(function),它会在被请求时一次产生一个值。它不会构建并返回完整列表(list),而是按需计算每个值,并在两次调用之间“记住”上次执行到哪里。
下面是用生成器实现的相同功能:
# 创建一个平方数生成器
def get_squares_generator(n):
"""Generate squares from 0 to n-1, one at a time."""
for i in range(n):
yield i * i # yield 会暂停函数并返回一个值
# 这会创建一个生成器对象,而不是列表
squares_gen = get_squares_generator(1_000_000)
print(squares_gen) # Output: <generator object get_squares_generator at 0x...>
# 一次获取一个值
print(next(squares_gen)) # Output: 0
print(next(squares_gen)) # Output: 1
print(next(squares_gen)) # Output: 4生成器不会预先计算全部一百万个平方数。相反,它只会在你对其调用 next() 时计算下一个平方数。在两次调用之间,生成器会“暂停”并记住它的状态(i 的当前值)。
36.1.3) 内存效率:关键优势
对于大型数据集,列表(list)与生成器(generator)之间的内存差异会变得非常明显。我们来对比一下:
import sys
# 列表方式:存储所有值
def squares_list(n):
return [i * i for i in range(n)]
# 生成器方式:按需计算值
def squares_generator(n):
for i in range(n):
yield i * i
# 对比 100,000 个数字的内存使用情况
list_result = squares_list(100_000)
gen_result = squares_generator(100_000)
print(f"List size in memory: {sys.getsizeof(list_result):,} bytes")
# Output: List size in memory: 800,984 bytes (actual size may vary)
print(f"Generator size in memory: {sys.getsizeof(gen_result)} bytes")
# Output: Generator size in memory: 200 bytes (actual size may vary)列表会消耗超过 800 KB 的内存,而生成器只使用 200 字节——与它最终会产生多少个值无关。生成器只保存函数的状态(i 的当前值以及从哪里继续执行),而不是实际的值序列。
36.1.4) 生成器何时有用
生成器在几种常见场景中表现出色:
处理大文件:
def read_large_file(filename):
"""Generate lines from a file one at a time."""
with open(filename, 'r') as file:
for line in file:
yield line.strip()
# 在不将超大日志文件全部加载到内存的情况下进行处理
for line in read_large_file('huge_log.txt'):
if 'ERROR' in line:
print(line)无限序列:
def fibonacci():
"""Generate Fibonacci numbers indefinitely."""
a, b = 0, 1
while True:
yield a
a, b = b, a + b
# 永远生成斐波那契数列(或者直到你不再请求)
fib = fibonacci()
print([next(fib) for _ in range(10)])
# Output: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]36.1.5) 生成器就是迭代器
正如我们在第 35 章学到的,生成器实际上是一种特殊的迭代器(iterator)。它们会自动实现迭代器协议(__iter__() 和 __next__()),这就是为什么它们能与 for 循环(loop)无缝配合:
def countdown(n):
"""Generate countdown from n to 1."""
while n > 0:
yield n
n -= 1
# 生成器可以直接用于 for 循环
for num in countdown(5):
print(num)
# Output:
# 5
# 4
# 3
# 2
# 1当你在 for 循环中使用生成器时,Python 会自动反复对其调用 next(),直到生成器耗尽(抛出 StopIteration)。
36.2) 使用 yield 创建生成器函数
36.2.1) yield 语句:暂停与恢复
yield 语句让一个函数变成生成器。当 Python 遇到 yield 时,会做一件特殊的事:它不会返回值并结束函数,而是暂停函数并返回该值。下一次你对生成器调用 next() 时,执行会从 yield 语句之后继续。
下面是一个简单示例,用来演示这种暂停与恢复的行为:
def simple_generator():
"""Demonstrate how yield pauses execution."""
print("Starting generator")
yield 1
print("Resuming after first yield")
yield 2
print("Resuming after second yield")
yield 3
print("Generator finished")
gen = simple_generator()
print("Created generator")
# Output:
# Created generator
print(f"First value: {next(gen)}")
# Output:
# Starting generator
# First value: 1
print(f"Second value: {next(gen)}")
# Output:
# Resuming after first yield
# Second value: 2
print(f"Third value: {next(gen)}")
# Output:
# Resuming after second yield
# Third value: 3
try:
next(gen)
except StopIteration:
print("Generator exhausted - no more values")
# Output:
# Generator finished
# Generator exhausted - no more values注意,函数的执行过程与 next() 调用是交错进行的。每次 yield 都会暂停函数,而每次 next() 都会从上次停止的位置继续执行。
36.2.2) 生成器状态:记住局部变量
生成器会在两次 yield 之间记住它的所有局部变量。这使它们非常适合在多次调用之间维护状态:
def counter(start=0):
"""Generate sequential numbers starting from start."""
current = start
while True:
yield current
current += 1
# 生成器会在 yield 之间记住 'current'
count = counter(10)
print(next(count)) # Output: 10
print(next(count)) # Output: 11
print(next(count)) # Output: 12
# 每个生成器都有自己独立的状态
count1 = counter(0)
count2 = counter(100)
print(next(count1)) # Output: 0
print(next(count2)) # Output: 100
print(next(count1)) # Output: 1
print(next(count2)) # Output: 101每次生成器在 yield 处暂停并在下一次 next() 调用时恢复,变量 current 都会被保留。这使生成器能够从上次的值继续计数。每个生成器实例都维护其独立的状态。
36.2.3) 在循环中 yield:最常见的模式
生成器最常见的用法是在循环(loop)里产出值。这个模式会生成一串值的序列:
def even_numbers(start, end):
"""Generate even numbers in the given range."""
current = start if start % 2 == 0 else start + 1
while current <= end:
yield current
current += 2
# 使用生成器
evens = even_numbers(1, 20)
print(list(evens))
# Output: [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]循环的每次迭代都会 yield 一个值,然后在再次调用 next() 时继续下一次迭代。
36.2.4) 多个 yield 语句
一个生成器可以在代码的不同位置包含多个 yield 语句。执行会按顺序流经它们:
def process_data(data):
"""Generate processed data with status messages."""
yield "Starting processing..."
cleaned = [item.strip().lower() for item in data]
yield f"Cleaned {len(cleaned)} items"
unique = list(set(cleaned))
yield f"Found {len(unique)} unique items"
for item in sorted(unique):
yield item
# 处理一些数据
data = [" Apple ", "Banana", "apple", "Cherry", "BANANA"]
processor = process_data(data)
for result in processor:
print(result)
# Output:
# Starting processing...
# Cleaned 5 items
# Found 3 unique items
# apple
# banana
# cherry这种模式适用于需要执行一些初始化工作、产出状态信息,然后再产出实际数据的生成器。
36.3) 生成器表达式 vs 列表推导式
36.3.1) 介绍生成器表达式
在第 34 章中,我们学习了列表推导式(list comprehensions)——一种创建列表(list)的简洁方式。生成器表达式(generator expressions)使用几乎相同的语法,但创建的是生成器而不是列表。
生成器表达式本质上是一种用紧凑形式编写简单生成器函数的方法。对比下面两种等价写法:
# 生成器函数
def squares_function(n):
for x in range(n):
yield x * x
# 生成器表达式——做同样的事情
squares_expression = (x * x for x in range(10))
# 两者都会创建生成器对象
gen1 = squares_function(10)
gen2 = squares_expression
print(type(gen1)) # Output: <class 'generator'>
print(type(gen2)) # Output: <class 'generator'>
# 两者会产生相同的值
print(list(squares_function(10))) # Output: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
print(list(squares_expression)) # Output: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]其语法几乎与列表推导式相同。差异在于:使用圆括号 () 而不是方括号 [];并且列表推导式会创建列表,而生成器表达式会创建生成器:
# 列表推导式——在内存中创建完整列表
squares_list = [x * x for x in range(10)]
print(squares_list)
# Output: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
# 生成器表达式——创建生成器对象
squares_gen = (x * x for x in range(10))
print(squares_gen)
# Output: <generator object <genexpr> at 0x...>
# 转换为列表以查看值
print(list(squares_gen))
# Output: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]生成器表达式既提供了与列表推导式相同的简洁语法,又拥有生成器的内存效率。
36.3.2) 内存对比:何时重要
对于小型序列,列表推导式与生成器表达式之间的内存差异可以忽略不计。但对于大型序列,这种差异就会变得显著:
import sys
# 小序列——差异很小
small_list = [x for x in range(100)]
small_gen = (x for x in range(100))
print(f"Small list: {sys.getsizeof(small_list)} bytes")
# Output: Small list: 920 bytes (actual size may vary)
print(f"Small generator: {sys.getsizeof(small_gen)} bytes")
# Output: Small generator: 192 bytes (actual size may vary)
# 大序列——差异巨大
large_list = [x for x in range(1_000_000)]
large_gen = (x for x in range(1_000_000))
print(f"Large list: {sys.getsizeof(large_list):,} bytes")
# Output: Large list: 8,448,728 bytes (actual size may vary)
print(f"Large generator: {sys.getsizeof(large_gen)} bytes")
# Output: Large generator: 192 bytes (actual size may vary)无论生成器最终会产生多少个值,它的大小都保持不变——它只存储表达式与当前状态。而列表必须将所有值存入内存,这就是为什么列表的大小会随着元素数量成比例增长。
36.3.3) 在函数调用中使用生成器表达式
当将生成器表达式直接传给消费可迭代对象(iterable)的函数时,生成器表达式尤其优雅。如果生成器表达式是唯一参数,你可以省略额外的括号:
# 计算平方和而不创建列表
total = sum(x * x for x in range(100)) # Note: no extra parentheses needed
print(total)
# Output: 328350
# 求变换后值的最大值
numbers = [1, 2, 3, 4, 5]
max_square = max(x * x for x in numbers)
print(max_square)
# Output: 25
# 检查是否有任意值满足条件
data = [10, 15, 20, 25, 30]
has_large = any(x > 100 for x in data)
print(has_large)
# Output: False这种模式既内存高效又易读。像 sum()、max()、min()、any() 和 all() 这样的函数会一次处理生成器的一个值,从不创建中间列表。
36.3.4) 使用生成器表达式进行过滤
生成器表达式支持与列表推导式相同的条件逻辑:
# 过滤偶数
numbers = range(20)
evens = (x for x in numbers if x % 2 == 0)
print(list(evens))
# Output: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
# 变换并过滤
words = ["hello", "world", "python", "programming"]
long_upper = (word.upper() for word in words if len(word) > 5)
print(list(long_upper))
# Output: ['PYTHON', 'PROGRAMMING']36.3.5) 生成器表达式不够用的时候
生成器表达式简洁且优雅,但也有局限。当你需要以下能力时,应使用生成器函数:
复杂逻辑:
# 对生成器表达式来说过于复杂
def process_log_lines(filename):
"""Process log file with complex logic."""
with open(filename, 'r') as file:
for line in file:
line = line.strip()
if not line or line.startswith('#'):
continue # 跳过空行与注释
parts = line.split('|')
if len(parts) >= 3:
timestamp, level, message = parts[0], parts[1], parts[2]
if level in ('ERROR', 'CRITICAL'):
yield {
'timestamp': timestamp,
'level': level,
'message': message
}多个 Yield 或状态:
# 生成器表达式无法跨迭代维护状态
def running_total(numbers):
"""Generate running total of numbers."""
total = 0
for num in numbers:
total += num
yield total
numbers = [1, 2, 3, 4, 5]
print(list(running_total(numbers)))
# Output: [1, 3, 6, 10, 15]错误处理:
# 生成器表达式无法处理异常
def safe_divide(numbers, divisor):
"""Generate division results, handling errors."""
for num in numbers:
try:
yield num / divisor
except ZeroDivisionError:
yield float('inf')36.4) 何时用生成器而不是列表
36.4.1) 大型数据集:最主要的使用场景
使用生成器最有说服力的原因是处理大量数据时的表现。如果你在处理数百万条记录,生成器可能决定了程序是平稳运行还是直接崩溃。
糟糕的做法——把整个文件加载进内存:
# DON'T DO THIS with large files
def count_errors_bad(filename):
"""Load entire file into memory - will crash with large files."""
with open(filename, 'r') as file:
lines = file.readlines() # Loads ENTIRE file into memory
error_count = 0
for line in lines:
if 'ERROR' in line:
error_count += 1
return error_count
# If the file is 10 GB, this tries to load 10 GB into memory!好的做法——使用生成器:
def read_log_lines(filename):
"""Generate lines from a log file one at a time."""
with open(filename, 'r') as file:
for line in file:
yield line.strip()
def count_errors_good(filename):
"""Count errors without loading entire file into memory."""
error_count = 0
for line in read_log_lines(filename):
if 'ERROR' in line:
error_count += 1
return error_count
# 即使是 GB 级别的日志文件,这也能高效工作
# 因为它一次只在内存中保留一行
count = count_errors_good('huge_application.log')
print(f"Found {count} errors")生成器方式一次处理一行,因此无论文件大小如何,内存占用都保持不变。一个 10 GB 的文件与一个 10 KB 的文件使用的内存量相同。
36.4.2) 无限或长度未知的序列
生成器非常适合那些你无法提前知道长度的序列,或者在概念上是无限的序列:
def user_input_stream():
"""Generate user inputs until they type 'quit'."""
while True:
user_input = input("Enter a number (or 'quit'): ")
if user_input.lower() == 'quit':
break
try:
yield int(user_input)
except ValueError:
print("Invalid number, try again")
# 随到随处理用户输入
total = 0
count = 0
for number in user_input_stream():
total += number
count += 1
print(f"Running average: {total / count:.2f}")你无法创建一个长度未知的列表,但生成器可以自然地处理这种情况。
36.4.3) 链式变换:构建数据流水线
当你需要对数据应用多步变换时,生成器允许你在不创建中间列表(list)的情况下链式组合操作:
# 将数字通过多个阶段进行变换
def generate_numbers(n):
"""Generate numbers from 1 to n."""
for i in range(1, n + 1):
yield i
def square_numbers(numbers):
"""Generate squares of input numbers."""
for num in numbers:
yield num * num
def keep_even(numbers):
"""Generate only even numbers."""
for num in numbers:
if num % 2 == 0:
yield num
# 链式组合生成器——不创建中间列表
numbers = generate_numbers(10)
squared = square_numbers(numbers)
even_squares = keep_even(squared)
# 处理结果
print(list(even_squares))
# Output: [4, 16, 36, 64, 100]每个阶段一次处理一个值,并把它传递给下一个阶段。这既内存高效,也让你能够处理比可用 RAM 更大的数据集。
如果不使用生成器,你就需要中间列表:
# 非生成器方式——创建中间列表
numbers = list(range(1, 11)) # [1, 2, 3, ..., 10]
squared = [n * n for n in numbers] # [1, 4, 9, ..., 100]
even_squares = [n for n in squared if n % 2 == 0] # [4, 16, 36, 64, 100]
# 使用生成器——不创建中间列表
numbers = (i for i in range(1, 11))
squared = (n * n for n in numbers)
even_squares = (n for n in squared if n % 2 == 0)
print(list(even_squares))
# Output: [4, 16, 36, 64, 100]对于一个包含三个阶段、处理一百万个项目的流水线来说,列表方式会创建三个各含一百万个项目的列表。生成器方式一次只在内存中保留一个值。
36.4.4) 什么时候列表比生成器更好
尽管生成器有优势,但它们并不总是正确的选择。当你需要以下特性时,请使用列表:
多次迭代:
# 列表——可以迭代多次
numbers = [1, 2, 3, 4, 5]
print(sum(numbers)) # Output: 15
print(max(numbers)) # Output: 5 (works fine)
# 生成器——只能迭代一次
numbers_gen = (x for x in range(1, 6))
print(sum(numbers_gen)) # Output: 15
print(max(numbers_gen)) # Output: ValueError: max() iterable argument is empty如果你需要多次处理同一份数据,请用列表。
随机访问:
# 需要按索引访问元素——使用列表
students = ['Alice', 'Bob', 'Charlie', 'Diana']
print(students[2]) # Output: Charlie
# 生成器不支持索引
students_gen = (name for name in students)
# students_gen[2] # ERROR: 'generator' object is not subscriptable长度信息:
# 需要知道长度——使用列表
data = [1, 2, 3, 4, 5]
print(f"Processing {len(data)} items")
# 生成器没有长度
data_gen = (x for x in data)
# len(data_gen) # ERROR: object of type 'generator' has no len()小型数据集:
# 对于小型数据集,列表就足够了,而且更方便
small_data = [x * 2 for x in range(10)]
# 在这里生成器节省的内存并不显著
# 而列表更灵活36.4.5) 实用决策指南
下面是一份在生成器与列表之间做选择的实用指南:
在以下情况下使用生成器:
- 处理大文件或大型数据集
- 处理数据流或用户输入
- 构建数据处理流水线
- 内存效率很重要
- 你只需要迭代一次
- 序列是无限的或非常长
在以下情况下使用列表:
- 数据集很小(通常 < 10,000 个项目)
- 你需要多次迭代
- 你需要按索引随机访问
- 你需要知道长度
- 你需要把数据传给期望列表的代码
36.4.6) 在生成器与列表之间转换
在需要时,你可以轻松地在生成器与列表之间转换:
# 生成器转列表
numbers_gen = (x * 2 for x in range(5))
numbers_list = list(numbers_gen)
print(numbers_list)
# Output: [0, 2, 4, 6, 8]
# 列表转生成器(使用生成器表达式)
numbers_list = [1, 2, 3, 4, 5]
numbers_gen = (x for x in numbers_list)这种灵活性意味着,你可以先为了效率使用生成器,然后只在需要列表特性时再转换为列表:
# 先用生成器实现内存效率
numbers = (x for x in range(1, 1001))
filtered = (x for x in numbers if x % 7 == 0)
# 当你需要多次迭代时再转换为列表
multiples_of_seven = list(filtered)
# 现在你可以使用列表特性
print(f"Count: {len(multiples_of_seven)}")
# Output: Count: 142
print(f"First: {multiples_of_seven[0]}")
# Output: First: 7
print(f"Last: {multiples_of_seven[-1]}")
# Output: Last: 994
# 可以迭代多次
total = sum(multiples_of_seven)
average = total / len(multiples_of_seven)
print(f"Average: {average:.1f}")
# Output: Average: 500.5生成器是 Python 中最优雅的特性之一,用于编写内存高效代码。它们让你能够处理大型数据集、构建数据流水线,以及处理无限序列——同时保持代码干净且可读。随着经验增长,你会逐渐形成直觉:何时生成器是完成任务的合适工具。