reload()を使ってもloggingのloggerオブジェクトにHanlderを多重登録させない


この記事はTakumi Akashiro ひとり Advent Calendar 2020の17日目の記事です。

未だにloggingの綺麗な使い方が分からねェ……って思いながら書いた記事です。
ご了承ください。

始めに

普通のPythonを書いているとまず、reload関数なんて使わないですよね。
だが、DCCツールの開発を行っているTAやデザイナーならreload()を目にしたことはあるとおもいます。

(今回の記事はPython3で書くので、3系で非推奨になったreload()に代わって、importlib.reload()を使います。)

でも安易なreload()を使うとロガーがぶっ壊れます。

#! python3
import logging

logger= logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)

sh = logging.StreamHandler()
sh.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))

logger.addHandler(sh)

def main():
    pass
    logger.debug("test")

if __name__ == '__main__':
    main()

普通に使うと、

問題ないですね。

そしてインタプリタでモジュールをimportしてmain()を実行してみましょう。

これをreload()してからmain()してみましょう。

はい、1行多く書き出されましたね。
これはreload()でモジュールが再評価されるときに、再度log.addHandler(sh)が評価されたからです。

これを避ける為に、logger用のモジュールを分けてみます。

log.py
#! python3
import logging

def __gen_logger():
    # これだとlog階層に集まってしまうので間違いですが、、
    # 時間がないので見逃してください!
    logger_ = logging.getLogger(__name__)
    logger_.setLevel(logging.DEBUG)

    sh = logging.StreamHandler()
    sh.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))

    logger_.addHandler(sh)

    return logger_


logger = __gen_logger()


def get_logger():
    return logger

logger_sample.py
#! python3
import log

def main():
    logger = log.get_logger()
    logger.debug("test")

if __name__ == '__main__':
    main()

これで重複しなくなりましたね!

モジュールを分割しない方法

……でも毎回、log.pyみたいなロガー用サブモジュールを作るのもな……と思っていましたが、
最近になって良い方法を見つけました。

#! python3
import logging

def get_logger():
    logger_ = logging.getLogger(__name__)
    if logger_.hasHandlers() is False:
        logger_.setLevel(logging.DEBUG)

        sh = logging.StreamHandler()
        sh.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))

        logger_.addHandler(sh)

    return logger_

def main():
    logger = get_logger()
    logger.debug("test")

if __name__ == '__main__':
    main()

締め

安易なreload()してるスクリプト絶滅しろ