はじめに
ミリオンライク展、集まった俺たちの列も展示物だったんだ... nikkieです。
今回は以下のコードを理解していきます。
>>> import logging >>> logging.getLogger("foo") <Logger foo (WARNING)>
Pythonのバージョンは 3.12.3 です。
目次
- はじめに
- 目次
- 対話モードに出力されたのは、ロガーのレベルそのものではない?
- 対話モードに出力されているのは__repr__の返り値
- ロガーのgetEffectiveLevel()メソッド
- 素振りコーナー
- 終わりに
- おまけ:getEffectiveLevel()メソッドの実装
対話モードに出力されたのは、ロガーのレベルそのものではない?
logging.getLogger()
は渡した名前のロガーを返します1。
https://docs.python.org/ja/3/library/logging.html#logging.getLogger
ここではfooロガーを返しました。
ロガーにはレベルを設定できます。
ドキュメントによると、
https://docs.python.org/ja/3/library/logging.html#logger-objects
level
属性がありsetLevel()
メソッドで設定できます
レベルの一覧はこちら(「ロギングレベル」)
https://docs.python.org/ja/3/library/logging.html#levels
- NOTSET (
0
) - DEBUG (
10
) - INFO (
20
) - WARNING (
30
) - ERROR (
40
) - CRITICAL (
50
)
さて、logging.getLogger("foo")
と生成したロガーのレベルはどうなっているのでしょうか?
私たちは特にレベルを指定していませんよね。
https://docs.python.org/ja/3/library/logging.html#logging.Logger.setLevel
ロガーが生成された際、レベルは NOTSET (略) に設定されます。
生成時はNOTSET
、変えたい場合はsetLevel()
で変更します。
NOTSET
は一番下のレベルですね。
でも待ってください!
対話モードに出力されたレベルはWARNING
ですよね?
これはどういうことなのでしょう?
対話モードに出力されているのは__repr__
の返り値
https://docs.python.org/ja/3/library/functions.html#repr
オブジェクトの印字可能な表現を含む文字列を返します。
>>> repr(logging.getLogger("foo")) '<Logger foo (WARNING)>'
この表現を返している、logging.Logger
クラスの__repr__
を確認します。
https://github.com/python/cpython/blob/v3.12.5/Lib/logging/__init__.py#L1850-L1852
class Logger(Filterer): def __repr__(self): level = getLevelName(self.getEffectiveLevel()) return '<%s %s (%s)>' % (self.__class__.__name__, self.name, level)
level
はロガー自身のレベル(self.level
)ではなく、getEffectiveLevel()
メソッドの返り値です!
ロガーのgetEffectiveLevel()
メソッド
getEffectiveLevel()
メソッドは、ロガー自身のレベル(0
=NOTSET
)ではなく、WARNING
レベル(=30
)を返しています。
>>> import logging >>> foo_logger = logging.getLogger("foo") >>> foo_logger.level 0 >>> foo_logger.getEffectiveLevel() 30
https://docs.python.org/ja/3/library/logging.html#logging.Logger.getEffectiveLevel
このロガーの実効レベルを示します。
NOTSET 以外の値が setLevel() で設定されていた場合、その値が返されます。
そうでない場合、 NOTSET 以外の値が見つかるまでロガーの階層をルートロガーの方向に追跡します。見つかった場合、その値が返されます。
- fooロガーのレベルは
NOTSET
です- 「そうでない場合」に該当します
- ロガーの階層をルートロガーの方向に、すなわち親のロガーを追跡します
- fooの親のロガーはルートロガーです(ルートロガーはすべてのロガーの親)
- ルートロガーは
WARNING
レベルで生成されます NOTSET
以外の値が見つかったので、WARNING
レベルと返されました
つまり、fooロガーのgetEffectiveLevel()
メソッドは、ロガーの階層構造の中で有効なレベルを返したのです!
素振りコーナー
ここまでの理解が正しいか確認のために手を動かします。
(1) ルートロガーのレベルを変更したら、fooロガーの実効レベルは変わるはず
>>> import logging >>> logging.getLogger().setLevel(logging.DEBUG) >>> foo_logger = logging.getLogger("foo") >>> foo_logger.level 0 >>> foo_logger.getEffectiveLevel() 10 >>> foo_logger <Logger foo (DEBUG)>
fooロガーの実効レベルはルートロガーと同じDEBUG
と出力されています。
仮説通り!
※logging.getLogger()
とname引数を渡さないと、ルートロガー(への参照)が返ります
(2) ロガーの階層構造がもう少し深い例
階層構造を少し深くします。
- ルートロガー
a
ロガーa.b
ロガー
>>> import logging >>> logger = logging.getLogger("a.b") >>> logger <Logger a.b (WARNING)>
a.b
ロガーの実効レベルは
a.b
ロガーのレベルはNOTSET
- 親の
a
ロガーのレベルもNOTSET
- ルートロガーのレベル(
WARNING
)が返される
続くコードで、親のa
ロガーのレベルを変更すると
>>> logging.getLogger("a").setLevel(logging.INFO)
>>> logger
<Logger a.b (INFO)>
a.b
ロガーの実効レベルはINFO
に変わりました!
終わりに
fooロガーを作ったときに、レベルはNOTSET
であるのに、対話モードへの出力はレベルがWARNING
である疑問を解き明かしました。
対話モードに出力されたのは、ロガーのgetEffectiveLevel()
メソッドが返す実効レベルです。
実効レベルは
- そのロガーが
NOTSET
以外にsetLevel
されていたらそのレベルが返る NOTSET
だったら、親について実効レベルを同様に調べる- 一番の親ルートロガーは、何もしていなければ
WARNING
レベル2
- 一番の親ルートロガーは、何もしていなければ
というロジックで決まります。
この記事は対話モードに印字されたレベルの出所を追うものでした。
ロガーのレベル、特にNOTSETのときの実効レベルがログ出力にどのように関わるかは別の記事としたいと思います3。
おまけ:getEffectiveLevel()
メソッドの実装
getEffectiveLevel()
メソッドは、whileループでレベルを親へ親へと確認していく実装でした。
https://github.com/python/cpython/blob/v3.12.5/Lib/logging/__init__.py#L1776-L1788
def getEffectiveLevel(self): logger = self while logger: if logger.level: return logger.level logger = logger.parent return NOTSET
NOTSET
レベルが0
、すなわち真理値としてはFalse
と評価されることをうまく使ったコードです。