nikkie-ftnextの日記

イベントレポートや読書メモを発信

PythonのASTと戯れる第一歩:文字列リテラルを全部置き換えてみる

目次

きっかけ

昨日参加したYAPC::Kyoto 2023。
その中のASTのトークに触発されました

いくつか例を見せていただき、「真似してやってみよう!」とモチベーションが上がりました。

一番はじめの例、「console.logの中身の置き換え、とりあえず(すべての)文字列を置き換え」をPythonで考えてみます。

今回の参考文献

フューチャーさんの技術ブログに例を見つけ、

そこから公式のastモジュールのドキュメント

をたどって、「ast.NodeTransformerを継承すればできそう」と感触を得ました。

できたもの:コード中のすべての文字列を置き換え

Python 3.10.9 で動かしています

% python replace_str_literal.py
1 + 2
'ham' + 'ham'
print('ham')
print(108)
print('ham', 'ham')

文字が全部"ham"に!

YAPCで知ったのですが、ASTをいじって文字列として出力、それをPython処理系に実行させられます!

% python replace_str_literal.py | python
ham
108
ham ham

ドキュメントメモ

parse / unparse

https://docs.python.org/ja/3/library/ast.html#ast.parse

小さいコードを文字列(改行のために3連引用符使用)として定義し、parse関数でASTに変換します。

https://docs.python.org/ja/3/library/ast.html#ast.unparse

ASTを文字列に戻すのがunparse関数。
(実は追加されたのがPython 3.9とけっこう最近なのです)

dump

https://docs.python.org/ja/3/library/ast.html#ast.dump

主な使い道はデバッグです。

スクリプトpython -iで実行し、print("spam")といったコードのASTがどうなっているか確認するのに使いました。

>>> tree = ast.parse('print("spam")')
>>> print(ast.dump(tree, indent=2))
Module(
  body=[
    Expr(
      value=Call(
        func=Name(id='print', ctx=Load()),
        args=[
          Constant(value='spam')],
        keywords=[]))],
  type_ignores=[])

「文字列リテラルConstantらしいぞ」という感じで変えるべきところを見定めていきます。

NodeTransformer

https://docs.python.org/ja/3/library/ast.html#ast.NodeTransformer

NodeVisitor のサブクラスで抽象構文木を渡り歩きながらノードを変更することを許すものです。

ビジター・メソッドの戻り値が None ならば、ノードはその場から取り去られ、そうでなければ戻り値で置き換えられます。

ビジター・メソッドはself.visit_<classname>となるそうです(親クラスのNodeVisitorのドキュメントより)。
今回はConstantノードを変えるのでvisit_Constantですね。

「定数をまとめて扱うの?」と思いましたが、親クラスのドキュメントに以下のようにもありました。

バージョン 3.8 で非推奨: visit_Num(), visit_Str(), visit_Bytes(), visit_NameConstant() および visit_Ellipsis() の各メソッドは非推奨です。(略) 全ての定数ノードを扱うには visit_Constant() を追加してください。

Constant

https://docs.python.org/ja/3/library/ast.html#ast.Constant

ドキュメントの「リテラル」のゾーンにあります。

Constant リテラルvalue 属性は定数値を表す Python オブジェクトを保持します。

value属性の値の型を確認して、文字列だったら置き換えるという実装をしました。

こうして、文字列を全部置き換えちゃう、やべープログラムが爆誕しました!