处理 10GB 文件不爆内存,Python 迭代器凭什么
1. 场景案例:Python大文件处理
我曾接手一个数据清洗项目,需要处理一个12GB的日志文件。团队最初的方案是使用readlines()将整个文件加载到内存中,再进行逐行处理。这个操作直接导致了MemoryError,程序在运行三分钟后崩溃。
解决方案其实很简单:将readlines()替换为直接遍历文件对象。仅此一项调整,内存使用峰值就从接近14GB骤降至100MB以下。
这背后的核心差异在于数据加载模式:前者是急切加载,后者则利用了Python迭代器的惰性求值特性。许多开发者即便有多年经验,也习惯于用列表推导式处理所有数据。虽然列表推导式并非不能用,但在处理大规模数据集时,迭代器和生成器才是内存高效的正确选择。
2. 什么是迭代器
从本质上讲,迭代器是一个实现了特定协议的对象,它知道如何按需逐个“产出”元素。你每次请求下一个元素,它就提供下一个;当数据耗尽时,它会发出明确的终止信号。
Python中常见的for x in something:语法,其底层机制就是调用迭代器协议。该协议的核心由两个方法构成:
__iter__:返回迭代器对象本身。__next__:返回序列中的下一个元素。若没有更多元素,则抛出StopIteration异常。
这就是迭代器协议的全部,两个方法,简洁而强大。
class Counter:
def __init__(self, max_num):
self.max_num = max_num
self.current = 0
def __iter__(self):
return self
def __next__(self):
if self.current >= self.max_num:
raise StopIteration
self.current += 1
return self.current
for i in Counter(5):
print(i) # 输出:1, 2, 3, 4, 5
上面的Counter类就是一个自定义迭代器。__iter__返回自身,__next__负责递增计数并返回当前值,达到上限后抛出异常以终止迭代。
3. 可迭代对象 vs 迭代器
这两个术语常被混淆,但它们在Python中有着明确的区别。
- 可迭代对象:实现了
__iter__()方法,能够返回一个迭代器对象。例如列表(list)、元组(tuple)、字符串(str)、字典(dict)。 - 迭代器:不仅实现了
__iter__()方法,还实现了__next__()方法,因此自身就可以被遍历。
以列表为例,它是可迭代对象,但不是迭代器。你可以用for循环遍历它,但不能直接对其调用next()函数。
lst = [1, 2, 3]
next(lst) # TypeError: list object is not an iterator
it = iter(lst) # 通过iter()函数获取其迭代器
next(it) # 1
next(it) # 2
next(it) # 3
next(it) # 抛出StopIteration异常
这里的iter()内置函数,实际上就是调用了对象内部的__iter__()方法。
关键点总结:
- 可迭代对象不一定是迭代器。
- 迭代器一定是可迭代对象。
- 可迭代对象可以通过
iter()函数获得一个迭代器。
4. 生成器:迭代器的快捷方式
手动实现一个完整的迭代器类需要定义两个方法,略显繁琐。生成器提供了一种更为简洁的语法来创建迭代器,它会自动满足迭代器协议的所有要求。
创建生成器最常用的方式是使用yield关键字。任何包含yield语句的函数都会自动变为生成器函数。
def counter(max_num):
current = 0
while current < max_num:
current += 1
yield current
for i in counter(5):
print(i) # 输出:1, 2, 3, 4, 5
yield与return的关键区别在于执行状态:return会终止函数并返回结果,而yield会暂停函数执行,保留所有局部状态,待下次调用next()时从暂停点继续执行。这正是生成器节省内存的核心机制——它不预计算和存储所有结果,而是按需生成。
(1) 生成器表达式
还有一种更轻量的创建方式:生成器表达式。其语法与列表推导式类似,只需将方括号[]替换为圆括号()。
# 列表推导式 - 急切求值,一次性生成所有结果并存入内存
squares = [x*x for x in range(1000000)] # 内存占用显著
# 生成器表达式 - 惰性求值,按需计算,内存占用极低
squares = (x*x for x in range(1000000)) # 内存占用极小,仅约200字节
当数据规模增长到百万甚至千万级别时,这两种方式的差异就不再是性能上的细微差别,而是决定了程序能否成功运行的根本性问题。
5. 实战场景
(1) 场景一:大文件处理
# 错误写法:急切加载,内存风险高
with open('huge.log') as f:
lines = f.readlines() # 返回列表,全部加载到内存
for line in lines:
process(line)
# 正确写法:利用文件对象自身的迭代器特性进行惰性读取
with open('huge.log') as f:
for line in f: # 文件对象是迭代器,每次迭代只读取一行到内存
process(line)
Python的文件对象本身就是一个迭代器,直接遍历它会触发解释器按行惰性读取,从而有效避免内存溢出。
(2) 场景二:数据流处理
def read_api_pages(url):
page = 1
while True:
response = requests.get(url, params={'page': page})
data = response.json()
if not data:
break
yield from data # 将子迭代器的值直接产出
page += 1
for item in read_api_pages('https://api.example.com/items'):
process(item)
这里使用了yield from语法,它能将子迭代器产生的值直接传递出去。这种模式非常适合处理分页API、数据库游标等需要连续获取数据流的场景。
(3) 场景三:管道式数据处理
def read_csv(filepath):
with open(filepath) as f:
for line in f:
yield line.strip().split(',')
def filter_age(records, min_age):
for record in records:
if int(record[2]) >= min_age:
yield record
def format_output(records):
for record in records:
yield f"{record[0]}|{record[1]}|{record[2]}"
# 像组装流水线一样组合各个生成器
data = read_csv('users.csv')
filtered = filter_age(data, 18)
formatted = format_output(filtered)
for line in formatted:
print(line)
每个函数都是一个独立的生成器,数据像在流水线(pipeline)中传递。整个过程无需额外的列表或容器存储中间结果,内存效率极高。
6. 常见坑
第一个坑:迭代器和生成器是“一次性消耗品”。
gen = (x for x in range(5))
list(gen) # 第一次消耗,得到 [0, 1, 2, 3, 4]
list(gen) # 第二次尝试,得到 [] —— 生成器已耗尽
迭代器或生成器在遍历一次后即被耗尽。若需重复使用,要么将其结果转换为列表,要么重新创建生成器对象。
第二个坑:生成器函数返回的是生成器对象,而非执行结果。
def my_gen():
yield 1
yield 2
yield 3
result = my_gen() # 此处仅获得一个生成器对象,函数体内的代码并未执行
# 需要通过遍历(如for循环或next())来触发yield语句执行
新手常会困惑:明明调用了函数,为何没有输出?请记住,调用生成器函数返回的是一个待“激活”的生成器对象,真正的计算发生在迭代过程中。
7. 总结
最后,我们厘清这几个核心概念的关系:
- 可迭代对象 (Iterable):实现了
__iter__()方法。 - 迭代器 (Iterator):是可迭代对象的子集,额外实现了
__next__()方法。__iter__()通常返回self,并能维持迭代状态。文件对象即属于此类。 - 生成器 (Generator):一种特殊的迭代器,由生成器函数或生成器表达式创建。除了标准的迭代器方法,还拥有
send(),throw(),close()等方法,通过yield关键字实现。
归根结底,迭代器和生成器的核心优势在于惰性求值:按需访问、按需计算,用一次算一次,对内存极其友好。它们的协议设计简洁,只需实现__iter__和__next__两个方法。生成器则提供了更便捷的语法糖,yield让一个普通函数轻松转变为迭代器。
处理小规模数据时,列表与生成器的差异可能微不足道。但当数据量攀升时,这种选择往往决定了程序是稳健运行,还是因资源耗尽而崩溃。
文章开头的12GB日志文件案例,后来成为了技术面试中的一个经典问题。题目本身并不复杂,但它能清晰地区分出一个开发者是否真正理解了Python迭代器与生成器的工作原理,以及能否在关键场景下做出正确的架构决策。



