当前位置:首页>python>【一起学 Python】第 67 天:生成器的优势:惰性计算与内存节省

【一起学 Python】第 67 天:生成器的优势:惰性计算与内存节省

  • 2026-09-02 19:06:14
【一起学 Python】第 67 天:生成器的优势:惰性计算与内存节省

欢迎来到 Python 学习计划的第 67 天!🎉

昨天我们学习了 生成器函数与 yield 关键字,掌握了如何创建生成器。但为什么要使用生成器?它比普通列表好在哪里?

今天我们将深入探讨 生成器的核心优势:惰性计算(Lazy Evaluation)与内存节省。理解这一点,你才能写出高效处理大数据的 Python 代码!

一、什么是惰性计算?

1. 核心定义

惰性计算 是指推迟计算直到真正需要结果时才执行。生成器是惰性计算的典型实现,它不会预先计算所有值,而是在每次请求(next())时才计算下一个值。

2. 列表 vs 生成器

# 列表:立即计算所有值( eager evaluation)numbers_list = [x ** 2 for x in range(1000000)]  # 立即生成 100 万个元素# 生成器:惰性计算,按需生成(lazy evaluation)numbers_gen = (x ** 2 for x in range(1000000))   # 不会立即计算

二、内存对比实验

生成器最显著的优势是内存占用极小,且与数据量无关。

1. 使用 sys.getsizeof 对比

import sys# 列表占用大量内存numbers_list = [i for i in range(1000000)]print(f"列表内存:{sys.getsizeof(numbers_list):,} 字节")  # 约 8,448,728 字节 (8MB+)# 生成器几乎不占内存numbers_gen = (i for i in range(1000000))print(f"生成器内存:{sys.getsizeof(numbers_gen):,} 字节")  # 约 200 字节

2. range() 也是惰性的

Python 3 的 range 对象也是惰性序列。

# range 对象是惰性的r = range(1000000000)  # 10 亿个数print(f"range 内存:{sys.getsizeof(r)} 字节")  # 只有 48 字节# 转为列表会占用大量内存(不要运行,会卡死)# l = list(r)  # 需要约 40GB 内存

三、惰性计算的四大优势

特性

立即计算 (列表)

惰性计算 (生成器)

内存占用

与数据量成正比

固定且很小

启动时间

需要先生成所有数据

立即可用

无限序列

不可能(内存有限)

可以表示

中途停止

浪费已计算的资源

只计算需要的部分

示例:只取前 5 个

def expensive_calculation(n):    print(f"计算 {n}")    return n ** 2# 列表:全部计算(即使只需要前 5 个)result_list = [expensive_calculation(i) for i in range(100)]first_five = result_list[:5]  # 但已经计算了 100 次# 生成器:按需计算def gen_expensive(n):    for i in range(n):        yield expensive_calculation(i)gen = gen_expensive(100)first_five = [next(gen) for _ in range(5)]  # 只计算 5 次

四、实际应用示例

1. 处理大文件

避免一次性读取整个文件到内存。

# ❌ 错误做法:一次性读取def process_file_bad(filepath):    with open(filepath) as f:        lines = f.readlines()  # 大文件会耗尽内存    for line in lines:        process(line)# ✅ 正确做法:使用生成器逐行处理def process_file_good(filepath):    with open(filepath, encoding="utf-8") as f:        for line in f:  # 文件对象本身是迭代器            yield line.strip()# 即使文件有 10GB,内存占用也很小for line in process_file_good("huge_log.txt"):    if "ERROR" in line:        print(line)

2. 数据流水线(Pipeline)

构建流式数据处理链,每个步骤都是惰性的,数据不在中间积累。

def read_data(filepath):    """第一步:读取数据"""    with open(filepath) as f:        for line in f:            yield line.strip()def parse_data(lines):    """第二步:解析数据"""    for line in lines:        parts = line.split(",")        yield {"id": parts[0], "value": float(parts[1])}def filter_data(records, threshold):    """第三步:过滤数据"""    for record in records:        if record["value"] > threshold:            yield recorddef transform_data(records):    """第四步:转换数据"""    for record in records:        yield {**record, "doubled": record["value"] * 2}# 构建管道 - 数据流式处理,内存占用极小pipeline = transform_data(    filter_data(        parse_data(            read_data("data.csv")        ),        threshold=100    ))# 遍历时才真正执行for record in pipeline:    print(record)

3. 分批处理(Batching)

将大数据分批处理,控制内存峰值。

def batch_process(iterable, batch_size):    """将数据分批处理"""    batch = []    for item in iterable:        batch.append(item)        if len(batch) >= batch_size:            yield batch            batch = []    if batch:  # 处理剩余的        yield batch# 处理 100 万条数据,每批 1000 条data = range(1000000)for batch in batch_process(data, 1000):    # 每次只有 1000 条在内存中    process_batch(batch)

4. 无限数据流

模拟传感器数据或时间序列。

import randomimport timedef sensor_data():    """模拟传感器数据流"""    while True:        yield {            "timestamp": time.time(),            "temperature": random.uniform(20, 30),            "humidity": random.uniform(40, 60)        }def moving_average(data_stream, window_size):    """计算移动平均"""    window = []    for data in data_stream:        window.append(data["temperature"])        if len(window) > window_size:            window.pop(0)        yield sum(window) / len(window)# 实时处理传感器数据sensor = sensor_data()for avg in moving_average(sensor, 10):    print(f"平均温度:{avg:.2f}")    time.sleep(1)

5. 大数据聚合

结合 sum(), max() 等聚合函数使用生成器表达式。

# 错误:先转列表再求和,浪费内存# total = sum(list(x * 2 for x in range(10000000)))# 正确:直接用生成器求和total = sum(x * 2 for x in range(10000000))  # 内存占用极小

五、生成器 vs 列表:选择指南

场景

推荐

原因

数据量很大

生成器

节省内存

只遍历一次

生成器

更省内存

需要多次遍历

列表

生成器只能用一次

需要随机访问

列表

生成器只能顺序访问

数据流处理

生成器

流式处理

需要知道长度

列表

len() 只对列表有效

结合聚合函数

生成器

sum(), max() 等支持

何时不适合使用生成器?

  1. 需要随机访问:data[50] 无法实现。
  2. 需要多次遍历:生成器耗尽后需重新创建。
  3. 数据量小:10 个元素用列表即可,无需过度优化。
  4. 需要知道长度:生成器无法直接获取 len()。

六、类型提示与调试(结合进阶模块)

1. 类型提示

结合 [File 85](85-Python 3.10+ 新语法:联合类型.md) 和 typing 模块。

from typing import Generator, Iterabledef read_lines(filepath: str) -> Generator[str, None, None]:    with open(filepath) as f:        for line in f:            yield linedef process_data(data: Iterable[int]) -> Generator[int, None, None]:    for item in data:        yield item * 2

2. 调试技巧

生成器无法直接打印所有内容(会耗尽)。

# ❌ 调试陷阱gen = (x for x in range(5))print(list(gen))  # [0, 1, 2, 3, 4] - 但生成器已耗尽# ✅ 使用 itertools.tee 创建副本(仅用于调试)from itertools import teegen = (x for x in range(5))gen1, gen2 = tee(gen, 2)print(list(gen1))  # 用于调试# gen2 仍可使用

3. 内存分析工具

使用 tracemalloc 对比内存使用。

import tracemalloctracemalloc.start()# 列表方式numbers_list = [x ** 2 for x in range(100000)]snapshot1 = tracemalloc.take_snapshot()# 生成器方式numbers_gen = (x ** 2 for x in range(100000))list(numbers_gen)  # 消费生成器snapshot2 = tracemalloc.take_snapshot()# 打印统计print("列表方式的内存使用:")for stat in snapshot1.statistics('lineno')[:3]:    print(stat)

七、常见误区与注意事项

1. 一次性消费

生成器只能迭代一次,耗尽后需要重新创建。

gen = (x for x in range(3))print(list(gen))  # [0, 1, 2]print(list(gen))  # [] (已耗尽)# 正确:重新创建gen = (x for x in range(3))

2. 无法获取长度

必须遍历才知道有多少元素。

def gen_length(gen):    return sum(1 for _ in gen)  # 会耗尽生成器# 如果需要长度和数据,先转列表gen = (x for x in range(100))data = list(gen)print(len(data), data)

3. 不能回退

只能向前,不能回到之前的元素。

八、总结

知识点

说明

惰性计算

推迟计算直到需要结果时才执行

内存优势

生成器对象内存固定,列表与数据量成正比

适用场景

大文件、数据流、无限序列、ETL 管道

不适用场景

随机访问、多次遍历、需要长度

调试技巧

使用 itertools.tee 创建副本

类型提示

Generator[T, None, None] 或 Iterable[T]

核心要点

  1. 生成器通过惰性计算实现显著的内存节省。
  2. 适合处理大数据、流式数据、无限序列。
  3. 生成器只能迭代一次,无法随机访问。
  4. 结合聚合函数(sum, max)使用生成器表达式是高效模式。
  5. 根据需求选择列表还是生成器。

📌 明日预告:全局解释器锁 (GIL) 的现状

明天我们将进入 并发与迭代模块第四天!

  • 主题:全局解释器锁 (GIL) 的现状与 3.14 的无 GIL 模式
  • 核心问题:
    1. 什么是 GIL?为什么存在?
    2. GIL 对 CPU 密集型和 I/O 密集型任务有什么影响?
    3. 如何绕过 GIL 限制(多进程、C 扩展、Asyncio)?
    4. Python 3.13/3.14 的无 GIL 模式是什么?
    5. 生产环境应该如何选择并发方案?

💡 提前思考:

  1. 为什么 Python 多线程无法利用多核 CPU?
  2. 如果是 CPU 密集型任务,应该用多线程还是多进程?
  3. 无 GIL 版本稳定吗?适合生产环境吗?

掌握生成器的内存优势,是迈向高效 Python 编程的关键一步!明天深入理解 Python 并发底层限制!继续加油!

最新文章

随机文章