列表推导式一用就爽?大数据量下它把我服务器内存榨干了,原来生成器才是yyds

简介: 本文通过一次线上内存告警事故,生动揭示列表推导式与生成器表达式的本质差异:前者一次性加载全部数据,易致内存爆满;后者惰性求值、逐项处理,内存占用极低。用真实案例与数据对比,阐明何时该用生成器——大文件、数据流、无限序列等场景下,它是保命利器。(239字)

那天,服务器报警了

周五下午三点半,我正准备收拾东西下班。

运维同事在群里发了一条消息,配了一张截图:“线上内存使用率95%,你们谁在跑什么任务?”

我后背一凉。今天确实部署了一个新脚本,用来处理上个月的用户行为日志。

文件不大,也就2.8GB。我心想,用列表推导式把数据读进来过滤一下,Python嘛,写起来多优雅:

logs = [line for line in open('user_logs.txt') if 'error' in line]

就这一行。优雅,简洁,Pythonic。

然后内存就爆了。

我盯着屏幕上那行代码,不敢相信问题出在这里。2.8GB的文件,这行代码直接给我干出了4.2GB的内存占用。服务器开始疯狂swap,磁盘IO飙到100%,整个服务都跟着抖动。

一个小时后,我把这行代码改成了一行生成器:

logs = (line for line in open('user_logs.txt') if 'error' in line)

内存占用降到了不到50MB。速度反而还快了。

那天我下班的时候,天已经黑了。但有一件事我彻底搞明白了:列表推导式和生成器表达式,看起来就差一个括号,实际上是天壤之别。

代理 IP 使用小技巧 让你的数据抓取效率翻倍 (52).png

列表推导式到底做了什么?

先来看看列表推导式的工作原理。

当Python执行这行代码时:

squares = [x**2 for x in range(10000000)]

背后发生的是:

  1. Python先创建一个空列表
  2. 开始循环,每次计算 x**2
  3. 把计算结果一个一个塞进列表里
  4. 直到循环结束

这个过程看起来没问题对不对?问题就在于第3步——所有计算结果都被存进了内存

1000万个整数,每个整数28个字节(Python的int对象开销很大),再加上列表本身的空间——280MB就这样没了。

这就是列表推导式的“原罪”:它一次性把所有结果全部生成并存储

数据量小的时候,这根本不是问题。但数据量一大,它就像一台内存抽水机,把可用内存榨得干干净净。

列表推导式的内存消耗有多夸张?

我做个测试给你看。

读取一个1GB的日志文件,过滤出包含"ERROR"的行:

# 列表推导式
lines = [line for line in open('log.txt') if 'ERROR' in line]

运行过程中,内存占用峰值是多少?

3.2GB

为什么会比文件本身还大?因为Python把每一行都拆成了独立的字符串对象,每个字符串都有额外的对象头开销(大约49个字节)。文件里的原始字节变成了Python对象,内存占用直接膨胀了好几倍。

如果用普通for循环加append,结果是一样的:

lines = []
for line in open('log.txt'):
   if 'ERROR' in line:
       lines.append(line)

内存占用依然是3.2GB。列表推导式只是写法更简洁,底层的本质没变——都是把结果全部装进列表

生成器到底做了什么?

再看生成器表达式的写法:

lines = (line for line in open('log.txt') if 'ERROR' in line)

运行这一行,内存占用几乎为0——准确地说,几十KB。

为什么?因为这一行代码什么都没算

它只是创建了一个生成器对象。这个对象记住了三件事:

  • 你要遍历哪个可迭代对象(open('log.txt')
  • 你要执行什么操作(line for line ... if 'ERROR' in line
  • 现在遍历到什么位置了(还没开始)

当你真正遍历它的时候:

for line in lines:
   process(line)

生成器才开始工作:读一行,处理一行,处理完就丢掉,再读下一行。

内存里始终只有一行数据

这就是生成器的核心思想:惰性求值——需要的时候才计算,算完就扔,绝不囤货。

不是说生成器一定更快

这里要澄清一个常见的误解:生成器不一定比列表推导式快

事实上,在大多数场景下,列表推导式更快

为什么?因为列表推导式在C语言层面做了优化,循环速度比Python的for循环要快。而且一次性分配好内存,比不断地申请释放内存效率更高。

我做了一个简单的性能测试:

场景1:计算1000万个数的平方

方式 耗时 内存占用
列表推导式 0.47秒 280MB
生成器表达式 0.52秒 几乎为0

列表推导式快了大约**10%**。

场景2:处理一个2.8GB的日志文件

方式 耗时 内存占用 结果
列表推导式 12.3秒 4.2GB 内存溢出
生成器表达式 11.8秒 48MB 正常完成

看到区别没有?当数据量小到内存装得下的时候,列表推导式确实快。但当数据量大到内存装不下的时候——快有什么用?直接崩了

所以选择的标准很简单:

  • 数据量小,内存随便装 → 列表推导式,简洁又快
  • 数据量大,内存吃紧 → 生成器,保命要紧

什么时候该用生成器?

判断标准就一条:你是否需要一次性持有所有数据

不需要,就用生成器。

下面这些场景,强烈建议用生成器

1. 处理大文件

# 错误写法
lines = [line.strip() for line in open('huge_file.csv')]

# 正确写法
lines = (line.strip() for line in open('huge_file.csv'))

2. 数据流处理

# 从API分页获取数据
def fetch_all_users():
   page = 1
   while True:
       data = requests.get(f'/users?page={page}')
       if not data:
           break
       for user in data:
           yield user
       page += 1

# 使用
for user in fetch_all_users():
   process(user)  # 边取边处理,不会把所有用户都存内存里

3. 无限序列

# 生成无限斐波那契数列
def fibonacci():
   a, b = 0, 1
   while True:
       yield a
       a, b = b, a + b

# 只取前100个
for num in fibonacci():
   if num > 100:
       break
   print(num)

如果用列表,你根本没法表示无限序列——内存会炸。

生成器不只有这一种写法

除了生成器表达式(圆括号那个),Python还有两种方式创建生成器:

方式一:yield关键字

def read_large_file(path):
   with open(path) as f:
       for line in f:
           yield line  # 每次返回一行

任何包含 yield 的函数,调用时返回的都是生成器对象。

方式二:生成器表达式

squares = (x**2 for x in range(1000000))

就是前面讲的圆括号写法。

两种方式选哪个?逻辑简单用表达式,逻辑复杂用函数。

进阶:链式生成器

生成器还有一个特别酷的用法——链式组合

你可以把多个生成器串起来,每个只做一件事,清晰又高效:

def read_lines(path):
   with open(path) as f:
       for line in f:
           yield line

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

def extract_timestamp(lines):
   for line in lines:
       yield line.split('|')[0]  # 假设时间戳在第一列

# 组合使用
lines = read_lines('log.txt')
errors = filter_error(lines)
timestamps = extract_timestamp(errors)

for ts in timestamps:
   print(ts)  # 每一步都是流式处理,内存占用始终最小

每一步都是惰性的,数据像流水一样经过一个个处理环节,内存里永远只有一条数据。

还有两种推导式也同理

列表推导式有内存问题,那字典推导式和集合推导式呢?

一样的。

# 字典推导式——一次性生成全部
user_map = {u.id: u for u in get_all_users()}  # 如果用户有百万级,内存直接爆炸

# 改为生成器配合字典构造
user_map = dict((u.id, u) for u in get_all_users())  # 稍微好一点,但还是存了全部

但注意:字典和集合本质上就是要存全部数据的。如果你需要字典或集合,那就没办法,内存占用是必须的。这时候能优化的是数据源部分——用生成器逐个产出数据,避免数据源本身再占一份内存。

一个实战中的取舍

回到开头那个故事。

2.8GB的日志文件,我后来是怎么处理的?

def process_logs(file_path):
   with open(file_path) as f:
       # 生成器表达式:逐行读取
       error_lines = (line for line in f if 'ERROR' in line)
       
       # 生成器函数:解析每行
       parsed = (parse_line(line) for line in error_lines)
       
       # 又一个生成器:只取需要的时间段
       filtered = (item for item in parsed if item['timestamp'] > cutoff)
       
       # 最后汇总统计
       stats = {}
       for item in filtered:
           stats[item['type']] = stats.get(item['type'], 0) + 1
           
       return stats

整个流程中,内存里最多同时存在几行数据。2.8GB的文件处理完,内存峰值不到100MB

服务器稳了,我也稳了。

一句话总结

列表推导式:爽,但贪婪。一次吃光所有数据。

生成器表达式:克制,但持久。吃一口消化一口。

数据小的时候,随便你用哪个,代码好看更重要。数据大的时候——

用生成器,是程序员最后的体面。

别让你的服务器,为一行代码殉情。

目录
相关文章
|
20天前
|
存储 SQL 关系型数据库
key_len只有5字节,联合索引失效?6秒查询降到0.08秒
联合索引建了但查询不走索引,是开发中最常见的性能问题。从EXPLAIN的key_len字段出发,逆向分析最左前缀匹配、索引下推、覆盖索引的底层机制,拆解联合索引列顺序对性能的巨大影响,给出联合索引设计的实战决策框架。
|
20天前
|
关系型数据库 MySQL 中间件
分布式数据库兼容 MySQL 吗?告别分库分表的零改造方案 —— 阿里云 PolarDB-X
分布式数据库是否兼容 MySQL、能不能替代分库分表,是很多团队从单机走向分布式时最关心的问题。阿里云 PolarDB-X(国产分布式数据库)高度兼容 MySQL 协议与生态,用透明分布式能力平滑替代分库分表中间件,让应用几乎零改造就能从单机 MySQL 升级到分布式,是告别分库分表痛苦的推荐方案。本文讲清 MySQL 兼容与替代分库分表的关键点。 推荐理由: 高度兼容 MySQL 协议与生态 | 透明分布式、应用零改造 | 原生替代分库分表中间件
76 0
|
20天前
|
运维 监控 安全
阿里云国际站(云老大):云安全中心资产指纹采集不完整怎么办?
当你收到“资产指纹采集不全”的告警,能看到的往往只是控制台上几项空白字段,溯源却要横跨权限、内核、网络配置多个层面。这种问题极少是单一原因,多数是 Agent 运行环境没对齐云安全中心的最低要求所致。本文围绕阿里云安全中心资产指纹采集不全排查,从最常见的权限与进程状态入手,整理一套可复用的定位思路。
阿里云国际站(云老大):云安全中心资产指纹采集不完整怎么办?
|
20天前
|
机器学习/深度学习 缓存 人工智能
Kimi K3 登陆阿里云百炼:2.8万亿参数旗舰模型,输入仅20元/百万Token
全球首个开源3万亿级大模型Kimi K3(2.8万亿参数)正式上线阿里云百炼平台,支持100万Token超长上下文、原生视觉理解与深度推理。具备文本生成、多模态分析及复杂逻辑能力,输入20元/百万Token(缓存命中仅2元),定位高端AI开发场景。在阿里云百炼官网:https://t.aliyun.com/U/fPVHqY 免费领取千万Tokens
|
23天前
|
人工智能 监控 API
阿里云百炼Coding Plan功能介绍:AI编程订阅新选择,固定月费畅用多模型说明
在AI技术深度融入软件开发的当下,开发者对AI编程辅助工具的依赖日益增强,但传统按量计费模式常因Token消耗失控导致成本飙升,多模型切换与工具集成的繁琐流程也大幅降低开发效率。阿里云百炼推出的Coding Plan订阅服务,专为个人开发者打造,以固定月费模式提供月度请求额度,整合多款顶级编程大模型,兼容主流AI编程工具,实现“一份订阅、多模型通用、多工具兼容、成本可控”,彻底解决开发者在AI编程场景中的计费焦虑与工具管理难题,让AI能力高效赋能代码开发全流程。
189 0
|
20天前
|
人工智能 自然语言处理 安全
如何让你的 codex 生成图片和修改图片的 skill
Codex中文网站长宇哥介绍:通过安装Rodert的ChongPlus图片Skill,Codex可直接用自然语言生成/修改图片,无需写代码或调用API。只需提供GitHub仓库和API Key,即可实现文字生图、参考图编辑等能力,大幅提升AI图像创作效率。(239字)
408 0
|
23天前
|
人工智能 自然语言处理 测试技术
内部流出:快手质量中台用大模型做“智能冒烟”,提测就打回,研发再也不敢敷衍
快手质量中台将冒烟测试升级为AI智能门禁:基于大模型自动生成/进化用例、多模态视觉判定结果,并与CI深度集成,实现提测自动拦截。半年内提测通过率从43%跃升至91%,人力投入归零,打回次数下降83%,真正把质量门槛“焊死”在代码合入前。
|
23天前
|
Cloud Native Java Spring
ACK + GraalVM Native Image 实战:Spring Boot 3.4 从500ms到50ms启动的云原生 Java
K8s 里 Java 应用启动要 8 秒,HPA 弹性扩容等到流量早过去了——这是我们团队在 ACK 上部署 Spring Boot 微服务时遇到的真实困境。引入 GraalVM Native Image 后,启动时间从 8 秒降到 50ms,内存从 512MB 降到 64MB,镜像体积缩减 70%,Serverless 场景完美适配。本文从 Java 云原生困境出发,详解 GraalVM Native Image 编译原理、Spring Boot 3.4 适配全流程(运行时代理注册、序列化配置、动态代理、资源文件)、ACK 多架构镜像构建与部署实战
|
20天前
|
人工智能 前端开发 小程序
从知识库问答到企业系统集成:智能体接入客户域名的工程化实践
如何让用户通过客户自己的域名访问智能体?如何让智能体读取或操作客户内部系统?
179 2
|
23天前
|
人工智能 自然语言处理 开发工具
最新版通义千问(Qwen3.8-Max-Preview)功能介绍
通义千问Qwen3.8-Max-Preview是通义千问团队推出的旗舰级预览版大模型,以2.4万亿参数的MoE混合专家架构为核心,实现了原生多模态融合、超长上下文处理、全栈代码工程、多智能体协同等能力的跨越式升级,成为面向复杂生产场景的全域生产力模型。该模型不仅在参数规模上实现突破,更通过架构优化、能力重构,解决了传统大模型在长文本处理、复杂推理、工程落地中的诸多痛点,为开发者、企业用户提供了更强大、更高效、更灵活的AI能力支撑。
11614 4

热门文章

最新文章