3. 生成器与迭代器:列表推导式 vs 生成器表达式、yield关键字详解、懒加载模式实战

这一章我们来聊聊 Python 里一个特别实用的内存优化手段——生成器与迭代器。说实话,我见过太多人一上来就用列表推导式,结果内存直接爆了。我自己也踩过这个坑,后来才真正理解了「懒加载」的威力。

3.1 列表推导式 vs 生成器表达式

先看一个最常见的场景。你有一个大列表,想对它做点变换。很多人会这么写:

# 列表推导式——一次性生成所有数据
squares = [x**2 for x in range(1000000)]
print(len(squares))  # 1000000,内存里全占着

这段代码跑起来,内存占用直接飙升。为什么?因为列表推导式会一次性把所有元素算出来,存到内存里。你想想看,如果数据量是 1000 万呢?

换成生成器表达式:

# 生成器表达式——按需生成,不占内存
squares_gen = (x**2 for x in range(1000000))
print(squares_gen)  # <generator object <genexpr> at 0x...>

嗯,这里要注意。生成器表达式用的是圆括号,不是方括号。它不会立即计算,而是返回一个生成器对象。只有当你迭代它的时候,才会一个一个地产生值。

核心区别一句话:列表推导式是「全都要」,生成器表达式是「用的时候再说」。

我在项目中遇到过这样一个情况:处理一个 500 万行的日志文件,同事用列表推导式做过滤,结果内存直接飙到 2GB,程序卡死了。换成生成器表达式后,内存占用降到 50MB 左右。差距就是这么大。

对比维度 列表推导式 生成器表达式
语法 [expr for item in iterable] (expr for item in iterable)
计算时机 立即计算全部 惰性计算,按需生成
内存占用 高(O(n)) 低(O(1))
可重复使用 可以多次遍历 只能遍历一次
适用场景 数据量小,需要多次访问 数据量大,只需遍历一次

我的建议:如果你不确定数据量有多大,优先用生成器表达式。等确定数据量很小、需要多次访问时,再转成列表也不迟。

3.2 yield 关键字详解

生成器表达式适合简单的变换。但如果你需要更复杂的逻辑——比如带状态、带条件判断、甚至需要暂停恢复——那就得用 yield 了。

说白了,yield 就是函数的「暂停键」。普通函数遇到 return 就结束了,但 yield 会让函数暂停,把值抛出来,下次调用时从暂停的地方继续。

def read_large_file(file_path):
    """逐行读取大文件,每次只返回一行"""
    with open(file_path, 'r') as f:
        for line in f:
            yield line.strip()  # 暂停在这里,返回一行

# 使用
for line in read_large_file('huge_log.txt'):
    process(line)  # 每次只处理一行,内存友好

我记得有一次要处理一个 10GB 的 CSV 文件。如果用 readlines(),内存直接炸。用 yield 逐行读取,内存占用始终只有几十 KB。这就是懒加载的威力。

再来看一个更复杂的例子——带状态的生成器:

def fibonacci():
    """生成斐波那契数列,无限序列"""
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

# 只取前10个
fib = fibonacci()
for _ in range(10):
    print(next(fib), end=' ')  # 0 1 1 2 3 5 8 13 21 34

注意看,这个生成器永远不会结束(while True)。但因为它用 yield 暂停了,所以不会占满内存。你取多少,它就生成多少。

曾经踩过的坑:我曾经在生成器里用了 return 而不是 yield,结果函数只返回第一个值就结束了。记住:生成器函数里用 yield,不要用 return 返回值(return 会触发 StopIteration)。

3.3 懒加载模式实战

懒加载(Lazy Loading)说白了就是「用到才加载」。这个模式在资源压缩里特别重要。我们来看几个实战场景。

场景一:分页查询

假设你要从数据库查 100 万条记录。一次性查出来?内存扛不住。用懒加载:

def paginated_query(cursor, table, page_size=1000):
    """懒加载分页查询,每次只取一页"""
    offset = 0
    while True:
        sql = f"SELECT * FROM {table} LIMIT {page_size} OFFSET {offset}"
        cursor.execute(sql)
        rows = cursor.fetchall()
        if not rows:
            break
        for row in rows:
            yield row
        offset += page_size

# 使用
for row in paginated_query(cursor, 'users'):
    process_user(row)  # 每次只处理1000条

这个模式我用了很多次。数据库查询最怕的就是一次性加载太多数据。用生成器做分页,内存占用可控,代码还简洁。

场景二:树形结构遍历

处理文件系统、组织架构等树形数据时,懒加载特别有用:

def walk_files(root_dir):
    """递归遍历目录,懒加载每个文件"""
    for entry in os.scandir(root_dir):
        if entry.is_dir():
            yield from walk_files(entry.path)  # yield from 委托给子生成器
        else:
            yield entry.path

# 使用
for file_path in walk_files('/var/log'):
    if file_path.endswith('.log'):
        analyze(file_path)  # 一次只处理一个文件

这里用了 yield from,它是 Python 3.3 引入的语法糖。说白了就是把子生成器的 yield 委托给外层,省得你手动写循环。

场景三:数据管道

懒加载最强大的用法是串联成管道。每个生成器只做一件事,然后串起来:

def read_lines(file_path):
    with open(file_path) as f:
        for line in f:
            yield line

def filter_errors(lines):
    for line in lines:
        if 'ERROR' in line:
            yield line

def parse_timestamp(lines):
    for line in lines:
        parts = line.split(' ')
        yield {'timestamp': parts[0], 'message': ' '.join(parts[1:])}

# 串联管道——数据流式处理,内存占用极低
pipeline = parse_timestamp(
    filter_errors(
        read_lines('app.log')
    )
)

for record in pipeline:
    save_to_db(record)  # 每次只处理一条记录

你看,每个函数只做一件事,通过生成器串联起来。数据像流水一样流过每个环节,内存里永远只有一条记录。这就是懒加载模式的精髓。

懒加载的核心原则:

  • 绝不一次性加载全部数据
  • 每次只处理一个元素
  • 用生成器串联成数据管道
  • 内存占用 = 单个元素大小,与数据总量无关

下面这张图展示了生成器与迭代器的核心关系:

生成器与迭代器核心关系图 迭代器协议 __iter__() + __next__() 生成器 yield / yield from 生成器表达式 (x for x in data) 懒加载模式:按需生成,一次一个,内存 O(1) 读取数据 过滤转换 解析处理 输出/存储 💡 关键点: • 生成器是迭代器的语法糖实现,自动维护状态 • 生成器表达式是生成器的简洁写法,适合简单变换

实战建议:我一般这样选:

  • 简单变换 → 生成器表达式 (x for x in data)
  • 复杂逻辑 → 生成器函数 def gen(): yield x
  • 需要状态 → 生成器函数 + 局部变量
  • 需要委托 → yield from sub_generator()

最后说一句:生成器和迭代器不是银弹。如果你需要多次遍历数据,或者数据量很小,列表推导式反而更合适。但如果你在做资源压缩、内存优化,生成器绝对是你工具箱里最趁手的工具之一。