🧠 lru_cache を使った「遅延・一度きり」の logger 初期化
🧭 はじめに(What)
このページでは、
functools.lru_cache(maxsize=1) を使って logging の「セットアップだけ」を一度に制御する設計パターン
を解説する。
重要なのは、 logger を Singleton 化することではない。 副作用を伴う logging セットアップを「必要になった瞬間に、一度だけ」実行することが目的である。
🎯 ねらい
- logging 設定で起こりがちな 実行順序依存バグを防ぐ
- import と副作用を分離する
- logger 名(
__name__)を潰さずに保持する - テスト可能性を高める
🧱 背景:logger 実装で壊れやすいポイント
Python の logging 周りで問題になるのは、ほぼ次の3点に集約される。
- import しただけで設定が走る
- 設定が複数回適用される
- 初期ログだけ挙動が違う
これらはすべて、
logging の「設定(handlers / level / format)」という副作用を どこで・いつ実行しているか分からない
ことが原因。
logger オブジェクト自体は logging モジュール内部で共有されるが、設定は何度でも適用できる副作用である。
❌ よくあるアンチパターン
import 時に logging 設定を行う
# logging_.py
configure_logging() # importした瞬間に副作用
logger = logging.getLogger("app")
問題点
- import 順序に挙動が依存する
- どこで設定されたか追跡しづらい
- テストや再利用で事故りやすい
ライブラリコードが import 時に logging 設定を行うのは、最も避けるべき設計の一つ。
❌ もう一つの誤解:logger を Singleton 化する
@lru_cache(maxsize=1)
def get_logger():
configure_logging()
return logging.getLogger("app")
問題点
- すべてのログの
nameが"app"になる - どのモジュールがログを出したか分からない
- logging の階層構造を自ら破壊している
Singleton 化すべき対象を誤ると、情報が失われる。
✅ 解決策:セットアップだけを lru_cache で一度にする
基本設計
- logging 設定:1回だけ実行したい
- logger 取得:各モジュールごとに行いたい(
__name__)
この2つを明確に分離する。
🧠 正しい実装例
logging セットアップ(副作用を1回に縛る)
# logging_setup.py
from functools import lru_cache
import logging.config
@lru_cache(maxsize=1)
def setup_logging() -> None:
logging.config.dictConfig({
'version': 1,
'formatters': {
'default': {
'format': '[%(levelname)s] %(name)s: %(message)s',
},
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'default',
},
},
'root': {
'handlers': ['console'],
'level': 'INFO',
},
})
各モジュール側(logger 名は __name__)
# any_module.py
import logging
from logging_setup import setup_logging
setup_logging() # 何度呼ばれてもOK
logger = logging.getLogger(__name__)
🔍 このパターンの挙動(実行順序)
初回
setup_logging()が呼ばれる- キャッシュ未作成 → 関数本体を実行
- logging 設定が適用される
- logger を取得してログ出力
2回目以降
setup_logging()が呼ばれる- キャッシュヒット
- 設定処理は一切走らない
- そのまま logger を使用
import 順序や呼び出し元に依存しないのが最大の利点。
🧩 「main 冒頭で設定」との違い
main 冒頭で設定する方式
configure_logging()
import feature_a
- 守れればシンプル
- ただし 順序を破ると即破綻
- 起動経路が複数あると統制が難しい
lru_cache 方式
- 呼び出し側がどこでも安全
- import 時に副作用が起きない
- セットアップの責務が 関数境界に閉じる
これは「規律で守る設計」ではなく、構造で守る設計。
🧪 テストでの利点
def test_logging():
setup_logging.cache_clear()
setup_logging()
- 初期化状態を明示的にリセット可能
- テスト間の干渉を防げる
テスト容易性はこのパターンの重要な副産物。
⚠️ 使いどころの判断基準
向いているケース
- logging 設定が副作用を伴う
- import 時に設定したくない
- 呼び出し元の main を制御できない
- テストで初期化をやり直したい
向いていないケース
- 単に
getLogger(__name__)するだけ - main を完全に掌握している単純なアプリ
このパターンは常に使うものではない。
✅ チェックリスト
- セットアップは
lru_cache(maxsize=1)で1回に制御しているか - logger 名に
__name__を使っているか - import 時に副作用が起きていないか
- テストで初期化をリセットできるか
🔚 まとめ
lru_cacheは logger を統一するための道具ではない- キャッシュするのは セットアップという副作用
- logger 名は 必ず
__name__ - 「設定は1回」「取得は各所」が Python logging の定石
Singleton を作る話ではなく、副作用を制御する話。 これが、このパターンの本質である。