Python技术迷

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

defmark(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 sys

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": sys.intern(cols[4]),
            }

这个我一般只给“重复率很高”的短字符串用。比如状态、枚举、类型码。用户昵称、地址、备注这种别瞎 intern,重复率不高,收益不明显。

第五个技巧:用数组,不要什么都 list[int]

Python 的 int 是对象,不是 C 里的 4 字节整数。你搞一个几百万个数字的 list,内存大头不在 list 本身,而在这些整数对象上。

如果只是存一批 ID、分数、计数,试试 array:

from array import array

defload_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 array

nums = 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 费内存”强得多。