Python 迭代器与生成器精讲:大幅降低内存占用

Python 迭代器与生成器精讲:大幅降低内存占用

处理大规模数据时,内存往往是第一道瓶颈。列表一次要把所有数据加载进内存,100 万条记录可能直接吃掉几百 MB。迭代器和生成器,就是 Python 给你的省内存利器


一、先搞清:什么是可迭代对象

能被 for 循环遍历的,都叫可迭代对象(Iterable)

python 复制代码
python
list, tuple, str, dict, set  → 都是可迭代对象

但可迭代 ≠ 迭代器。


二、迭代器(Iterator):边用边取,用完即丢

迭代器是一个记住了当前位置的对象,每次只返回一个值,取完就没了。

python 复制代码
python
it = iter([1, 2, 3])
print(next(it))  # 1
print(next(it))  # 2
print(next(it))  # 3
print(next(it))  # StopIteration ← 取完了

关键特性

特性 说明
__iter__() 方法 返回自己
__next__() 方法 返回下一个值,没有则抛 StopIteration
只能前进,不能回退 像磁带,不能倒带
只能遍历一次 读完就空了

列表是可迭代对象,但不是迭代器:

python 复制代码
python
lst = [1, 2, 3]
hasattr(lst, '__next__')  # False

it = iter(lst)
hasattr(it, '__next__')   # True ← 这才是迭代器

for 循环的本质,就是不断调用 next(),直到捕获 StopIteration


三、生成器(Generator):写起来像函数,用起来像迭代器

生成器是一种特殊的迭代器 ,不用写类,用 yield 就能造出来。

1. 生成器函数

python 复制代码
python
def count_up_to(n):
    i = 1
    while i <= n:
        yield i      # 暂停,返回 i,下次从这里继续
        i += 1

gen = count_up_to(5)
print(next(gen))  # 1
print(next(gen))  # 2

yieldreturn 的区别:

return yield
执行后 函数结束,局部变量销毁 暂停,局部变量保留
调用次数 一次 可以多次 resume
返回值 一个结果 每次返回一个值

2. 生成器表达式

语法和列表推导式一样,只是把 [] 换成 ()

ini 复制代码
python
# 列表推导式 → 一次性生成所有数据,占内存
squares_list = [x**2 for x in range(1000000)]   # ~40 MB

# 生成器表达式 → 用一个取一个,几乎不占内存
squares_gen = (x**2 for x in range(1000000))    # ~几十字节

四、内存对比:数据说话

处理 1000 万个整数的平方:

方式 内存占用 说明
列表 [x**2 for x in range(10_000_000)] ~80 MB 全部加载进内存
生成器 (x**2 for x in range(10_000_000)) ~120 字节 只存当前状态
迭代器 iter(range(10_000_000)) ~48 字节 连计算都省了

差距是 6 位数级别的。


五、实战:什么时候该用生成器

✅ 适合用生成器的场景

场景 原因
读取大文件(GB 级) 不用一次性读入内存
数据管道/流处理 上游产一个,下游消一个
无限序列 列表存不下,生成器可以无限产生
中间结果不需要保留 用完就丢,没必要存

示例:逐行读取大文件

python 复制代码
python
# ❌ 错误:一次性读入内存
with open('huge_log.txt') as f:
    lines = f.readlines()  # 内存爆炸

# ✅ 正确:生成器逐行读取
with open('huge_log.txt') as f:
    for line in f:         # f 本身就是迭代器
        if 'ERROR' in line:
            print(line)

open() 返回的文件对象本身就是迭代器,每次 for 循环只读一行。

示例:数据管道

python 复制代码
python
def read_log(filename):
    with open(filename) as f:
        for line in f:
            yield line.strip()

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

def extract_time(lines):
    for line in lines:
        yield line.split()[0]

# 管道组合:数据从左到右流动,全程不存中间结果
pipeline = extract_time(filter_errors(read_log('app.log')))
for t in pipeline:
    print(t)

三个生成器串联,内存占用始终只有一行数据的大小


六、yield from:生成器嵌套的简洁写法

csharp 复制代码
python
def gen1():
    yield 1
    yield 2

def gen2():
    yield from gen1()  # 等价于逐个 yield gen1() 的值
    yield 3

print(list(gen2()))  # [1, 2, 3]

yield from 还能透明传递 send()throw(),是生成器组合的推荐写法。


七、常见误区

误区 真相
生成器比列表快 不一定。生成器省内存,但有 yield 开销。小数据用列表更快
生成器可以倒回去 不能。要重遍历,重新调用函数
yield 后的代码不执行 执行,只是暂停。下次 next()yield 下一行继续
生成器表达式可以重复用 不能。用完就空了,要重新创建

八、一张表总结

概念 本质 内存 复用性 典型写法
列表 一次性存储所有元素 ✅ 可重复 [x for x in range(n)]
迭代器 边用边取,记住位置 极低 ❌ 一次性 iter(list)
生成器 yield 实现的迭代器 极低 ❌ 一次性 (x for x in range(n))
生成器函数 包含 yield 的函数 极低 ❌ 一次性 def f(): yield x

核心结论

  • 内存紧张时,能用生成器就别用列表
  • for 循环天然支持迭代器,改起来成本很低。
  • 文件对象、range()map()filter() 本身都是迭代器,直接用就行,别包一层 list()

省下来的内存,就是你程序能处理的数据量上限。

相关推荐
AINative软件工程2 小时前
Tool Schema 写得好,模型少出错:5 个工程师必知的设计原则
后端·openai
AINative软件工程2 小时前
AI 写的代码,Review 要怎么改?我们团队的 15 条 PR 检查清单
后端·openai
武子康2 小时前
Java-21 深入浅出 MyBatis 手写ORM框架2 手写Resources、MappedStatment、XMLBuilder等
java·后端
techdashen2 小时前
在 Fly.io 上使用 Rust 构建远程开发环境:从 Tokio 到 eBPF
开发语言·后端·rust
摇滚侠3 小时前
Spring 零基础入门到进阶 面向切面 AOP 52-60
java·后端·spring
雪隐3 小时前
AI股票小助手07-TA-Lib 技术指标计算实战
人工智能·后端
掘金者阿豪3 小时前
一本书读懂微积分!
后端
Cosolar3 小时前
深入理解 LangChain Callback 机制:从入门到实战
人工智能·后端·面试
我登哥MVP3 小时前
Spring Boot 从“会用”到“精通”:SpringBoot MVC 请求处理全流程
java·spring boot·后端·spring·mvc·maven·intellij-idea