首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Python的列表推导式把我写崩了,原来圆括号和方括号不是一回事

Python的列表推导式把我写崩了,原来圆括号和方括号不是一回事

原创
作者头像
风一样的男子
发布于 2026-09-04 13:55:17
发布于 2026-09-04 13:55:17
1640
举报
文章被收录于专栏:编程教程编程教程

讲个真事儿。

上个月我写一个数据处理脚本,要读一个2.8GB的日志文件,过滤出所有包含"ERROR"的行。我心想,用列表推导式多优雅啊,一行代码搞定:

代码语言:javascript
复制
lines = [line for line in open('user_logs.txt') if 'error' in line]

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

然后服务器内存就爆了。运维同事在群里发了一张截图,内存使用率95%,问我跑什么任务了。我盯着屏幕上那行代码,不敢相信问题出在这里。2.8GB的文件,这行代码直接给我干出了4.2GB的内存占用。服务器开始疯狂swap,磁盘IO飙到100%,整个服务都跟着抖动。

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

代码语言:javascript
复制
lines = (line for line in open('user_logs.txt') if 'error' in line)

就换了个括号。方括号变成了圆括号。内存占用从4.2GB降到了不到50MB。

那天我彻底搞明白了一件事:列表推导式的方括号和生成器表达式的圆括号,看起来就差一个符号,实际上是天壤之别。

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

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

当你写下这行代码时:

代码语言:javascript
复制
squares = [x**2 for x in range(10000000)]

Python背后做的是这样几件事:

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

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

1000万个整数,每个整数在Python里是一个对象,占用28个字节,再加上列表本身的空间——280MB就这样没了。

这就是列表推导式的“原罪”:它一次性把所有结果全部生成并存储。数据量小的时候,这根本不是问题。但数据量一大,它就像一台内存抽水机,把可用内存榨得干干净净。

回到那个日志文件的例子。为什么2.8GB的文件会吃掉4.2GB内存?因为Python把每一行都拆成了独立的字符串对象,每个字符串都有额外的对象头开销(大约49个字节)。文件里的原始字节变成了Python对象,内存占用直接膨胀了好几倍。

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

代码语言:javascript
复制
lines = []
for line in open('log.txt'):
    if 'ERROR' in line:
        lines.append(line)

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

生成器表达式做了什么?

再看生成器表达式:

代码语言:javascript
复制
squares = (x**2 for x in range(10000000))

这次Python没有创建列表,没有预计算任何平方值,没有往内存里塞任何东西。它只做了一件事:创建了一个生成器对象。

这个生成器对象大概只占100个字节左右的内存。不管你要处理100万个元素还是100亿个元素,它占用的内存几乎不变。

为什么?因为生成器根本不存储数据,它只存储一个“配方”——怎么计算下一个值。你问它要一个值,它就计算一个给你;你不问,它就不算。这就是所谓的 “惰性求值” 。

打个比方:

  • 列表推导式像你去超市买菜,把所有菜都买回来堆在冰箱里,要用的时候直接从冰箱拿。冰箱够大还好,冰箱不够大就塞爆了。
  • 生成器表达式像你楼下的菜店,做菜的时候才下楼买一棵葱,吃完再做下一道菜再下楼买。冰箱里永远只有当下要用的那一点东西。

看起来一样,用起来完全不一样

咱们来做个对比实验。

内存占用

代码语言:javascript
复制
import sys

big_list = [x for x in range(1000000)]
big_gen = (x for x in range(1000000))

print("列表内存:", sys.getsizeof(big_list))    # 约 8MB
print("生成器内存:", sys.getsizeof(big_gen))   # 约 100 字节

100万个元素,列表占了8MB,生成器只占了100个字节。差了8万倍。

能不能反复用

列表可以反复遍历,想遍历几次就遍历几次:

代码语言:javascript
复制
lst = [x for x in range(3)]
print([x for x in lst])  # [0, 1, 2]
print([x for x in lst])  # [0, 1, 2] —— 还能用

生成器是一次性的,遍历完就没了:

代码语言:javascript
复制
gen = (x for x in range(3))
print([x for x in gen])  # [0, 1, 2]
print([x for x in gen])  # [] —— 空了

因为生成器不存储数据,它像一巻电影胶片,只能从头播到尾,播完就没了。想再看一遍?重新创建一个生成器。

能不能用索引

列表支持索引,想取第几个就取第几个:

代码语言:javascript
复制
lst = [x for x in range(10)]
print(lst[5])  # 5 —— 随便取

生成器不支持索引:

代码语言:javascript
复制
gen = (x for x in range(10))
print(gen[5])  # TypeError: 'generator' object is not subscriptable

因为生成器根本没把所有数据存下来,它只知道“下一步怎么算”,不知道第5个是什么。

一个让你彻底理解的例子

来看一个经典的场景。假设你要计算1到1000万个数的平方和:

代码语言:javascript
复制
# 列表推导式版本
sum([x*x for x in range(10000000)])

这段代码的问题在哪?你可能以为问题出在range(10000000)。其实不是。在Python 3中,range()返回的是一个惰性的range对象,它像一个“取号机”,你叫一个号它才吐一个号,本身几乎不占内存。

真正的问题出在方括号上。[x*x for x in range(10000000)]会强制把所有平方结果全部提前计算出来,塞进一个巨大的列表里,然后再交给sum()去求和。

而改成圆括号之后:

代码语言:javascript
复制
sum(x*x for x in range(10000000))

sum()每需要一个值,生成器就计算一个值给它,算完就扔掉,永远不积压。内存占用从几百MB降到了几乎为零。

而且,把生成器表达式作为单参数函数调用时,可以省略外层的圆括号。所以sum(x*x for x in range(...))是合法的,不需要写成sum((x*x for x in range(...)))。

那到底该用哪个?

记住一条简单的判断标准:

数据量小,或者需要反复使用、随机访问——用列表推导式(方括号) 。

配置列表、小数据集过滤、需要多次遍历的场景,列表推导式更合适。而且列表推导式的底层是用C实现的,执行速度通常比普通for循环快20%到30%。

数据量大,或者只需要遍历一次——用生成器表达式(圆括号) 。

处理百万级以上的数据、读取大文件、流式数据处理,生成器表达式是绝对的内存救星。

对比项

列表推导式 []

生成器表达式 ()

返回类型

列表

生成器对象

计算时机

立即全部计算

惰性计算,需要时才算

内存占用

与元素数量成正比

固定大小,约100字节

可迭代次数

可反复遍历

只能遍历一次

索引支持

支持

不支持

适用场景

小数据集、需反复使用

大数据集、流式处理

还有几个坑,顺便说一下

坑一:别以为圆括号是元组推导式

很多人第一次看到(x for x in range(10)),会以为这是“元组推导式”。不是的,Python没有元组推导式。圆括号括起来的这个玩意儿是生成器表达式,返回的是生成器对象,不是元组。

想生成元组?用tuple(x for x in range(10))。

坑二:生成器表达式里面的for语句,错误是延迟报的

列表推导式在定义的时候就会执行所有检查:

代码语言:javascript
复制
[x*y for x in range(3) for z in range(5)]  # 立即报错:y没定义

生成器表达式不一样,它只在第一个for语句执行时做检查,后面的for语句要等到你真正开始迭代的时候才检查:

代码语言:javascript
复制
g = (x*y for x in range(3) for z in range(5))  # 不报错
next(g)  # 这时候才报错:y没定义

这个特性有时候会让人困惑,明明写了语法错误,程序却没立刻报错,等跑了好一会儿才炸。

坑三:别在需要列表的地方用生成器

生成器不支持索引、不支持切片、不支持len()。如果你接下来要对数据做下标访问、多次遍历,老老实实用列表推导式。非要用生成器的话,可以用list()把它转成列表,但这样就失去了内存优势。

总结

方括号和圆括号的区别,核心就一句话:

方括号是“全部给我”,圆括号是“要一个给一个”。

方括号的列表推导式,简单直接,适合小数据。圆括号的生成器表达式,精打细算,适合大数据。

下次写代码的时候,多问自己一句:这些数据我真的需要全部存下来吗?如果答案是否定的,把方括号换成圆括号。

就这么简单。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 列表推导式到底干了什么?
  • 生成器表达式做了什么?
  • 看起来一样,用起来完全不一样
    • 内存占用
    • 能不能反复用
    • 能不能用索引
  • 一个让你彻底理解的例子
  • 那到底该用哪个?
  • 还有几个坑,顺便说一下
  • 总结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档