快轉到主要內容
  1. 教學文章/

Python platformdirs 實戰:跨平台管理設定、快取與應用程式資料夾

·7 分鐘· loading · loading · ·
Python Platformdirs Filesystem Configuration Cache Cross-Platform
每日拍拍
作者
每日拍拍
科學家 X 科技宅宅
目錄
Python 學習 - 本文屬於一個選集。
§ 96: 本文

featured

一. 前言:~/.myapp 在你的電腦可以,不代表到處都對
#

寫一個 Python 小工具時,最容易出現的路徑大概長這樣:

from pathlib import Path
config_file = Path.home() / ".paipaijun" / "config.json"

在 Linux 上看起來很合理。

但搬到 macOS,應用程式資料通常應該放進 ~/Library/Application Support; 到了 Windows,又有 AppData\LocalAppData\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 在執行時決定。

appnameappauthor 是什麼?
#

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"})

這個設計有三個好處:

  1. OS 判斷被 platformdirs 收好。
  2. 檔案用途在一個地方看得懂。
  3. 測試可以直接換掉整組路徑。

設定「值」本身的驗證,則可以交給 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 實戰

需要測平台差異時怎麼辦?
#

不要在單元測試裡硬假裝整個作業系統。

比較實際的分層是:

  1. 你的商業邏輯用注入路徑做單元測試。
  2. 少量整合測試確認 PlatformDirs 能建立預期目錄。
  3. CI matrix 分別在 Linux、macOS、Windows 執行。

跨平台的真實行為,最後仍應在真實平台驗證。

九. 常見陷阱:路徑正確,設計仍可能錯
#

  • 把 cache 當永久資料庫:刪掉不能重建,就不該只放 cache。
  • 假設目錄存在:寫入前要 mkdir,或使用 ensure_exists=True
  • import 時建立所有目錄:這會讓測試與 CLI completion 產生副作用。
  • 把 token 明文塞進 configplatformdirs 解決位置,不負責秘密管理。
  • 寫死完整路徑: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?

分類答對,路徑通常就跟著答對了。🐍

延伸閱讀
#

Python 學習 - 本文屬於一個選集。
§ 96: 本文

相關文章

Python fsspec 實戰:統一讀寫本機、S3、HTTP 與資料管線路徑
·7 分鐘· loading · loading
Python Fsspec Filesystem S3 Data-Engineering ETL
Python shutil 實戰:檔案複製、搬移、壓縮與安全清理
·7 分鐘· loading · loading
Python Shutil Filesystem Automation Standard-Library Developer-Tools
Python dotenv 實戰:安全管理 .env、環境變數與部署設定
·7 分鐘· loading · loading
Python Dotenv Environment Variables Configuration Security
Python pydantic-settings 實戰:型別安全管理 .env 與設定檔
·6 分鐘· loading · loading
Python Pydantic Pydantic-Settings Dotenv Configuration Developer-Tools
Python tempfile 實戰:安全建立暫存檔案、目錄與測試資料
·9 分鐘· loading · loading
Python Tempfile Filesystem Testing Standard-Library Developer-Tools
Python watchdog 實戰:檔案變更監控與自動化完全攻略
·8 分鐘· loading · loading
Python Watchdog Automation Filesystem Developer-Tools