2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Python】抽象構文木(AST)入門:解析・変換・安全評価まで "手を動かして"理解する

Posted at

はじめに

Pythonのソースコードは、実行前に一度「構文木(AST:Abstract Syntax Tree)」という形に変換されます。 このASTを扱えるようになると、コードを安全に解析・変換・自動操作できる強力なツールを手に入れることができます。

今回は、Pythonの ast モジュールを使って...

  • コードを木構造として捉える感覚
  • NodeVisitor / NodeTransformer の使い方
  • AST → ソースコードへ戻す方法(ast.unparse
  • 安全な eval の作り方(式だけを評価するミニ実装)
  • ログ自動挿入、演算子カウント、型ヒント自動付与などの応用

までを、実行可能なコードとともに整理しました。

この記事で得られること

  • ast.parse でコードを"木"として扱うイメージがつかめる
  • NodeVisitor / NodeTransformer を使った解析・変換の基本
  • ast.unparse によるコード再生成(AST → ソースへのラウンドトリップ)
  • ASTを使った「安全な式評価(安全なeval)」の仕組み
  • 応用例:ログ自動挿入、演算子カウント、型ヒント自動付与

こんな人におすすめ

  • eval は危険と聞いたけど、安全な使い方を知りたい
  • ASTって聞いたことあるけど、実例付きで理解したい
  • リファクタリングやコード自動修正をASTでやってみたい
  • NodeVisitor / NodeTransformer を実務で使えるレベルで学びたい

サンプルコード(Colabで実行可)

ノートブック形式のサンプルコードは GitHub にあります。
Google Colab で開けばそのまま実行可能です。

📄 https://github.com/sakai-path/python-ast-guide/tree/main/notebooks
(ファイル:python_ast_guide.ipynb

Open In Colab

1. ASTって何?なぜ使う?

AST(Abstract Syntax Tree)=コードの構造を表現する木 です。

Pythonのソースコードは実行される前に、まず「抽象構文木」という中間表現に変換されます。この木構造では、コードの各要素(変数、演算子、関数呼び出しなど)がノードとして表現され、親子関係で構造化されています。

なぜASTを使うのか?

メリットは大きく3つ

  • 安全性:文字列置換ではなく"構造"に基づく変換で壊しにくい
  • 精密さ:関数だけ、演算子だけ、など"狙い撃ち"の解析ができる
  • 再利用性:解析(Visitor)と変換(Transformer)を分離できる

例えば、コード内のすべての print 文を logging.info に置き換えたい場合、正規表現では複雑な条件分岐が必要ですが、ASTなら確実に「関数呼び出しノード」だけを対象にできます。

2. 最小の例:ast.parseast.dump

まずは最もシンプルな例から始めましょう。ast.parse() はPythonコードを構文木に変換し、ast.dump() はその構造を見やすく表示します。

python
import ast

code = """
x = 10
y = x * 2 + 5
def add(a, b):
    return a + b
print(add(y, 3))
"""

tree = ast.parse(code, mode="exec")
print(type(tree))              # -> <class '_ast.Module'>
print(ast.dump(tree, indent=4))  # 構造を階層表示

modeパラメータの使い分け

ast.parse()mode パラメータは、解析するコードの種類を指定します。

  • mode="exec":複数の文を含むコード(通常のPythonファイル)
  • mode="eval":単一の式(eval() で評価可能なもの)
  • mode="single":対話モードの1行(REPLのような動作)

実行すると、コードが木構造として解析され、各要素(代入、関数定義、演算など)がノードとして表現されているのが確認できます。

3. ノードを歩く:ast.walk / NodeVisitor

ASTの木構造を探索する方法は2つあります。ast.walk() は全ノードを順に訪問し、NodeVisitor は特定のノードタイプに対して処理を定義できます。

python
from collections import Counter

# 全ノードの種類を数える
counts = Counter(type(n).__name__ for n in ast.walk(tree))
print(counts)

# 関数名だけ拾う Visitor
class FuncCollector(ast.NodeVisitor):
    def __init__(self):
        self.func_names = []
    
    def visit_FunctionDef(self, node):
        self.func_names.append(node.name)
        self.generic_visit(node)  # 子ノードも訪問

collector = FuncCollector()
collector.visit(tree)
print(collector.func_names)  # -> ['add']

NodeVisitorの仕組み

NodeVisitor クラスを継承すると、visit_ノード名 というメソッドで特定のノードタイプだけを処理できます。上の例では visit_FunctionDef で関数定義ノードだけを処理しています。

self.generic_visit(node) を呼ぶと、そのノードの子要素も再帰的に訪問します。これを忘れると、ネストした構造(関数内の関数など)が処理されません。

4. コードを"安全に"書き換える:NodeTransformer

NodeTransformer を使うと、ASTを変更できます。例として、「2倍の掛け算」を「左シフト演算」に置き換えてみましょう(x * 2x << 1)。

python
class TimesTwoToShift(ast.NodeTransformer):
    def visit_BinOp(self, node):
        # まず子ノードを処理
        self.generic_visit(node)
        
        # 条件: 掛け算で、右辺が定数2の場合
        if (isinstance(node.op, ast.Mult) and 
            isinstance(node.right, ast.Constant) and 
            node.right.value == 2):
            # 左シフト演算に置き換え
            return ast.BinOp(
                left=node.left, 
                op=ast.LShift(), 
                right=ast.Constant(1)
            )
        return node

tree2 = ast.parse(code)
new_tree = TimesTwoToShift().visit(tree2)
ast.fix_missing_locations(new_tree)   # 位置情報を補完

重要なポイント

  1. self.generic_visit(node) を最初に呼んで、子ノードも変換対象にする
  2. 新しいノードを返すことで、元のノードを置き換える
  3. ast.fix_missing_locations() で行番号などの位置情報を補完(これを忘れるとコンパイル時にエラーになることがある)

5. ast.unparse でソースへ戻す(3.9+)

Python 3.9以降では、変更したASTを再びPythonコードに戻すことができます。

python
print(ast.unparse(new_tree))

これにより、変換後のコードを実際に確認できます。

例えば、先ほどの例では y = x * 2 + 5(x << 1) + 5 に変わります
(演算子の優先順位を保つため、必要な場合は括弧が自動的に付与されます)。

補足:x << 1 + 5 としてしまうと、再パース時に x << (1 + 5) と解釈されてしまい意味が変わります。ASTでは常に (x << 1) + 5 の構造になっています。

Python 3.8以下の場合

3.8以下では標準ライブラリに unparse がないため、サードパーティの astor ライブラリを使います。

python
# pip install astor
import astor
print(astor.to_source(new_tree))

6. ミニ実装:ASTで作る「安全な式評価器」

eval() は便利ですが、ユーザーが入力した文字列をそのまま実行すると、ファイル削除など任意コード実行の危険があります。
ASTを使えば、「数式として安全なものだけ」 を評価できます。

python
import ast, operator, math

# 許可する演算子と関数のみ
BIN_OPS = {
    ast.Add: operator.add,
    ast.Sub: operator.sub,
    ast.Mult: operator.mul,
    ast.Div: operator.truediv,
}
ALLOWED_FUNCS = {"sqrt": math.sqrt, "sin": math.sin}
ALLOWED_CONSTS = {"pi": math.pi, "e": math.e}

def safe_eval(expr: str):
    """eval の安全な代替:四則演算+一部の関数だけ許可"""
    node = ast.parse(expr, mode="eval").body

    def _ev(n):
        if isinstance(n, ast.Constant):
            return n.value
        if isinstance(n, ast.BinOp) and type(n.op) in BIN_OPS:
            return BIN_OPS[type(n.op)](_ev(n.left), _ev(n.right))
        if isinstance(n, ast.UnaryOp):  # +x, -x
            return +_ev(n.operand) if isinstance(n.op, ast.UAdd) else -_ev(n.operand)
        if isinstance(n, ast.Name) and n.id in ALLOWED_CONSTS:
            return ALLOWED_CONSTS[n.id]
        if isinstance(n, ast.Call) and n.func.id in ALLOWED_FUNCS:
            return ALLOWED_FUNCS[n.func.id](*[_ev(a) for a in n.args])
        raise ValueError("未許可の構文です")

    return _ev(node)

# 使用例
print(safe_eval("1 + 2 * 3"))      # 7
print(safe_eval("sqrt(2) * pi"))   # OK
# safe_eval("__import__('os').system('rm -rf /')")  # → エラー

なぜ安全なのか?

防げるもの 理由
__import__ / exec それらのノードをASTで許可していないため
os.system(...) 属性アクセス(.)を禁止しているため
代入・関数定義など mode="eval" なので式以外は構文エラー
辞書やリストアクセス x[0] のような Subscript も許可していない

7. 応用①:関数へ自動ログを差し込む

デバッグ時に、すべての関数の実行をログに記録したい場合があります。ASTを使えば、既存のコードを変更せずに自動でログを挿入できます。

python
class InjectLogger(ast.NodeTransformer):
    def visit_FunctionDef(self, node):
        # 子ノードを処理
        self.generic_visit(node)
        
        # ログ出力文を作成
        log_stmt = ast.parse(f'print("[LOG] {node.name} called with args:", locals())').body[0]
        
        # 関数本体の先頭に挿入
        node.body.insert(0, log_stmt)
        return node

# 使用例
sample_code = """
def multiply(x, y):
    return x * y

def calculate(a, b):
    result = multiply(a, b)
    return result + 10
"""

tree3 = ast.parse(sample_code)
logged = InjectLogger().visit(tree3)
ast.fix_missing_locations(logged)

# 変換後のコードを実行
exec(compile(logged, "<ast>", "exec"))
print(calculate(3, 4))  # [LOG] が出力される

実用的な拡張

実際の開発では、print の代わりに logging モジュールを使ったり、関数の引数や戻り値も記録したりすることができます。また、デコレータと組み合わせて、特定の関数だけを対象にすることも可能です。

8. 応用②:演算子の出現回数を集計する

コードの複雑さを測る指標として、演算子の使用頻度を調べることができます。

python
sample = """
def calc(a, b):
    x = a + b
    y = x * 2 - a ** 2
    return (y / 3) + (a - b)

def distance(x1, y1, x2, y2):
    dx = x2 - x1
    dy = y2 - y1
    return (dx ** 2 + dy ** 2) ** 0.5
"""

# 演算子名のマッピング
op_names = {
    ast.Add: '+', 
    ast.Sub: '-', 
    ast.Mult: '*', 
    ast.Div: '/', 
    ast.Pow: '**',
    ast.Mod: '%',
    ast.FloorDiv: '//'
}

from collections import Counter

def count_operators(code_str):
    """コード内の演算子使用回数を集計"""
    counts = Counter()
    tree = ast.parse(code_str)
    
    for node in ast.walk(tree):
        if isinstance(node, ast.BinOp) and type(node.op) in op_names:
            counts[op_names[type(node.op)]] += 1
    
    return counts

result = count_operators(sample)
print("演算子の使用頻度:")
for op, count in result.most_common():
    print(f"  {op:3s}: {count}")

活用例

この手法は以下のような場面で役立ちます。

  • コードメトリクスの計算:複雑度の指標として使用
  • 最適化の候補探し:頻繁に使われる演算を特定
  • コーディング規約のチェック:特定の演算子の使用を制限

9. 応用③:関数に型ヒントを自動付与(デモ)

既存のコードに型ヒントを追加する作業を自動化できます。

python
class AddTypeHints(ast.NodeTransformer):
    def __init__(self, default_type="Any"):
        self.default_type = default_type
    
    def visit_FunctionDef(self, node):
        # 子ノードを処理
        self.generic_visit(node)
        
        # 戻り値の型ヒントがない場合は追加
        if node.returns is None:
            # 簡単な推論: return文を探す
            for stmt in ast.walk(node):
                if isinstance(stmt, ast.Return) and stmt.value:
                    if isinstance(stmt.value, ast.Constant):
                        if isinstance(stmt.value.value, int):
                            node.returns = ast.Name(id="int")
                            break
                        elif isinstance(stmt.value.value, float):
                            node.returns = ast.Name(id="float")
                            break
            else:
                node.returns = ast.Name(id=self.default_type)
        
        # 引数の型ヒントを追加
        for arg in node.args.args:
            if arg.annotation is None:
                # 引数名から型を推測(簡易版)
                if 'id' in arg.arg or 'count' in arg.arg:
                    arg.annotation = ast.Name(id="int")
                elif 'name' in arg.arg or 'text' in arg.arg:
                    arg.annotation = ast.Name(id="str")
                else:
                    arg.annotation = ast.Name(id=self.default_type)
        
        return node

# 使用例
code_without_hints = """
def calculate_area(width, height):
    return width * height

def get_user_name(user_id):
    # 実際の実装は省略
    return "John Doe"
"""

tree = ast.parse(code_without_hints)
typed = AddTypeHints(default_type="Any").visit(tree)
ast.fix_missing_locations(typed)

print("型ヒント付きコード:")
print(ast.unparse(typed))

より高度な型推論

実用的なツールにするには、以下のような改善が考えられます。

  • 変数の使用パターンから型を推論
  • 外部ライブラリの型情報を参照
  • テストコードから実際の型を収集
  • mypy のスタブファイルと連携

10. まとめ・実運用の勘所

ベストプラクティス

  • Visitorで解析、Transformerで変換:責務を分離すると拡張しやすい
  • 置換後は ast.fix_missing_locations() を忘れずに
  • バージョン差:Python 3.9+ なら ast.unparse()、それ未満は astor などで代替
  • 安全評価や自動変換など、"文字列置換の限界"を超えるときにASTが効く

実運用での注意点

  1. パフォーマンス:ASTの解析・変換は文字列操作より遅いため、大量のコードを処理する場合は注意
  2. Python バージョンの互換性:新しい構文(match 文など)は古いPythonでは解析できない
  3. コメントの消失:ASTはコメントを保持しないため、ソース変換時は要注意
  4. デバッグ情報:行番号などの情報が失われやすいので、エラー時の追跡が困難になることがある

実際のユースケース

  • 静的解析ツールpylintflake8 などのリンターツール
  • コード変換ツール2to3(Python 2→3変換)、black(フォーマッター)
  • メタプログラミング:デコレータの実装、マクロ的な機能
  • セキュリティ:ユーザー入力の安全な評価、サンドボックス実行

ASTは強力なツールですが、適切な場面で使うことが重要です。単純な文字列置換で済む場合は無理に使う必要はありませんが、コードの構造を理解した上での変換が必要な場合は、ASTが最適な選択肢となります。

※ASTはコードを「構造」として扱うため、コメントや空行などの情報は保持しません。
コード整形や再構築をするツール(black, autopep8)などと組み合わせて使うと実用的になります。

参考資料


Happy Coding! 🐍✨

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?