🧠 lru_cache を使った「遅延・一度きり」の logger 初期化
🧭 はじめに(What)
このページでは、
functools.lru_cache(maxsize=1) を使って loggerlogging を「遅延初期化」し、かつ「一度だけ設定する」設計パターンの「セットアップだけ」を一度に制御する設計パターン
を解説する。
これは単なるシングルトン実装ではない。重要なのは、
ログ設定という副作用を、適切なタイミング・回数・責務の境界に閉じ込めるための設計技法logger を Singleton 化することではないである。。
副作用を伴う logging セットアップを「必要になった瞬間に、一度だけ」実行することが目的である。
🎯 ねらい
loggerlogging実装で起こりがちな設定で起こりがちな初期化タイミング事故実行順序依存バグを防ぐimportと副作用を切り離すと副作用を分離するログ設定のlogger責務と実行順序を明示化する名(__name__)を潰さずに保持する- テスト可能性を高める
🧱 背景:logger が壊れやすい理由実装で壊れやすいポイント
Python の logging 周りで頻発する問題は、ほぼ次の3つに集約できる。周りで問題になるのは、ほぼ次の3点に集約される。
- import
時に設定が走ってしまうしただけで設定が走る - 設定が複数回適用される
- 初期ログだけ挙動が違う
これらはすべて、「
副作用(logging設定)がどこで・いつ走るか分からない」ことが原因。
logger オブジェクト自体は共有されるが、の「設定(handlers / level / format)は副作用」という副作用を どこで・いつ実行しているか分からないとして何度でも適用できてしまう。
ことが原因。
logger オブジェクト自体は logging モジュール内部で共有されるが、設定は何度でも適用できる副作用である。
❌ よくあるアンチパターン
import 時に設定してしまう時に logging 設定を行う
# logging_.py
configure_logging() # importした瞬間に副作用
logger = logging.getLogger("app")
問題点
- import
しただけで挙動が変わる順序に挙動が依存する import 順序に依存するどこで設定されたか追跡しづらい- テストや再利用で事故りやすい
ライブラリコードが import 時に logging 設定を行うのは、最も避けるべき設計の一つ。
✅❌ 解決策:lru_cache(maxsize=1)もう一つの誤解:logger による遅延初期化を Singleton 化する
基本形
from functools import lru_cache
import logging
import logging.config
@lru_cache(maxsize=1)
def get_logger() -> logging.Logger::
configure_logging()
return logging.getLogger("app")
何をしているか問題点
loggerすべてのログのをname関数として提供が"app"になる初回呼び出し時のみconfigure_logging()を実行どのモジュールがログを出したか分からない2回目以降はキャッシュされたlogginglogger を即返却の階層構造を自ら破壊している
Singleton 化すべき対象を誤ると、情報が失われる。
✅ 解決策:セットアップだけを lru_cache で一度にする
基本設計
- logging 設定:1回だけ実行したい
- logger 取得:各モジュールごとに行いたい(
__name__)
この2つを明確に分離する。
🧠 このパターンの本質正しい実装例
この設計の本質は
logging Singletonセットアップ(副作用を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__)
🔍 実行順序で見る挙動このパターンの挙動(実行順序)
初回呼び出し初回
が呼ばれるget_logger(setup_logging()- キャッシュ未作成 → 関数本体を実行
loggingconfigure_logging()が実行される設定が適用される- logger
を返す ログ出力を取得してログ出力
2回目以降
が呼ばれるget_logger(setup_logging()- キャッシュヒット
- 設定処理は一切走らない
- そのまま logger
を即返却を使用
この挙動は import 順序・呼び出し順序に依存しない順序や呼び出し元に依存しない。のが最大の利点。
🧩 「main 冒頭で設定」との違い
main 冒頭で設定する方式
configure_logging()
import feature_a
import feature_b
実行順序を守れれば最も単純守れればシンプル- ただし 順序を破ると即破綻
呼び出し経路が複数あると統制が難しい起動経路が複数あると統制が難しい
lru_cache 方式
呼び出し側がどこで logger を使っても安全呼び出し側がどこでも安全- import
しただけでは副作用が起きない時に副作用が起きない 初期化の責務がセットアップの責務が 関数境界に閉じる
これは「規律で守る設計」ではなく、「規律で守る設計」から「構造で守る設計」への転換構造で守る設計。
🧪 テストでの嬉しさテストでの利点
def test_logging():
get_logger.setup_logging.cache_clear()
logger = get_logger(setup_logging()
- 初期化状態を明示的にリセット可能
- テスト間の干渉を防げる
モジュール変数より圧倒的に扱いやすい
テスト容易性はこのパターンの重要な副産物。
⚠️ 使いどころの見極め使いどころの判断基準
向いているケース
- logging
設定が副作用を伴う設定が副作用を伴う - import 時に設定したくない
- 呼び出し元の main を制御できない
- テストで初期化をやり直したい
向いていないケース
- 単に
getLogger(__name__)するだけ アプリのmainを完全に掌握しているlogging 設定を一切行わないライブラリを完全に掌握している単純なアプリ
このパターンは「常に使うもの」ではなく、このパターンは副作用管理が問題になったときの選択肢常に使うものではない。
✅ チェックリスト
loggingセットアップは設定はlru_cache(maxsize=1)import 時に走っていないか?で1回に制御しているか設定処理は1回だけでよいか?logger 名に__name__を使っているか初期化のタイミングを制御したいか?import 時に副作用が起きていないかテストで状態を戻す必要があるか?テストで初期化をリセットできるか
🔚 まとめ
はlru_cache(maxsize=1)lru_cacheSingletonloggerのための道具ではないを統一するための道具ではない本質はキャッシュするのは副作用付き初期化の制御セットアップという副作用- logger
実装では特に相性が良い名は 必ず__name__ main「設定は1回」「取得は各所」が冒頭で設定できるなら不要Python できない/したくない状況で威力を発揮するlogging の定石
「いつ・どこで・何が起きるか」をコードから読めること。Singleton を作る話ではなく、副作用を制御する話。
それが、この logger 実装の最大の価値である。これが、このパターンの本質である。