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

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

🧭 はじめに(What)

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

これは単なるシングルトン実装ではない。重要なのは、 ログ設定という副作用を、適切なタイミング・回数・責務の境界に閉じ込めるための設計技法logger を Singleton 化することではないである。副作用を伴う logging セットアップを「必要になった瞬間に、一度だけ」実行することが目的である。


🎯 ねらい

  • loggerlogging 実装で起こりがちな設定で起こりがちな 初期化タイミング事故実行順序依存バグを防ぐ
  • import と副作用を切り離すと副作用を分離する
  • ログ設定のlogger 責務と実行順序を明示化する名(__name__)を潰さずに保持する
  • テスト可能性を高める

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

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

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

これらはすべて、

副作用(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回目以降はキャッシュされたlogging logger を即返却の階層構造を自ら破壊している

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

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

    初回呼び出し初回

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

    2回目以降

    1. get_logger(setup_logging() が呼ばれる
    2. キャッシュヒット
    3. 設定処理は一切走らない
    4. そのまま 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 実装の最大の価値である。これが、このパターンの本質である。