一. 前言:~/.myapp 在你的電腦可以,不代表到處都對
#
寫一個 Python 小工具時,最容易出現的路徑大概長這樣:
from pathlib import Path
config_file = Path.home() / ".paipaijun" / "config.json"
在 Linux 上看起來很合理。
但搬到 macOS,應用程式資料通常應該放進 ~/Library/Application Support;
到了 Windows,又有 AppData\Local、AppData\Roaming 等不同慣例。
更麻煩的是,設定、快取、持久資料、執行狀態與 log 根本不是同一種東西:
- 設定檔需要備份,快取通常不需要。
- 使用者資料不能隨便刪,log 卻應該能輪替。
- Windows roaming profile 適合小型偏好設定,不適合塞大型模型快取。
- Linux 使用者可能用
XDG_CONFIG_HOME改掉預設位置。
這些差異不該散落成一堆 if sys.platform == ...。
platformdirs 就是專門處理這件事的小套件。
它幫我們回答一個簡單但容易答錯的問題:
這種應用程式檔案,在目前作業系統應該放哪裡?
這篇會用一個「拍拍君 CLI」示範如何管理 config、cache、data、state 與 log, 再補上目錄建立、版本隔離、安全清理和測試策略。
如果你還不熟悉 Path 的基本操作,可以先看 pathlib 教學;
本文的重點不是怎麼操作路徑,而是怎麼選對路徑。
二. 安裝:小套件,一個依賴就夠 #
用 uv 建立專案:
uv init paipaijun-cli
cd paipaijun-cli
uv add platformdirs
習慣 pip 也可以:
python -m pip install platformdirs
確認套件可以載入:
python -c "import platformdirs; print(platformdirs.__version__)"
本文使用的是現代版 platformdirs API。
套件同時提供兩種回傳形式:
user_config_dir():回傳str。user_config_path():回傳pathlib.Path。
拍拍君建議新程式優先使用 _path 版本,因為後面讀寫檔案會更自然。
三. 第一個例子:先找出 config 與 cache #
先印出目前平台的常用目錄:
from platformdirs import user_cache_path, user_config_path
APP_NAME = "PaipaiJun"
APP_AUTHOR = "DailyPypy"
config_dir = user_config_path(APP_NAME, APP_AUTHOR)
cache_dir = user_cache_path(APP_NAME, APP_AUTHOR)
print(f"config: {config_dir}")
print(f"cache: {cache_dir}")
同一段程式在不同平台可能得到這類結果:
| 平台 | config 範例 | cache 範例 |
|---|---|---|
| Linux | ~/.config/PaipaiJun |
~/.cache/PaipaiJun |
| macOS | ~/Library/Application Support/PaipaiJun |
~/Library/Caches/PaipaiJun |
| Windows | %LOCALAPPDATA%\DailyPypy\PaipaiJun |
%LOCALAPPDATA%\DailyPypy\PaipaiJun\Cache |
實際路徑仍會受作業系統設定與環境變數影響。
所以不要把上表複製成字串常數;讓 platformdirs 在執行時決定。
appname 與 appauthor 是什麼?
#
appname 是應用程式名稱,appauthor 是發行者或組織名稱。
appauthor 對 Windows 的路徑結構特別重要:
from platformdirs import user_data_path
print(user_data_path("PaipaiJun", "DailyPypy"))
print(user_data_path("PaipaiJun", appauthor=False))
如果沒有發行者層級的需求,可以明確傳入 appauthor=False。
不要隨意省略後再改,因為 Windows 上的預設行為可能形成兩層同名目錄; 應用程式發布後再改路徑,也等於要做一次資料 migration。
四. 分清楚五種目錄:不要全部塞進 config #
platformdirs 最有價值的地方,不只跨平台,還包括語意分類。
from platformdirs import PlatformDirs
dirs = PlatformDirs("PaipaiJun", "DailyPypy")
print("config:", dirs.user_config_path)
print("cache: ", dirs.user_cache_path)
print("data: ", dirs.user_data_path)
print("state: ", dirs.user_state_path)
print("log: ", dirs.user_log_path)
這五個目錄適合放的內容不同。
1. Config:使用者想調整的設定 #
例如:
- API endpoint。
- 顯示語言。
- 預設輸出格式。
- 功能開關。
config_file = dirs.user_config_path / "config.json"
2. Cache:刪掉也能重建的資料 #
例如:
- HTTP 回應快取。
- 縮圖。
- 已下載套件索引。
- 可重新計算的 embeddings。
response_cache = dirs.user_cache_path / "responses.sqlite3"
判斷標準很直白:
整個 cache 目錄消失後,程式能不能正常重建?
如果答案是否定的,那它就不該只放 cache。
3. Data:真正需要保留的應用程式資料 #
例如:
- 使用者建立的筆記。
- 本機資料庫。
- 離線工作佇列。
- 已匯入且無法重建的內容。
database_file = dirs.user_data_path / "paipaijun.sqlite3"
移除應用程式時,使用者通常會希望有機會保留或匯出這些資料。
4. State:執行狀態,而不是偏好設定 #
例如:
- 上次同步游標。
- 最近一次成功執行時間。
- 可恢復的 session 狀態。
- process checkpoint。
sync_state = dirs.user_state_path / "sync-state.json"
State 和 data 的邊界不一定絕對,但至少不要把它混進設定檔。
5. Log:診斷紀錄 #
log_file = dirs.user_log_path / "paipaijun.log"
Log 要考慮輪替、保存期限與隱私,不能無限長大。
五. 用 ensure_exists 安全建立目錄
#
只取得路徑,不代表目錄已經存在。
你可以自己建立:
from platformdirs import user_config_path
config_dir = user_config_path("PaipaiJun", "DailyPypy")
config_dir.mkdir(parents=True, exist_ok=True)
也可以交給 platformdirs:
from platformdirs import PlatformDirs
dirs = PlatformDirs(
appname="PaipaiJun",
appauthor="DailyPypy",
ensure_exists=True,
)
config_file = dirs.user_config_path / "config.json"
當你存取對應屬性時,目錄會一併建立。
這很方便,但別在單純匯入 module 時就建立五個空資料夾。
拍拍君偏好的做法是:
- 共用的
PlatformDirs物件先不開ensure_exists。 - 真正寫入某類資料前,只建立需要的那個目錄。
- 測試時注入暫存路徑,不碰使用者真實目錄。
def save_text(path, content: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
這樣副作用發生的位置更清楚。
六. 實戰:整理一個可維護的 AppPaths #
專案稍微長大後,不要在每個檔案都重複呼叫 user_cache_path()。
集中定義路徑會更好測試,也比較不會把資料放錯分類。
from dataclasses import dataclass
from pathlib import Path
from platformdirs import PlatformDirs
@dataclass(frozen=True)
class AppPaths:
config_file: Path
database_file: Path
response_cache: Path
sync_state: Path
log_file: Path
@classmethod
def from_platform(cls) -> "AppPaths":
dirs = PlatformDirs("PaipaiJun", "DailyPypy")
return cls(
config_file=dirs.user_config_path / "config.json",
database_file=dirs.user_data_path / "paipaijun.sqlite3",
response_cache=dirs.user_cache_path / "responses.sqlite3",
sync_state=dirs.user_state_path / "sync-state.json",
log_file=dirs.user_log_path / "paipaijun.log",
)
使用端只依賴 AppPaths:
import json
def save_config(paths: AppPaths, config: dict) -> None:
paths.config_file.parent.mkdir(parents=True, exist_ok=True)
paths.config_file.write_text(
json.dumps(config, ensure_ascii=False, indent=2),
encoding="utf-8",
)
paths = AppPaths.from_platform()
save_config(paths, {"nickname": "拍拍君", "theme": "green"})
這個設計有三個好處:
- OS 判斷被
platformdirs收好。 - 檔案用途在一個地方看得懂。
- 測試可以直接換掉整組路徑。
設定「值」本身的驗證,則可以交給 pydantic-settings;兩者是互補,不是替代關係。
七. 版本、Windows 與清理策略 #
version 可以讓不相容的主要版本使用不同目錄:
dirs = PlatformDirs(
"PaipaiJun",
"DailyPypy",
version="2",
)
它適合隔離不相容的資料格式或 cache,但不要每個 patch release 都換路徑。 版本目錄也不是 migration 的替代品;舊資料需要保留時,仍要寫搬遷流程。
Windows 另有 Local 與 Roaming application data:
local_dirs = PlatformDirs("PaipaiJun", "DailyPypy", roaming=False)
roaming_dirs = PlatformDirs("PaipaiJun", "DailyPypy", roaming=True)
roaming=True 適合可能跟著網域帳號同步的小型偏好設定,
不適合大型 cache、模型權重、機器專屬索引或含本機絕對路徑的狀態。
appauthor 也最好在第一版就決定;改名或移除 author 層級都可能需要 migration。
清理方面,只刪能重建的 cache:
from pathlib import Path
import shutil
def clear_cache(cache_dir: Path) -> int:
if not cache_dir.exists():
return 0
removed = sum(1 for path in cache_dir.rglob("*") if path.is_file())
shutil.rmtree(cache_dir)
return removed
實務上還要顯示目標、提供確認或 --dry-run,並確保路徑位於自己 app 的
user_cache_path 下。config、data 與 state 不應被「清快取」順手刪掉。
更完整的搬移與清理技巧,可以參考 shutil 實戰。
八. 測試:不要碰開發者真正的 Library 或 AppData #
最糟的測試,是跑完後把你真正的設定檔改掉。
因為前面把路徑包成 AppPaths,現在可以注入 tmp_path:
def test_save_config(tmp_path):
paths = AppPaths(
config_file=tmp_path / "config" / "config.json",
database_file=tmp_path / "data" / "paipaijun.sqlite3",
response_cache=tmp_path / "cache" / "responses.sqlite3",
sync_state=tmp_path / "state" / "sync-state.json",
log_file=tmp_path / "log" / "paipaijun.log",
)
save_config(paths, {"theme": "green"})
assert paths.config_file.exists()
assert '"theme": "green"' in paths.config_file.read_text()
這裡使用 pytest 的 tmp_path fixture。
它與 platformdirs 的責任很不一樣:
- 正式執行:
platformdirs找 OS 慣用位置。 - 測試執行:
tmp_path提供隔離環境。
若你想深入 fixture 設計,可接著看 pytest fixtures 實戰。
需要測平台差異時怎麼辦? #
不要在單元測試裡硬假裝整個作業系統。
比較實際的分層是:
- 你的商業邏輯用注入路徑做單元測試。
- 少量整合測試確認
PlatformDirs能建立預期目錄。 - CI matrix 分別在 Linux、macOS、Windows 執行。
跨平台的真實行為,最後仍應在真實平台驗證。
九. 常見陷阱:路徑正確,設計仍可能錯 #
- 把 cache 當永久資料庫:刪掉不能重建,就不該只放 cache。
- 假設目錄存在:寫入前要
mkdir,或使用ensure_exists=True。 - import 時建立所有目錄:這會讓測試與 CLI completion 產生副作用。
- 把 token 明文塞進 config:
platformdirs解決位置,不負責秘密管理。 - 寫死完整路徑:XDG 變數和作業系統設定都可能改變結果。
- 改 app 名稱卻不搬資料:新名稱就是新目錄,產品改名需要 migration。
十. 一張小抄:該選哪個目錄? #
| 內容 | 建議位置 | 能否安全清除? |
|---|---|---|
| 使用者偏好、endpoint | config | 通常不能 |
| 本機資料庫、使用者內容 | data | 不能 |
| HTTP cache、縮圖、可重建索引 | cache | 可以 |
| 同步游標、可恢復狀態 | state | 視設計而定 |
| 診斷紀錄 | log | 可依保留政策清除 |
| 測試資料 | pytest tmp_path / tempfile |
測試後可以 |
記住三個 API 使用原則:
from platformdirs import PlatformDirs
dirs = PlatformDirs("PaipaiJun", "DailyPypy")
# Path 物件比較好用
config_file = dirs.user_config_path / "config.json"
# 真正寫入前才建立
config_file.parent.mkdir(parents=True, exist_ok=True)
# 測試時注入 tmp_path,不碰真實目錄
結語:讓檔案待在它該待的地方 #
跨平台應用程式不該把所有東西都塞進當前工作目錄、~/.myapp 或 /tmp。
platformdirs 雖然小,卻能幫你建立很重要的邊界:
- config 是使用者偏好。
- data 是需要保留的內容。
- cache 是能重建的加速資料。
- state 是執行狀態。
- log 是可控管的診斷紀錄。
再搭配 pathlib 操作路徑、pydantic-settings 驗證設定值、
pytest tmp_path 隔離測試,就能讓 CLI、桌面工具與本機服務在不同平台都更像個好公民。
拍拍君的建議很簡單:下一次想寫 Path.home() / ".myapp" 時,先停一下。
那個檔案究竟是設定、資料、快取,還是 log?
分類答對,路徑通常就跟著答對了。🐍