Python技术迷

别再说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 Counter

counter = Counter(user_ids)
print(counter.most_common(10))

需要默认值就上 defaultdict:

from collections import defaultdict

scene_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%,是真能肉眼看见进程安静下来了。