📝

コードを変更せずにPython実行時のログをとる

2024/12/19に公開

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

  • ASTは、Pythonコードの文法構造を木構造で表現したものです
  • Pythonコードは、まず文字列として書かれたコードから構文解析され、ASTに変換されます
  • その後、このASTがバイトコードにコンパイルされ、インタプリタによって実行されます

プログラムが実行される前にとる中間形式のようなものです。

解析用プログラム

解析用プログラムの処理内容は以下の通りです。

program flow

  1. ファイル(調査対象のプログラム)を読み込む
  2. 読み込んだプログラムのASTを作成
  3. 作成したASTにログを取る処理を付け足す
  4. 付け足したASTを実行
  5. 実行時にログが出力される

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文をあちこちに入れる必要がない
  • デバッガのように一時停止をしない -> 時間や処理速度が原因のバグを突き止めやすい
  • 再利用可能
  • 外部ツール依存がない -> リモート接続などで環境に制約がある場合に嬉しい
Aidemy Tech Blog

Discussion