📝
コードを変更せずにPython実行時のログをとる
tl;dr
思いついたのでやってみた- Python実行時のログを取る
- ログを取得するためのコードは追加しない
- 追加せずにログを取得するために、astモジュールを使用した解析用プログラムを作成する
記事の内容
↓このコードから
def calculate_factorial(n: int) -> int:
"""階乗を計算する関数
Args:
n (int): 階乗を計算する数
Returns:
int: 階乗の計算結果
"""
if n <= 1:
return 1
else:
return n * calculate_factorial(n - 1)
if __name__ == "__main__":
result = calculate_factorial(5)
print(result)
↓このログを取得します
関数calculate_factorialに入りました 引数: n=5
関数calculate_factorialに入りました 引数: n=4
関数calculate_factorialに入りました 引数: n=3
関数calculate_factorialに入りました 引数: n=2
関数calculate_factorialに入りました 引数: n=1
returnします 返り値: 1
returnします 返り値: 2
returnします 返り値: 6
returnします 返り値: 24
returnします 返り値: 120
120
コード実行時の関数の"引数の値"と"返り値"が記録されます。
方法
コードの変更なしにログを取得するには、astモジュールを使用した解析用プログラムを作成します。
astモジュールとは
astモジュールとは、AST(Abstract Syntax Tree、抽象構文木)を扱うライブラリです。
ASTの解説はそれだけで記事が1つかけてしまうので、ざっくり概要だけ説明します。
- ASTは、Pythonコードの文法構造を木構造で表現したものです
- Pythonコードは、まず文字列として書かれたコードから構文解析され、ASTに変換されます
- その後、このASTがバイトコードにコンパイルされ、インタプリタによって実行されます
プログラムが実行される前にとる中間形式のようなものです。
解析用プログラム
解析用プログラムの処理内容は以下の通りです。
- ファイル(調査対象のプログラム)を読み込む
- 読み込んだプログラムのASTを作成
- 作成したASTにログを取る処理を付け足す
- 付け足したASTを実行
- 実行時にログが出力される
1. ファイル(調査対象のプログラム)を読み込む
# 調査対象プログラムの読み込み
file_name = "advent_calendar2024_sample.py"
with open(file_name) as file:
source_code = file.read()
単純に調査対象のプログラムを読み込みます。
2. 読み込んだプログラムのASTを作成
import ast
# 調査対象プログラムの抽象構文木を作成
tree = ast.parse(source_code, filename=file_name)
調査対象プログラムのASTを作成します
3. ログを取る処理をASTに付け足す
class FunctionLoggerTransformer(ast.NodeTransformer):
def visit_FunctionDef(self, node):
"""
関数の最初に引数の値を出力するprint文を挿入
"""
# 引数の変数名を取得する
args_str = ", ".join(f"{arg.arg}={{str({arg.arg})}}" for arg in node.args.args)
# 取得した変数名でprint文を作成し、ASTに変換
log_args = ast.parse(f'print(f"Entering {node.name} with arguments: {args_str}")').body[0]
# print文のASTを元のASTに挿入する
node.body = [log_args] + [self.visit(stmt) for stmt in node.body]
return node
def visit_Return(self, node):
"""
関数のreturn前に、返り値を出力するprint文を挿入
以下のようにコードを変換している
return a + b --> _return_value = a + b
--> print(_return_value)
--> return _return_value
"""
# returnの式を文字列として一時変数に保存
temp_var_name = "_return_value"
# returnの値を保存する代入文を作成(上記の_return_value = a + bに相当)
temp_var_assignment = ast.parse(f"{temp_var_name} = {ast.unparse(node.value)}").body[0]
# 返り値を表示するprint文(上記のprint(_return_value)に相当)を作成し、ASTに変換
log_return = ast.parse(f'print(f"Exiting with return value: {{{temp_var_name}}}")').body[0]
# 一時変数を返すreturn文に変更(上記のreturn _return_valueに相当)
updated_return = ast.parse(f"return {temp_var_name}").body[0]
# 上記3つの文を組み合わせて返す
return [temp_var_assignment, log_return, updated_return]
# ログ出力コードを挿入した抽象構文木を作成
transformer = FunctionLoggerTransformer()
modified_tree = transformer.visit(tree)
ログを出力するコードを埋め込みます。
埋め込む処理の詳細はコードのコメントに記載。
4. AST(付け足したプログラム)を実行
# ログ出力コードを挿入した抽象構文木を実行
compiled_code = compile(modified_tree, filename=file_name, mode="exec")
exec(compiled_code)
ログ出力コードを付け加えたプログラムを実行します。
5. ログが出力される
調査用プログラムを実行します。
$ python /workspace/advent_calendar2024_logging.py > log.txt
関数calculate_factorialに入りました 引数: n=5
関数calculate_factorialに入りました 引数: n=4
関数calculate_factorialに入りました 引数: n=3
関数calculate_factorialに入りました 引数: n=2
関数calculate_factorialに入りました 引数: n=1
returnします 返り値: 1
returnします 返り値: 2
returnします 返り値: 6
returnします 返り値: 24
returnします 返り値: 120
120
うまくログが出力されました。
astを使用する利点
さあ?
正直、利点があるのかわからないですが、思いつくのはこの辺でしょうか
- コードを変更せずにログが取れる
- 大規模コードでprint文をあちこちに入れる必要がない
- デバッガのように一時停止をしない -> 時間や処理速度が原因のバグを突き止めやすい
- 再利用可能
- 外部ツール依存がない -> リモート接続などで環境に制約がある場合に嬉しい
Discussion