はじめに
ミリオンライク展、集まった俺たちの列も展示物だったんだ... 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と評価されることをうまく使ったコードです。