2026-08-07
不太會注意到的記憶體浪費
AI 寫 Python 時,不知道為什麼常常在 __init__.py 重新 import 目錄內的東西,也許是受到前端的開發風格影響吧,導致有些 __init__.py 動不動就是一兩百行。
我原本也覺得這沒什麼問題,反正到最後都是會被載入的,我覺得只要避免在 top-level import tensorflow, numpy 之類的東西,應該都沒什麼問題。
但最近工作上遇到了一個特別的案例,有些 worker process 很明確只會處理特定的 Task,這些 worker process 又會在 task 很多時自動擴展,然後在擴展時就遇到 OOM 了。
在問 AI 前,我最一開始的思考方向主要是放在:
- 是不是哪一段 SQL 撈出的資料量太大
- 是不是哪邊的邏輯可以簡化
- 是不是哪個套件的記憶體用量比較大,不適合放在 top-level
但 AI 的思考方向完全不一樣,它居然直接從 import 下手,把 __init__.py 通通改寫成 PEP 562 的 lazy loading 的寫法。
雖然最後沒有直接採納 AI 的方式,因為那樣的寫法我總感覺有點殺雞用牛刀,沒必要。但 AI 的輸出卻讓我發現了可以從不必要的 import 下手。
明明是只處理 A service 的 worker process,B service 怎麼會被載入呢?之前一直都沒發現這點。
仔細一找才發現是 __init__.py 搞的鬼,我們的 __init__.py 幾乎把所有子目錄的所有 class 都給 import 一遍了。
# app/service/__init__.py
from .XXX import XXXService
from .YYY import YYYService
from .ZZZ import ZZZService
這有什麼問題呢? 就算明確使用 from app.service.XXX import XXXService,YYY 與 ZZZ 檔案也都被載入一遍了。一旦 YYYService、ZZZService 被 import,基本上 YYY 與 ZZZ 相關的 model 也都通通被 import 了。
最終光是把 __init__.py 清空,在 QA 環境上程式啟動時就省了 2XX MiB 左右,這個數字是遠超我想像的。
這個案例我覺得蠻有趣的,給了我不一樣的思考方式,我早已經知道 worker process 只會針對 XXX 進行處理,卻沒想過 YYY 與 ZZZ 是不是也被載入,更是沒想到 __init__.py 會是一切的兇手。