别再说Python费内存了:这5个内置技巧让你省下80%资源
一上来内存就飙到 1.2G,这种锅很多人顺手就扣给 Python。 我一般不急着骂语言,先看代码。真看进去,十有八九不是 Python 吃得多,是写法太大手大脚:本来能一条一条过,非得一次性全塞进列表;本来字典里放个状态码就够了,偏要塞一坨重复字符串;本来只是遍历,结果顺手又复制一份。
Python 确实不是省内存的代表,但它也没离谱到“随便写都炸”。很多项目里,光把几个内置技巧用对,内存占用能立刻掉一大截。下面这 5 个,我自己排查脚本、清洗数据、跑批任务时用得最多。不是花活,都是现场真能省资源的东西。
先说最容易踩的:别动不动就把可迭代对象转成 list。
很多代码一上来就是这样:
lines = list(open("access.log", "r", encoding="utf-8"))
errors = [x for x in lines if"ERROR"in x]
日志文件稍微大一点,这一下就不止是一份数据了。文件内容进了 lines,筛选结果又来一份 errors,内存翻着倍涨。
这类场景直接按流处理:
with open("access.log", "r", encoding="utf-8") as f:
for line in f:
if"ERROR"notin line:
continue
parts = line.rstrip("\n").split("|")
if len(parts) >= 3:
print(parts[0], parts[2])
文件对象本身就是迭代器,一行一行读,不需要整文件搬进内存。很多人嘴上说 Python 费内存,代码里却先来一个 list(...),这就有点冤枉人了。
第二个地方,生成器表达式能别改列表推导就别改。
这个区别看着像语法题,跑起来差很多。
nums = [int(x) for x in range(10_000_000)]
total = sum(nums)
如果你只是为了求和,中间这 1000 万个整数根本没必要先落地。
total = sum(int(x) for x in range(10_000_000))
前者先生成完整列表,再交给 sum。后者边生成边消费。 数据量小时没感觉,真到几百万、上千万条,差距就出来了。
我平时看这类代码,凡是“只遍历一次”的中间结果,第一反应就是:这东西有没有必要变成列表。没有,就别存。
第三个,善用 collections.Counter 和 defaultdict,别自己堆一堆笨重结构。
有些统计代码喜欢这么写:
result = {}
for uid in user_ids:
if uid notin result:
result[uid] = {"count": 0, "last_scene": ""}
result[uid]["count"] += 1
result[uid]["last_scene"] = "pay"
这种写法的问题不是不能跑,是太臃肿。特别是数据量大时,字典套字典,再塞重复字段,内存涨得很快。
能拆开的就拆开。只统计次数,直接用 Counter:
from collections import Countercounter = Counter(user_ids)
print(counter.most_common(10))
需要默认值就上 defaultdict:
from collections import defaultdictscene_count = defaultdict(int)
for scene in scenes:
scene_count[scene] += 1
内置容器不是为了写着高级,是为了少造轮子、少放冗余数据。很多自定义结构一层套一层,看着“业务清晰”,内存可一点都不清晰。
第四个,字符串别反复拼,尤其是在循环里。
这种代码在线上脚本里我见过很多次:
content = ""
for row in rows:
content += f"{row['uid']},{row['score']}\n"
小数据还能忍,数据一多就开始反复创建新字符串。字符串是不可变对象,每拼一次,基本就是新开一块。
该用 join 的时候别硬顶:
lines = []
for row in rows:
lines.append(f"{row['uid']},{row['score']}")
content = "\n".join(lines)
如果数据本身也不小,连 lines 都不想留,那就直接写文件,别在内存里攒全文:
with open("score.csv", "w", encoding="utf-8") as f:
for row in rows:
f.write(f"{row['uid']},{row['score']}\n")
这地方很典型。很多人不是算法有问题,就是习惯问题。循环里拼字符串、循环里 append 大对象,最后把内存顶上去,还以为是解释器不行。
第五个,能用 set 去重就别先存全量再二次处理,但也别乱用副本。
比如导入用户手机号,常见写法:
phones = []
for row in data:
phones.append(row["phone"])unique_phones = list(set(phones))
先来一份列表,再转集合,再转列表,等于至少折腾两三份数据。 如果你的目标只是去重,直接一步到位:
unique_phones = {row["phone"] for row in data if row.get("phone")}
如果后面只是判断“是否存在”,那更不该转回列表:
blocked = {"13800000001", "13900000002"}for row in data:
if row["phone"] in blocked:
row["status"] = "reject"
集合占不占内存?占。 但它在去重、成员判断这种场景下,通常比你“列表先存全量,后面再洗一遍”更省,也更快。关键不是哪个类型绝对省,而是别让同一批数据在内存里来回复制。
最后补一个很多人不当回事的点:别迷信“缓存一下总没错”。
我见过不少 Python 脚本,函数里顺手就把中间结果全存全局变量,生怕重复计算。结果任务跑 20 分钟,缓存对象越积越多,最后进程常驻几 G。
缓存是拿内存换时间的。这个交换值不值,要看命中率,不是看写起来顺不顺手。
真到排查现场,我一般会先盯这几类地方:
# 1. 大文件是否整读
data = f.read()# 2. 只遍历一次的结果是否强转 list
rows = list(query_result)
# 3. 循环里是否在堆大对象
all_rows.append(big_dict)
# 4. 是否存在重复副本
new_data = old_data[:]
# 5. 是否做了没必要的缓存
cache[key] = result
Python 当然有内存开销,这没必要洗。对象模型、引用计数、动态特性,这些东西本来就比 C 之类重。 但业务代码里真正夸张的内存浪费,很多时候跟语言没多大关系,跟写法关系更大。
别再一出问题就说 Python 费内存。 先把 list、生成器、Counter、join、set 这些手边就有的东西用对。代码还是那点代码,机器还是那台机器,资源占用往往先下去一截。 很多时候,省下来的不是 20%,是真能肉眼看见进程安静下来了。