メインコンテンツへスキップ

🧠 lru_cache を使った「遅延・一度きり」の logger 初期化

🧭 はじめに(What)

このページでは、 functools.lru_cache(maxsize=1) を使って logging の「セットアップだけ」を一度に制御する設計パターン を解説する。

重要なのは、 logger を Singleton 化することではない副作用を伴う logging セットアップを「必要になった瞬間に、一度だけ」実行することが目的である。


🎯 ねらい

  • logging 設定で起こりがちな 実行順序依存バグを防ぐ
  • import と副作用を分離する
  • logger 名(__name__)を潰さずに保持する
  • テスト可能性を高める

🧱 背景:logger 実装で壊れやすいポイント

Python の logging 周りで問題になるのは、ほぼ次の3点に集約される。

  1. import しただけで設定が走る
  2. 設定が複数回適用される
  3. 初期ログだけ挙動が違う

これらはすべて、

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__)

🔍 このパターンの挙動(実行順序)

初回

  1. setup_logging() が呼ばれる
  2. キャッシュ未作成 → 関数本体を実行
  3. logging 設定が適用される
  4. logger を取得してログ出力

2回目以降

  1. setup_logging() が呼ばれる
  2. キャッシュヒット
  3. 設定処理は一切走らない
  4. そのまま 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_cachelogger を統一するための道具ではない
  • キャッシュするのは セットアップという副作用
  • logger 名は 必ず __name__
  • 「設定は1回」「取得は各所」が Python logging の定石

Singleton を作る話ではなく、副作用を制御する話。 これが、このパターンの本質である。