游乐游手机版
首页/业界动态/文章详情

处理 10GB 文件不爆内存,Python 迭代器凭什么

时间:2026-04-16 11:21
1 场景案例:Python大文件处理 去年遇到一个数据清洗任务,日志文件足足有12GB。同事的第一反应很直接:用readlines()把文件全部读进内存,再逐条处理。结果程序跑了三分钟,直接报了个MemoryError。 其实只改了一个地方:把readlines()换成直接遍历文件对象。就这么一个

1. 场景案例:Python大文件处理

去年遇到一个数据清洗任务,日志文件足足有12GB。同事的第一反应很直接:用readlines()把文件全部读进内存,再逐条处理。结果程序跑了三分钟,直接报了个MemoryError

其实只改了一个地方:把readlines()换成直接遍历文件对象。就这么一个调整,内存峰值从接近14GB降到了不足100MB。

背后的区别很简单:前者是一次性加载,后者则利用了Python迭代器的惰性特性。很多开发者写了多年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 迭代器

这两个概念经常被混用,但它们确实不是一回事。

  • 可迭代对象:拥有__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

yieldreturn的关键区别在于:return执行完函数就彻底结束了,而yield会暂停函数执行,保存当前状态,下次调用next()时再从暂停的地方继续。这正是生成器节省内存的秘诀——它不事先存储所有结果,而是“用一次,算一次”。

(1) 生成器表达式

还有一种更轻量的创建方式:生成器表达式。只需把列表推导式的方括号[]换成圆括号()

# 列表推导式 - 一次性生成所有结果,全部加载到内存
squares = [x*x for x in range(1000000)]  # 占内存约8.06MB

# 生成器表达式 - 惰性计算,用一次算一次
squares = (x*x for x in range(1000000))  # 内存占用极小,约200字节

当数据量达到一定规模时,这两种方式的区别就不再是“快一点”或“慢一点”,而是“能跑”和“根本跑不了”的天壤之别。

5. 实战场景

(1) 场景一:大文件处理

# 错误写法:一次性加载
with open('huge.log') as f:
    lines = f.readlines()  # 返回list,全部加载到内存
    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迭代器与生成器的原理,以及能否在关键时刻做出正确的应用选择。

来源:https://www.51cto.com/article/840720.html
上一篇一加Ace 6至尊版「王牌觉醒」亮相,新机已定档本月见 下一篇小米上线「电池升级」服务,曝支持13系列手机更换更大容量电池
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
九号N1机甲风电动车发布 模拟声浪轻量化车架3499元起
业界动态 · 2026-05-29

九号N1机甲风电动车发布 模拟声浪轻量化车架3499元起

九号发布N1机甲风电动车系列,三款起售价3499元。N170极速47km h,轻量化车架;N185极速55km h,可选模拟声浪;旗舰N190极速60km h,标配模拟声浪及双通道ABS,7月上市。

九号2026新品发布会最强阵容连发4款新车重新定义好车标准
业界动态 · 2026-05-29

九号2026新品发布会最强阵容连发4款新车重新定义好车标准

九号公司发布2026年新品,推出N1、M1、M3及Fz5四款新车,覆盖电摩与电自领域。N1主打短轴距声光电酷玩体验,M1配备双通道ABS与100公里真续航,M3下放AXC车架技术,Fz5首搭载双向转把功能。同时推出3年原厂换新质保等用户权益。

世界超级摩托车锦标赛阿拉贡站张雪机车超级杆位赛获亚军
业界动态 · 2026-05-29

世界超级摩托车锦标赛阿拉贡站张雪机车超级杆位赛获亚军

5月29日,世界超级摩托车锦标赛(WSBK)阿拉贡站传来一则引人瞩目的消息——中国摩托车制造商“张雪机车”旗下的法国车手瓦伦丁·德比斯,在WorldSSP组别的超级杆位赛中成功夺得第二名。 先简要科普一下赛事背景:世界超级摩托车锦标赛(WSBK)是由国际摩托车联合会于1988年创立的顶级公路摩托车赛

英雄联盟海克斯大乱斗重大更新 移除羁绊新增技能符文
业界动态 · 2026-05-29

英雄联盟海克斯大乱斗重大更新 移除羁绊新增技能符文

英雄联盟海克斯大乱斗将在26 12版本移除羁绊系统,上线技能符文体系。该符文能重构技能释放逻辑,实现布里茨钩五人、拉克丝定全队等效果。部分原有羁绊效果转为独立专属符文,更新预计2026年6月中旬登陆国服。

领克10/10+正式上市限时价16.99-23.59万号称弯道之王
业界动态 · 2026-05-29

领克10/10+正式上市限时价16.99-23.59万号称弯道之王

```html 5月29日晚间,领克终于将其备受关注的中大型运动纯电轿车正式推向市场——领克10与领克10+同步上市,官方直接打出“弯道之王”的旗号。我们先不深究它是否真能“弯道超车”,单从价格来看,就已经颇具冲击力。 先奉上一张价格速览表,让大家心里有个底: 领克 10 701 长续航 Max:指