别再说Python费内存了:这5个内置技巧让你省下80%资源
别再一上来就说 Python 费内存。
我见过不少面试回答,张口就是“Python 对象开销大、解释器慢、GC 不行”。这话没错,但太粗了。真到线上脚本把内存打满,第一眼我一般不怀疑 Python,先看你是不是把 500 万行数据一次性塞进了 list。
这种代码最常见:
defload_orders(path):
rows = []
with open(path, "r", encoding="utf-8") as f:
for line in f:
rows.append(line.strip().split(","))
return rows
看着没毛病,跑小文件也没毛病。换成线上导出的订单明细,内存直接往上爬。不是 Python 突然变笨了,是你让它把所有数据都背在身上跑。
我一般先加一段很土的监控:
import os
import psutildefmark(tag):
rss = psutil.Process(os.getpid()).memory_info().rss / 1024 / 1024
print(f"[mem] {tag}: {rss:.1f} MB")
跑到哪一段涨得离谱,先别优化,先把它找出来。
第一个技巧:生成器,别急着 list
处理日志、CSV、导出文件,能一行一行过,就别一次性全读。
改成这样:
defiter_orders(path):
with open(path, "r", encoding="utf-8") as f:
for line in f:
cols = line.rstrip("\n").split(",")
if len(cols) < 5:
continue
yield {
"order_id": cols[0],
"user_id": cols[1],
"amount": int(cols[3] or0),
"status": cols[4],
}defsum_paid_amount(path):
total = 0
for order in iter_orders(path):
if order["status"] == "PAID":
total += order["amount"]
return total
这里省的不是一点点内存。原来 list 里挂着所有行,现在内存里基本只留当前这一行。
面试里讲生成器,别只背 yield。你就说:大文件清洗、日志扫描、批量校验,优先用生成器把“全量持有”变成“流式处理”。这个回答比“生成器是惰性求值”有用多了。
第二个技巧:用 sum / any / all 这类内置函数接生成器
我很烦这种写法:
amounts = [o["amount"] for o in iter_orders(path) if o["status"] == "PAID"]
total = sum(amounts)
中间那个 amounts 没什么存在价值,还白白占内存。
直接写:
total = sum(
o["amount"]
for o in iter_orders(path)
if o["status"] == "PAID"
)
注意这里不是方括号,是圆括号里的生成器表达式。
判断有没有异常订单也一样,别先收集:
has_bad_order = any(
o["amount"] < 0ornot o["order_id"]
for o in iter_orders(path)
)
any 找到一个满足条件的就停,后面不扫了。这个细节面试官一般会追问,因为它不只是省内存,也省时间。
第三个技巧:字段固定的对象,用 __slots__
Python 普通对象默认带一个 __dict__,方便是方便,但对象多了以后,这个字典开销很明显。
比如脚本里临时组装几十万条用户状态:
classUserState:
def__init__(self, uid, level, locked):
self.uid = uid
self.level = level
self.locked = locked
如果字段固定,我会直接改:
classUserState:
__slots__ = ("uid", "level", "locked")def__init__(self, uid, level, locked):
self.uid = uid
self.level = level
self.locked = locked
__slots__ 的意思很简单:别给每个对象都挂一个动态属性字典了,我就这几个字段。
这东西不适合到处乱用。配置对象、请求上下文这种字段可能变的,别硬上。它适合那种数量很多、字段固定、生命周期又比较长的小对象。
第四个技巧:重复字符串,能复用就别重复造
这个坑在状态字段、城市名、渠道码里很常见。
比如订单里状态只有几个值:
PAID
CANCELLED
REFUND
但你从文件里读出来,每一行都会产生一个新的字符串对象。数据量一大,内存里堆着一堆内容一样的字符串。
可以用 sys.intern 做驻留:
import sysdefiter_orders(path):
with open(path, "r", encoding="utf-8") as f:
for line in f:
cols = line.rstrip("\n").split(",")
if len(cols) < 5:
continue
yield {
"order_id": cols[0],
"user_id": cols[1],
"amount": int(cols[3] or0),
"status": sys.intern(cols[4]),
}
这个我一般只给“重复率很高”的短字符串用。比如状态、枚举、类型码。用户昵称、地址、备注这种别瞎 intern,重复率不高,收益不明显。
第五个技巧:用数组,不要什么都 list[int]
Python 的 int 是对象,不是 C 里的 4 字节整数。你搞一个几百万个数字的 list,内存大头不在 list 本身,而在这些整数对象上。
如果只是存一批 ID、分数、计数,试试 array:
from array import arraydefload_user_ids(path):
ids = array("I")
with open(path, "r", encoding="utf-8") as f:
for line in f:
uid = line.strip()
if uid:
ids.append(int(uid))
return ids
"I" 表示无符号整数。这个比普通 list[int] 紧凑很多。
当然,别把它吹成万能。你要频繁插入、删除、混合类型,array 不舒服。它适合很明确的场景:大量数字,主要顺序读,偶尔追加。
我通常会用这段代码粗看一下差距:
import sys
from array import arraynums = list(range(1_000_000))
packed = array("I", range(1_000_000))
print(sys.getsizeof(nums))
print(sys.getsizeof(packed))
sys.getsizeof(nums) 只算 list 容器本身,不包含里面每个 int 对象,所以真实差距会更大。这个点面试时最好顺手说出来,不然容易被追着问。
把这 5 个技巧放一起看,其实不是“Python 有什么神奇开关”。
真正的问题是:你有没有在代码里制造不必要的常驻对象。
大文件别全读,用生成器。
中间结果别乱建,用生成器表达式接内置函数。
大量固定字段对象,用 __slots__。
重复短字符串,考虑 sys.intern。
大量数字,别默认 list[int],看看 array。
Python 确实有对象开销,这个不用洗。但很多内存问题,根本没到解释器背锅那一步,代码自己先把内存吃完了。面试里把这几种场景说清楚,比喊一句“Python 费内存”强得多。