一. 前言:暫存檔不是隨便丟到 /tmp 就好 #
寫 Python 的時候,我們常常需要一個「先放一下」的地方。 例如把 API 回來的大檔案先落地。 例如把使用者上傳的 CSV 轉成乾淨版本。 例如測試時需要建立一組假的資料夾。 例如 CLI 工具產生中間報表,最後才輸出成正式檔案。 很多人第一反應是這樣:
path = "/tmp/report.csv"
拍拍君先皺眉一下。 這段看起來很直覺,但實務上會遇到不少麻煩。
-
檔名可能跟別的程序撞在一起
-
權限可能不是你以為的那樣
-
程式中途失敗後檔案可能永遠留著
-
Windows、macOS、Linux 的暫存路徑規則不完全一樣
-
測試跑平行時可能互相污染 所以今天要介紹 Python 標準庫裡很務實的模組:
tempfile。 它不是華麗框架。 它也不會讓程式看起來很潮。 但它會幫你安全地建立暫存檔案、暫存目錄,並且在適當時機清理掉。 這種工具平常不起眼,直到某天你發現伺服器的/tmp被幾十 GB 的殘檔塞爆。 那時候你就會開始尊敬它了。 今天這篇會聚焦幾個實戰場景: -
用
TemporaryDirectory建立會自動清理的工作目錄 -
用
NamedTemporaryFile產生有檔名的暫存檔 -
用
mkstemp處理需要手動控制生命週期的情境 -
用
SpooledTemporaryFile處理小檔留在記憶體、大檔才落地 -
在測試、CLI、資料處理裡安全使用暫存空間
-
避免常見的清理、權限、跨平台陷阱 如果你之前看過 pathlib 文章,可以把今天這篇想成補上「路徑背後的暫時空間管理」。
pathlib解決的是路徑操作。tempfile解決的是暫存資源的建立與清理。 兩個搭起來,檔案處理才比較不會變成一團亂。
二. 安裝:標準庫內建,直接 import #
好消息,tempfile 是 Python 標準庫。 不需要 pip。 不需要 uv add。 不需要跟系統套件搏鬥。 直接 import:
import tempfile
通常你也會搭配 pathlib.Path:
from pathlib import Path
import tempfile
確認 Python 版本:
python --version
今天的範例使用 Python 3.11 以上的寫法。 大多數概念在 Python 3.8 之後都可以用。 如果你還在維護更舊的版本,要特別確認 TemporaryDirectory 和部分參數的行為。 先看一個最短範例:
from pathlib import Path
import tempfile
with tempfile.TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
file_path = tmp_path / "hello.txt"
file_path.write_text("hello, 拍拍君\n", encoding="utf-8")
print(file_path.read_text(encoding="utf-8"))
print("暫存目錄已經被清理")
這段程式做了幾件事。 第一,建立一個系統認可的暫存目錄。 第二,在目錄裡寫入檔案。 第三,離開 with 區塊時自動刪掉整個目錄。 這就是 tempfile 最重要的精神: 暫存資源應該有清楚的生命週期。 不要靠「我晚點會記得刪」這種不可靠的信仰。
三. TemporaryDirectory:最推薦的預設選擇 #
如果你需要一個暫時工作區,拍拍君通常會先選 TemporaryDirectory。 它比單一暫存檔更有彈性。 因為很多實務流程不是只產生一個檔案,而是一整組中間檔。 例如:
- 下載一個 zip
- 解壓縮
- 讀取裡面的 CSV
- 產生清理後的 JSON
- 最後只把正式結果輸出到指定位置 中間那些東西不應該永久留在專案目錄。 範例:
from pathlib import Path
import tempfile
def build_report(rows: list[dict[str, str]]) -> str:
with tempfile.TemporaryDirectory(prefix="pypy-report-") as tmp:
workdir = Path(tmp)
raw_path = workdir / "raw.txt"
out_path = workdir / "report.txt"
raw_path.write_text(
"\n".join(row["name"] for row in rows),
encoding="utf-8",
)
names = raw_path.read_text(encoding="utf-8").splitlines()
out_path.write_text(
f"count={len(names)}\nfirst={names[0]}\n",
encoding="utf-8",
)
return out_path.read_text(encoding="utf-8")
print(build_report([
{"name": "拍拍君"},
{"name": "拍拍醬"},
]))
這裡的 prefix 很有用。 它讓你在除錯時可以看出這個暫存目錄大概屬於哪個功能。 但它仍然會加上隨機字串,避免撞名。 你可能會看到類似:
/var/folders/.../pypy-report-a8m9x2kq
不要自己拼:
# 不推薦
workdir = Path("/tmp/pypy-report")
workdir.mkdir(exist_ok=True)
這種寫法在本機小腳本可能沒事。 但在 CI、伺服器、多程序、平行測試裡,撞名只是早晚問題。 TemporaryDirectory 的另一個好處是清理範圍很明確。 只要把中間檔都放在 workdir 裡,離開 with 時整包帶走。 這比到處記得呼叫 unlink() 穩很多。
四. NamedTemporaryFile:需要「一個檔名」時使用 #
有些 API 不是吃 file object,而是吃檔案路徑。 這時候你需要一個真的存在於檔案系統上的暫存檔。 NamedTemporaryFile 就是這種情境的工具。 範例:
import tempfile
with tempfile.NamedTemporaryFile(
mode="w+",
encoding="utf-8",
prefix="pypy-",
suffix=".txt",
) as f:
f.write("hello\n")
f.write("temporary file\n")
f.seek(0)
print("path:", f.name)
print(f.read())
print("離開 with 後,檔案通常已被刪除")
幾個參數要看懂。 mode=“w+" 代表可寫也可讀。 encoding=“utf-8” 是文字檔應該養成的習慣。 prefix 和 suffix 方便辨識,也能讓外部工具看到副檔名。 例如有些程式會根據 .csv、.json 判斷格式。 你可以這樣做:
import csv
import tempfile
rows = [
{"name": "拍拍君", "score": 95},
{"name": "chatPTT", "score": 88},
]
with tempfile.NamedTemporaryFile(
mode="w+",
encoding="utf-8",
newline="",
suffix=".csv",
) as f:
writer = csv.DictWriter(f, fieldnames=["name", "score"])
writer.writeheader()
writer.writerows(rows)
f.seek(0)
print(f.read())
這裡的 newline=”" 是 csv 模組常見建議。 不加也不一定立刻爆炸。 但跨平台時換行可能變得奇怪。
Windows 上的小提醒 #
NamedTemporaryFile 在 Windows 上有一個常見坑。 當檔案還開著時,另一個程序可能不能重新打開同一路徑。 也就是說,這種寫法在某些外部工具情境會失敗:
with tempfile.NamedTemporaryFile(suffix=".txt") as f:
call_external_tool(f.name)
如果外部工具需要自己打開檔案,你可以改用 TemporaryDirectory,在裡面建立一般檔案。 這通常比較直覺,也比較跨平台。
from pathlib import Path
import tempfile
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "input.txt"
path.write_text("hello\n", encoding="utf-8")
# 外部工具可以用 path 重新開啟檔案
print(path)
拍拍君自己的偏好是: 需要一整段流程,就用 TemporaryDirectory。 真的只需要單一暫存檔,才用 NamedTemporaryFile。
五. mkstemp:需要手動控制時才拿出來 #
mkstemp 是比較底層的工具。 它會安全建立一個暫存檔,並回傳兩個東西:
- 檔案描述符 file descriptor
- 檔案路徑 範例:
import os
import tempfile
fd, path = tempfile.mkstemp(prefix="pypy-", suffix=".txt")
try:
with os.fdopen(fd, "w", encoding="utf-8") as f:
f.write("manual cleanup\n")
with open(path, "r", encoding="utf-8") as f:
print(f.read())
finally:
os.remove(path)
這段比較囉嗦。 但它給你更完整的控制權。 例如你需要把檔案關掉後,再交給另一個函式使用。 或者你需要在比較底層的系統整合裡處理 file descriptor。 不過請注意: 使用 mkstemp 時,清理責任就回到你身上。 你一定要搭配 try/finally。 不然例外一發生,暫存檔就可能留在磁碟上。 拍拍君通常不會把 mkstemp 當預設選擇。 它適合少數需要手動生命週期的場景。 大多數應用程式其實用 TemporaryDirectory 就夠了。
六. SpooledTemporaryFile:小檔留記憶體,大檔才落地 #
有些資料可能很小,也可能突然變大。 例如使用者上傳檔案。 例如把 API 回應轉成中間格式。 例如產生報表前先累積內容。 如果永遠用記憶體,遇到大檔會危險。 如果永遠寫磁碟,小檔又有點浪費。 SpooledTemporaryFile 的想法是: 先放在記憶體。 超過指定大小後,再自動轉成真正的暫存檔。 範例:
import tempfile
with tempfile.SpooledTemporaryFile(
max_size=1024,
mode="w+t",
encoding="utf-8",
) as f:
for i in range(100):
f.write(f"line {i}\n")
f.seek(0)
print(f.readline().strip())
max_size=1024 代表超過約 1 KB 後會 rollover 到磁碟。 如果你想手動強制落地,可以呼叫:
f.rollover()
這個工具在 web app 裡很常見。 小型上傳檔不需要馬上寫磁碟。 大型上傳檔又不能全塞記憶體。 不過要記得,這不是資料庫,也不是長期儲存。 它仍然是暫存資源。 處理完就該結束生命週期。
七. 實戰一:轉檔流程的安全工作區 #
假設我們有一個簡單需求: 讀取一批文字資料,整理成 JSONL,最後把結果寫到正式輸出路徑。 中間檔只用來檢查和轉換,不需要永久保存。 可以這樣寫:
from pathlib import Path
import json
import tempfile
def export_jsonl(names: list[str], output_path: Path) -> None:
with tempfile.TemporaryDirectory(prefix="pypy-export-") as tmp:
workdir = Path(tmp)
staging_path = workdir / "staging.jsonl"
with staging_path.open("w", encoding="utf-8") as f:
for index, name in enumerate(names, start=1):
record = {"id": index, "name": name}
f.write(json.dumps(record, ensure_ascii=False) + "\n")
content = staging_path.read_text(encoding="utf-8")
if not content.strip():
raise ValueError("empty export")
output_path.write_text(content, encoding="utf-8")
export_jsonl(
["拍拍君", "拍拍醬", "chatPTT"],
Path("names.jsonl"),
)
這裡有個重要設計。 正式輸出檔 names.jsonl 不放在暫存目錄裡。 因為 TemporaryDirectory 會在離開區塊後刪掉。 暫存目錄只負責中間工作。 正式成果要明確寫到外面。 這種分界很重要。 它讓你一眼看出哪些檔案是可丟棄的,哪些檔案是產物。
八. 實戰二:測試需要檔案時,不要污染專案目錄 #
測試常常需要建立檔案。 例如測試設定載入。 例如測試匯入資料。 例如測試 CLI 會不會產生輸出。 如果你直接寫到專案目錄,測試跑完就容易留下殘檔。 用 tempfile 可以把測試資料包起來:
from pathlib import Path
import tempfile
def load_names(path: Path) -> list[str]:
return [
line.strip()
for line in path.read_text(encoding="utf-8").splitlines()
if line.strip()
]
def test_load_names() -> None:
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "names.txt"
path.write_text("拍拍君\n拍拍醬\n", encoding="utf-8")
assert load_names(path) == ["拍拍君", "拍拍醬"]
如果你用 pytest,它其實有內建的 tmp_path fixture。 那會更方便:
from pathlib import Path
def load_names(path: Path) -> list[str]:
return path.read_text(encoding="utf-8").splitlines()
def test_load_names(tmp_path: Path) -> None:
path = tmp_path / "names.txt"
path.write_text("拍拍君\n拍拍醬\n", encoding="utf-8")
assert load_names(path) == ["拍拍君", "拍拍醬"]
那為什麼今天還要學 tempfile? 因為 pytest fixture 是測試框架提供的便利包裝。 tempfile 是標準庫能力。 你在一般程式、CLI、背景工作、web app 裡都會用到。 想更完整理解 pytest 的 fixture,可以回去看 pytest 文章。 今天這篇的主角是暫存資源本身。
九. 實戰三:CLI 工具先寫暫存檔,再原子替換 #
有些 CLI 會產生正式輸出檔。 如果你直接寫目標檔,程式跑到一半失敗時,使用者可能拿到半個檔案。 比較穩的做法是: 先寫到同一個目錄裡的暫存檔。 確認成功後,再用 replace 替換正式檔。 範例:
from pathlib import Path
import tempfile
def write_report_atomic(output_path: Path, content: str) -> None:
output_path.parent.mkdir(parents=True, exist_ok=True)
with tempfile.NamedTemporaryFile(
mode="w",
encoding="utf-8",
dir=output_path.parent,
prefix=f".{output_path.name}.",
suffix=".tmp",
delete=False,
) as f:
tmp_path = Path(f.name)
f.write(content)
try:
tmp_path.replace(output_path)
except Exception:
tmp_path.unlink(missing_ok=True)
raise
write_report_atomic(
Path("reports/daily.txt"),
"Daily Pypy report\nstatus=ok\n",
)
這裡有幾個細節。 dir=output_path.parent 很重要。 暫存檔和正式檔在同一個目錄時,替換通常比較可靠。 delete=False 代表離開 with 後不要自動刪掉。 因為我們要在檔案關閉後執行 replace。 但這也代表如果替換失敗,我們要自己清理。 所以有 try/except 裡的 unlink。 這種寫法很適合報表、設定檔、cache metadata。 它不能解決所有併發問題。 但比直接覆寫正式檔好多了。
十. 常見陷阱:拍拍君幫你先踩一遍 #
1. 不要手刻可預測檔名 #
不要這樣:
path = Path("/tmp") / "upload.csv"
這會有撞名和安全問題。 用 tempfile:
with tempfile.NamedTemporaryFile(suffix=".csv") as f:
print(f.name)
2. 不要把暫存檔當永久儲存 #
暫存目錄可能被系統清理。 container 重啟後可能消失。 serverless 環境甚至不保證長期存在。 如果資料重要,處理完就移到正式儲存位置。
3. 不要忘記關閉檔案 #
用 with。 真的需要手動控制,也要用 try/finally。 這句話很無聊,但很值錢。
4. 不要假設暫存路徑一定是 /tmp #
請用 tempfile.gettempdir() 查:
import tempfile
print(tempfile.gettempdir())
macOS 可能是 /var/folders/…。 Windows 可能是使用者 profile 下的 temp。 Linux 常見是 /tmp。 不要把平台細節寫死。
5. 不要把敏感資料留在暫存檔 #
暫存不代表安全。 如果你處理 token、API key、個資、上傳文件,就要更謹慎。 至少做到:
- 用最短生命週期
- 不把路徑印到公開 log
- 不把暫存檔 commit 進 repo
- 不在例外處理裡把內容 dump 出來
tempfile幫你安全建立檔案。 但它不會替你設計資料治理。
十一. 小抄:什麼情境用哪個 API? #
簡單整理一下。
| 情境 | 建議 API | 原因 |
|---|---|---|
| 一段流程需要多個中間檔 | TemporaryDirectory |
清理範圍清楚,最推薦 |
| 只需要一個有名字的暫存檔 | NamedTemporaryFile |
可取得路徑和副檔名 |
| 外部工具需要重新開啟檔案 | TemporaryDirectory + 一般檔案 |
跨平台比較穩 |
| 需要手動控制 file descriptor | mkstemp |
底層控制較完整 |
| 小檔留記憶體,大檔才落地 | SpooledTemporaryFile |
適合上傳或中間 buffer |
| pytest 測試資料 | tmp_path 或 TemporaryDirectory |
測試內用 tmp_path,程式內用 tempfile |
拍拍君自己的排序通常是: 先想 TemporaryDirectory。 不夠再想 NamedTemporaryFile。 真的需要底層控制,才拿 mkstemp。 這樣寫出來的程式通常最容易維護。 |
十二. 結語:好的暫存檔,存在感應該很低 #
tempfile 的目標不是讓你寫出很炫的程式。 它的目標是讓暫存檔安靜地出現、完成任務、然後消失。 這聽起來很普通。 但工程裡很多穩定性,就是靠這種普通的小習慣撐起來的。 不要手刻 /tmp/something.txt。 不要相信自己晚點會記得刪。 不要讓測試把專案目錄弄髒。 不要把暫存檔誤當永久資料。 下次你需要中間工作區時,先寫:
with tempfile.TemporaryDirectory() as tmp:
workdir = Path(tmp)
然後讓程式自己收尾。 拍拍君覺得,會自己收拾殘局的程式,通常比較值得信任。
延伸閱讀 #
- Python 官方文件:tempfile - Generate temporary files and directories
- Python pathlib 教學:用物件方式操作檔案路徑: ../python-pathlib/
- Python pytest:fixture + parametrize + mock 完整指南: ../python-pytest/
- Streamlit 部署實戰:Secrets、設定檔與雲端上線完整攻略: ../streamlit-deploy-secrets/