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

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

🧭 はじめに(What)

このページでは、 functools.lru_cache(maxsize=1) を使って logger を「遅延初期化」し、かつ「一度だけ設定する」設計パターン を解説する。

これは単なるシングルトン実装ではない。 ログ設定という副作用を、適切なタイミング・回数・責務の境界に閉じ込めるための設計技法である。


🎯 ねらい

  • logger 実装で起こりがちな 初期化タイミング事故を防ぐ
  • import と副作用を切り離す
  • ログ設定の 責務と実行順序を明示化する
  • テスト可能性を高める

🧱 背景:logger が壊れやすい理由

Python の logging 周りで頻発する問題は、ほぼ次の3つに集約できる。

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

これらはすべて、「副作用(logging 設定)がどこで・いつ走るか分からない」ことが原因。

logger オブジェクト自体は共有されるが、設定(handlers / level / format)は副作用として何度でも適用できてしまう。


❌ よくあるアンチパターン

import 時に設定してしまう

# logging_.py
configure_logging()   # importした瞬間に副作用
logger = logging.getLogger("app")

問題点

  • import しただけで挙動が変わる
  • import 順序に依存する
  • テストや再利用で事故りやすい

ライブラリコードが import 時に logging 設定を行うのは、最も避けるべき設計の一つ。


✅ 解決策:lru_cache(maxsize=1) による遅延初期化

基本形

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 を 関数として提供
  • 初回呼び出し時のみ configure_logging() を実行
  • 2回目以降はキャッシュされた logger を即返却

🧠 このパターンの本質

この設計の本質は Singleton ではない。

「副作用を伴う初期化処理を、一度だけ、必要になった瞬間に実行する」ことを、言語機能だけで保証している。


🔍 実行順序で見る挙動

初回呼び出し

  1. get_logger() が呼ばれる
  2. キャッシュ未作成 → 関数本体を実行
  3. configure_logging() が実行される
  4. logger を返す
  5. ログ出力

2回目以降

  1. get_logger() が呼ばれる
  2. キャッシュヒット
  3. 設定処理は一切走らない
  4. logger を即返却

この挙動は import 順序・呼び出し順序に依存しない


🧩 「main 冒頭で設定」との違い

main 冒頭で設定する方式

configure_logging()
import feature_a
import feature_b
  • 実行順序を守れれば最も単純
  • ただし 順序を破ると即破綻
  • 呼び出し経路が複数あると統制が難しい

lru_cache 方式

  • 呼び出し側がどこで logger を使っても安全
  • import しただけでは副作用が起きない
  • 初期化の責務が 関数境界に閉じる

「規律で守る設計」から「構造で守る設計」への転換


🧪 テストでの嬉しさ

def test_logging():
    get_logger.cache_clear()
    logger = get_logger()
  • 初期化状態を明示的にリセット可能
  • テスト間の干渉を防げる
  • モジュール変数より圧倒的に扱いやすい

⚠️ 使いどころの見極め

向いているケース

  • logging 設定が 副作用を伴う
  • import 時に設定したくない
  • 呼び出し元の main を制御できない
  • テストで初期化をやり直したい

向いていないケース

  • 単に getLogger(__name__) するだけ
  • アプリの main を完全に掌握している
  • logging 設定を一切行わないライブラリ

このパターンは「常に使うもの」ではなく、副作用管理が問題になったときの選択肢


✅ チェックリスト

  • logging 設定は import 時に走っていないか?
  • 設定処理は1回だけでよいか?
  • 初期化のタイミングを制御したいか?
  • テストで状態を戻す必要があるか?

🔚 まとめ

  • lru_cache(maxsize=1)Singleton のための道具ではない
  • 本質は 副作用付き初期化の制御
  • logger 実装では特に相性が良い
  • main 冒頭で設定できるなら不要
  • できない/したくない状況で威力を発揮する

「いつ・どこで・何が起きるか」をコードから読めること。 それが、この logger 実装の最大の価値である。