私は2年前に、Pythonを使ってGUI上でプログラムを選択して実行するようなタスクプログラムを作成していました。
その時に大変だったのが「マルチスレッド」と「ログ出力」です。
Pythonの場合、ログ出力は大抵 Logging
というライブラリを使うと思うのですが、中規模なプログラムだったので色んな場所で呼べる仕様にする必要があったのと、PySimpleGUIというのを使っていたので、画面上にも出力できるようにも工夫しなければなりませんでした。また、万が一のためを考えてログファイルにも転記して残しておきたかったので色々と苦戦しました。
そういった要件を満たすために、独自でLogging
のラッパークラスを作成したのですが、自分的にはかなり使い回しができる良いクラスだと思うのでご紹介したいと思います。
筆者の環境
筆者の環境はこんな感じです。3.7〜3.10のバージョンは動作確認済みです。
コード紹介
早速ですが私が作成したLogクラスのコードをご紹介します。
# -*- coding: utf-8 -*-
# 標準ライブラリ
import datetime
import inspect
import logging
import os
from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL
class Log:
loggers = {}
def __init__(
self,
name: str,
format: str = "%(asctime)s - %(levelname)s - %(name)s: %(message)s",
level=DEBUG,
):
"""
初期化 フォーマットやlogファイルの設定、
Args:
name (str): logにつける固有な名前
format (str, optional): 出力するフォーマット Defaults to '%(asctime)s - %(levelname)s - %(name)s: %(message)s '.
level : 表示するレベルを設定 [DEBUG, INFO, WARNING, ERROR, CRITICAL] Defaults to DEBUG.
"""
self.name = name
self.format = format
self.level = level
# 重複ログ防止のため、nameでのlogが作成済みならば再利用
if Log.loggers.get(name):
self.log = Log.loggers.get(name)
# ハンドラーをいったん削除して再設定する
self.log.handlers.clear()
else:
# loggersに格納して、同じlogを生成しないように管理
self.log = logging.getLogger(self.name)
self.log.setLevel(self.level)
Log.loggers[name] = self.log
# logの複数発行を防ぐ
self.log.propagate = False
# logファイルで使用する日付 ./log/2020年/11月/2020-11-04.logというふうにする
self.today = datetime.datetime.now()
self.text_today = datetime.datetime.strftime(self.today, "%Y-%m-%d")
# このクラスの1階層上に logフォルダを作成
self.log_folder_name = os.path.join(
os.path.dirname(__file__),
"../log/" + str(self.today.year) + "年/" + str(self.today.month) + "月",
)
self.log_file_name = self.text_today + ".log"
os.makedirs(self.log_folder_name, exist_ok=True)
# StreamHandlerの設定
self.sh = logging.StreamHandler()
self.sh.setLevel(self.level)
# FileHandlerの設定 logファイル場所を指定
self.fh = logging.FileHandler(
self.log_folder_name + "/" + self.log_file_name, encoding="utf-8"
)
self.fh.setLevel(self.level)
# フォーマットをセットし、logにハンドラーを追加する
self.formatter = logging.Formatter(self.format)
self.sh.setFormatter(self.formatter)
self.fh.setFormatter(self.formatter)
self.log.addHandler(self.sh)
self.log.addHandler(self.fh)
def get_log(self):
"""
log本体を返す
"""
return self.log
def debug(self, msg, extra=None) -> None:
"""
log.debugでメッセージを出力する
Args:
msg : logで表示する文章や変数など
extra (dict, optional): 付加情報 Defaults to None.
例)
log.debug("message", {foo: bar})
出力 : message [foo=bar]
"""
try:
self.log.debug(msg, extra=extra)
except UnicodeEncodeError:
self.log.debug(msg.encode("cp932", "ignore"), extra=extra)
def info(self, msg, extra: dict = None) -> None:
try:
self.log.info(msg, extra=extra)
except UnicodeEncodeError:
self.log.info(msg.encode("cp932", "ignore"), extra=extra)
def warning(self, msg, extra=None) -> None:
try:
self.log.warning(msg, extra=extra)
except UnicodeEncodeError:
self.log.warning(msg.encode("cp932", "ignore"), extra=extra)
def error(self, msg, extra: dict = None) -> None:
try:
self.log.error(msg, extra=extra)
except UnicodeEncodeError:
self.log.error(msg.encode("cp932", "ignore"), extra=extra)
def exception(self, extra=None) -> None:
"""
log.exceptionでエラーメッセージを出力する
"""
self.log.exception("\n---------------------------------------")
それではこのコードをご紹介します。
使い方
上のクラスをimportし、log = Log(名前)
というふうにインスタンスを作成します。
ログ出力するときにはlog.info(msg)
や log.error(msg)
というふうに出力すれば良いだけです。
Exceptionエラーをログ出力する際には、log.exception()
と宣言することでTracebackを取得して出力してくれます。
下のコード例のように使用する想定で作成しています。
# 使用例
import Log
log = Log("LogName")
log.debug("debugメッセージ")
log.info("infoメッセージ")
log.warning("warningメッセージ")
log.error("errorメッセージ")
try:
x = 1 / 0
except Exception:
log.exception() # エラーをキャッチしてログ出力する
================== 実行結果 ==================
2022-12-03 22:43:15,233 - DEBUG - LogName: debugメッセージ
2022-12-03 22:43:15,234 - INFO - LogName: infoメッセージ
2022-12-03 22:43:15,234 - WARNING - LogName: warningメッセージ
2022-12-03 22:43:15,234 - ERROR - LogName: errorメッセージ
2022-12-03 22:43:15,234 - ERROR - LogName:
---------------------------------------
Traceback (most recent call last):
File "/Users/teruteru/src/practice.py", line 10, in <module>
x = 1 / 0
ZeroDivisionError: division by zero
ログ出力すると、自動的にログファイルを格納するフォルダが1階層上に作成されるようになっています。log / 年 / 月 / 日時.log
という構造でファイルを作成するので管理しやすい仕様です。
.
├── log
│ └── 2022年
│ └── 12月
│ └── 2022-12-03.log
└── src
├── log_class.py
└── practice.py
次にコードの説明を行なっていきます。
コード説明
コンストラクター
コンストラクターでは、ログを判別するための名前、出力フォーマット、出力レベルを設定できます。
内部では、指定された名前で logging.getLogger()
からインスタンスを作成したあと、重複が起こらないように loggers
というset型
に格納しています。
もし既に同じ名前のインスタンスが作成されていたときは、loggers
にあるインスタンスを使用するようにし、出力先を整えるために handlers
を一度クリアしています。
...中略
loggers = {}
def __init__(
self,
name: str,
format: str = "%(asctime)s - %(levelname)s - %(name)s: %(message)s",
level=DEBUG,
):
"""
初期化 フォーマットやlogファイルの設定、
Args:
name (str): logにつける固有な名前
format (str, optional): 出力するフォーマット Defaults to '%(asctime)s - %(levelname)s - %(name)s: %(message)s '.
level : 表示するレベルを設定 [DEBUG, INFO, WARNING, ERROR, CRITICAL] Defaults to DEBUG.
"""
self.name = name
self.format = format
self.level = level
# 重複ログ防止のため、nameでのlogが作成済みならば再利用
if Log.loggers.get(name):
self.log = Log.loggers.get(name)
# ハンドラーをいったん削除して再設定する
self.log.handlers.clear()
else:
# loggersに格納して、同じlogを生成しないように管理
self.log = logging.getLogger(self.name)
self.log.setLevel(self.level)
Log.loggers[name] = self.log
# logの複数発行を防ぐ
self.log.propagate = False
さらに、念のために propagate
プロパティをFalseにすることで親ルートと子ルートのログ出力を防止しているため、複数のログが出力されないようになっています。
参考1:logging — Python 用ロギング機能 – propagate
参考2:Python loggingで2回同じログが表示される場合
ファイル作成
ログ出力先フォルダとファイルの名前を決める処理です。
特に大したことはしていないですが、本日の日付を取得して、フォルダ名とファイル名を決めてフォルダを作成しています。
...中略
# logファイルで使用する日付 ./log/2020年/11月/2020-11-04.logというふうにする
self.today = datetime.datetime.now()
self.text_today = datetime.datetime.strftime(self.today, "%Y-%m-%d")
# このクラスの1階層上に logフォルダを作成
self.log_folder_name = os.path.join(
os.path.dirname(__file__),
"../log/" + str(self.today.year) + "年/" + str(self.today.month) + "月",
)
self.log_file_name = self.text_today + ".log"
os.makedirs(self.log_folder_name, exist_ok=True)
ハンドラー
ハンドラー部分では、StreamHandler
と FileHandler
を指定しています。
それぞれ名前の通り、StreamHandler
はコンソールに対してログ出力できるようにし、FileHandler
はファイルに対してログ出力できるようにしています。
ログの出力先ファイルは self.log_file_name
で決めた名前のファイル名にしています。
出力フォーマットを決めたあと、それぞれのハンドラーにフォーマットを当ててインスタンスに追加します。
...中略
# StreamHandlerの設定
self.sh = logging.StreamHandler()
self.sh.setLevel(self.level)
# FileHandlerの設定 logファイル場所を指定
self.fh = logging.FileHandler(
self.log_folder_name + "/" + self.log_file_name, encoding="utf-8"
)
self.fh.setLevel(self.level)
# フォーマットをセットし、logにハンドラーを追加する
self.formatter = logging.Formatter(self.format)
self.sh.setFormatter(self.formatter)
self.fh.setFormatter(self.formatter)
self.log.addHandler(self.sh)
self.log.addHandler(self.fh)
参考:logging.handlers
— ロギングハンドラ
ログメソッド
このクラスで使用できるメソッドはこんな感じです。最低限のメソッドだけ用意しています。
# メソッド一覧
log.get_log() # getter
# メッセージ出力
log.debug(msg)
log.info(msg)
log.warning(msg)
log.error(msg)
# エラーメッセージ出力
log.exception()
ゲッターを実装しているので、PySimpleGUI などでログ出力したい時にはハンドラークラスを作成してログを渡すことができます。
直したかったところ、できなかったところ
引数に複数のオブジェクトが与えられるように実装できなかったのがとても悔しいですね。自分の実力だと実装できませんでした…
Pythonの print
文のように複数のオブジェクトでも出力できるようにしたかったですね。ソースコードを見ましたがC言語で書かれており参考にできませんでした。
良い実装方法があれば教えていただけると嬉しいです。
まとめ
当時は Logging
の使い方が全くわからなかったので、簡単に設定できたらいいなの気持ちでこのクラスを作成しました。
Logクラスを作成したことで使い回しができるようになり、機械学習やGUI作成の際にもログ出力が簡単に行えるようになったので今でもめちゃくちゃ重宝しています。
ぜひこちらを参考にしてカスタマイズしていただいて、最強のLogクラスを作ってみてください!
この記事が誰かの役に立てますように。
第1位:フリーランスを始めるならITプロパートナーズ
第2位:キャリアサポートサービス「クラウドテック」
第3位:ライティングからサイト制作まで【Bizseek】
コメントを書く