Pythonで標準出力や標準エラー出力をロガーにリダイレクトする

概要

PythonAPIが提供されているソフトウェアでバッチ処理を行っていますが、その処理過程の標準出力や標準エラー出力をロガー(logging.Logger)にリダイレクトしたいということがありました。標準ライブラリの contextlib を利用することでスマートに実装できたので紹介です。

標準出力や標準エラー出力をIOオブジェクトにリダイレクトする

標準出力や標準エラー出力をリダイレクトするには、contextlibcontextlib.redirect_stdout および contextlib.redirect_stderr が便利です。

以下のように任意のIOオブジェクトを引数に渡してwith文で使うことで、with文内での標準出力や標準エラー出力がIOオブジェクトにリダイレクトされます。

import contextlib

with open("test.txt", mode="w") as f, contextlib.redirect_stdout(f):
    print("this is first print message") # 標準出力には出力されず、"test.txt"に出力される

print("this is second print message") # 標準出力に出力される

以下では、リダイレクト先とする LoggerIO クラスを実装し、write メソッドをロガーへの出力としています。このクラスのインスタンスcontextlib.redirect_stdout に渡すことで標準出力をロガーにリダイレクトすることができます。

import contextlib
import logging


class LoggerIO:
    def __init__(self, logger):
        self.logger = logger

    def write(self, msg):
        if msg.strip() != "":
            self.logger.info(msg)

    def flush(self):
        pass


logger = logging.getLogger(__name__)
logger.setLevel("INFO")
logger.addHandler(logging.FileHandler("test.log"))


with contextlib.redirect_stdout(LoggerIO(logger)):
    print("this is print message")  # "test.log"に出力される

contextlib.redirect_stdout, contextlib.redirect_stderrの実装の確認

contextlibのソースコードを見ると、redirect_stdout および redirect_stderrcontextlib._RedirectStream を継承しています。そして、クラス変数 _stream の値によってリダイレクト元(標準出力もしくは標準エラー出力)を切り替えているようです。リダイレクト先の切り替えは、コンテキストマネージャを利用して sys.stdoutsys.stderr に代入されるIOオブジェクトを入れ替えることで実装されています。

class _RedirectStream(AbstractContextManager):

    _stream = None

    def __init__(self, new_target):
        self._new_target = new_target
        # We use a list of old targets to make this CM re-entrant
        self._old_targets = []

    def __enter__(self):
        self._old_targets.append(getattr(sys, self._stream))
        setattr(sys, self._stream, self._new_target)
        return self._new_target

    def __exit__(self, exctype, excinst, exctb):
        setattr(sys, self._stream, self._old_targets.pop())


class redirect_stdout(_RedirectStream):
    """Context manager for temporarily redirecting stdout to another file.
        # How to send help() to stderr
        with redirect_stdout(sys.stderr):
            help(dir)
        # How to write help() to a file
        with open('help.txt', 'w') as f:
            with redirect_stdout(f):
                help(pow)
    """

    _stream = "stdout"


class redirect_stderr(_RedirectStream):
    """Context manager for temporarily redirecting stderr to another file."""

    _stream = "stderr"

この contextlib._RedirectStream を継承することでより柔軟な実装ができそうです。

ロガーへのリダイレクト用クラスの実装

以下では、contextlib._RedirectStream を継承したクラスに直接 write メソッドを実装しています。標準出力と標準エラー出力を引数で指定できたり、RedirectStreamToLogger に直接ロガーを渡してリダイレクトできるようになるなど、よりスマートな実装になりました。

import contextlib
import logging


class RedirectStreamToLogger(contextlib._RedirectStream):
    def __init__(self, logger, stream="stdout"):
        super().__init__(new_target=self)
        self._stream = stream
        self.logger = logger

    def write(self, msg):
        if msg.strip() != "":
            self.logger.info(msg)

    def flush(self):
        pass


logger = logging.getLogger(__name__)
logger.setLevel("INFO")
logger.addHandler(logging.FileHandler("test.log"))

with RedirectStreamToLogger(logger, stream="stdout"):
    print("this is print message")  # "test.log"に出力される

参考文献