列表推导式和生成器表达式 笔记

1 阅读4分钟

第一部分:语法与核心区别(仅差一个括号)

语法上,生成器表达式把列表推导式的 [] 换成 () ,但背后的运行机制天差地别。

python

# 列表推导式:立即计算,返回 list
list_comp = [x**2 for x in range(5)]
print(list_comp)          # [0, 1, 4, 9, 16]
print(type(list_comp))    # <class 'list'>

# 生成器表达式:惰性计算,返回 generator 对象
gen_exp = (x**2 for x in range(5))
print(gen_exp)            # <generator object <genexpr> at 0x...>
print(type(gen_exp))      # <class 'generator'>

内存占用对比(实战震撼)

假设要处理 100 万个数字,列表推导式会瞬间吃掉大量内存,而生成器表达式几乎不占内存:

python

import sys

list_comp = [i for i in range(1000000)]
gen_exp = (i for i in range(1000000))

print(sys.getsizeof(list_comp))  # 约 8MB(实际取决于系统)
print(sys.getsizeof(gen_exp))    # 仅 112 字节(固定大小!)

第二部分:生成器表达式的独有特性——“一次性”

生成器是一个迭代器,它像“单程票”,遍历完就没了,不能回头。

python

gen = (x for x in ['A', 'B', 'C'])

# 第一次遍历
print(list(gen))  # ['A', 'B', 'C']

# 第二次遍历(已经耗尽)
print(list(gen))  # []  —— 空的!没有报错,但数据已丢失

避坑:如果你想复用数据,就别用生成器表达式,老老实实用列表。


第三部分:与面向对象(OOP)和类型提示的联动

在类的设计中,返回生成器表达式是处理“大数据集合”的标准模式。

类型提示:生成器的类型是 Generator[YieldType, SendType, ReturnType],但通常你用 Iterator[YieldType] 标注返回值就足够了。

python

from typing import Iterator

class LargeLogReader:
    """大型日志文件读取器(OOP 封装)"""
    
    def __init__(self, file_path: str) -> None:
        self.file_path = file_path
    
    # 返回生成器,内存友好
    def filter_errors(self, keyword: str = "ERROR") -> Iterator[str]:
        """逐行读取,包含特定关键词的行(生成器表达式)"""
        with open(self.file_path, 'r', encoding='utf-8') as f:
            # 生成器表达式:不一次读入全部,逐行实时过滤
            return (line.strip() for line in f if keyword in line)
    
    # 另一种写法:yield 函数(功能相同,写法不同)
    def filter_errors_yield(self, keyword: str = "ERROR") -> Iterator[str]:
        with open(self.file_path, 'r', encoding='utf-8') as f:
            for line in f:
                if keyword in line:
                    yield line.strip()

调用时,如果外部只要前 10 行,生成器只读取前 10 行日志,绝不会把整个几十 GB 的日志文件加载到内存


第四部分:与异步编程(Async)的特殊关系(高阶)

非常重要:在异步函数 async def 中,不能直接使用 await 在生成器表达式内部(因为 await 必须出现在函数内)。

但是,你可以用生成器表达式批量构建异步任务,或者在异步循环中消费生成器。

实战场景:异步批量下载文件,但限制并发数量,且结果用生成器流式吐出。

python

import asyncio
from typing import Iterator, Awaitable, Any

async def fetch_data(url: str) -> dict[str, Any]:
    await asyncio.sleep(0.5)  # 模拟网络 IO
    return {"url": url, "data": "fake_data"}

async def process_batch(urls: list[str]) -> None:
    # 生成器表达式 生成协程对象(注意:这里没有执行,只是懒构建)
    coroutines = (fetch_data(url) for url in urls)
    
    # 注意:生成器内部的协程并未启动。我们可以逐次消费它,限制并发
    results = []
    for coro in coroutines:
        result = await coro  # 每次 await 一个,相当于逐个执行(非并发)
        results.append(result)
        # 如果想并发,必须用 asyncio.gather,但那样就不需要生成器了,直接用列表

结论:在异步中,生成器表达式适合逐个处理流式数据(如分页 API 响应),而列表推导式适合 asyncio.gather(*[fetch(i) for i in items]) 进行批量并发


第五部分:何时必须用生成器表达式?(黄金法则)

场景推荐方案理由
数据集极大(百万级以上)生成器表达式防止内存溢出(OOM)
只需遍历一次(如 sumanyall生成器表达式计算完即丢弃,省内存且代码简洁
需要切片、反复索引、排序、多次遍历列表推导式生成器不支持索引,无法复用
求总和、最大值等聚合操作sum(x**2 for x in range(1e7))妙用:注意这里不用双层括号,sum 自认生成器

经典一行代码(极简高性能)

python

# 求 1 亿以内偶数的平方和(内存占用几乎为 0)
total = sum(x**2 for x in range(100_000_000) if x % 2 == 0)
print(total)

避坑终极总结

  1. 别把它当列表用:生成器没有 len(),没有 [0] 索引,只能 next() 或 for 遍历。

  2. 小心 any() 和 all() 的短路效应

    python

    gen = (x for x in [1, 2, 0, 5])
    print(any(gen))  # True(遇到 1 就停了,后面的 2,0,5 没消费)
    print(list(gen)) # [2, 0, 5] —— 1 已经没了!
    
  3. 性能权衡:生成器生成元素有计算开销。如果数据量小,列表推导式比生成器表达式更快(因为生成器有状态切换损耗)。数据量小(<1000)用列表,数据量大用生成器。