Upgrade to Pro — share decks privately, control downloads, hide ads and more …

PyCon JP 2022/ASTに入門する

atama plus
October 14, 2022

PyCon JP 2022/ASTに入門する

2022年10月14日のPyCon JP 2022にて、atama plus・アルゴリズム開発の安本(@myasumoto)が登壇しました。

▼PyCon JP 2022 登壇情報
https://2022.pycon.jp/timetable?id=GFM7LS

▼開発チームの情報発信リンク集
https://linktr.ee/atama_plus_dev

atama plus

October 14, 2022
Tweet

More Decks by atama plus

Other Decks in Programming

Transcript

  1. A S T に ⼊ ⾨ す る 安 本

    雅 啓 2022.10.14
  2. ⓒ 2022 atama plus Inc. アジェンダ 1. ⾃⼰紹介 2. 本セッションの概要説明

    3. Pythonコードはどのようにして実⾏されるか 4. ASTの構造 5. astモジュールについて 6. astモジュールの活⽤事例 7. まとめ 2
  3. ⓒ 2022 atama plus Inc. アジェンダ 1. ⾃⼰紹介 2. 本セッションの概要説明

    3. Pythonコードはどのようにして実⾏されるか 4. ASTの構造 5. astモジュールについて 6. astモジュールの活⽤事例 7. まとめ 3
  4. ⾃⼰紹介 Algorithm Engineer @ atama plus Inc. • SIerでのR&Dエンジニア、AI StartupでのCTOを経て、

    2021年4⽉よりatama plusにジョイン。以降、⼀貫し てatama+プロダクトのアルゴリズム開発に携わる。 • Like:機械学習/Deep Learning/強化学習/DDD(最近 勉強中) • Hobby:料理/LEGO 4 安 本 雅 啓 @ m y a s u m o t o
  5. ⓒ 2022 atama plus Inc. アジェンダ 1. ⾃⼰紹介 2. 本セッションの概要説明

    3. Pythonコードはどのようにして実⾏されるか 4. ASTの構造 5. astモジュールについて 6. astモジュールの活⽤事例 7. まとめ 5
  6. ⓒ 2022 atama plus Inc. 本セッションの概要 • 本セッションを通して得られるもの • ASTとは何か

    • Pythonの実⾏プロセスの中でのASTの位置付け • astモジュールの概要と使い⽅ • astモジュールを使うことで、どのようなことができるようになるか • 本セッションの想定する対象者 • ASTという⾔葉を初めて聞く/聞いたことはあるが詳しく知らない⽅ • Linter等の静的解析ツールを普段使っており背景技術に興味がある⼈ • など 6
  7. ⓒ 2022 atama plus Inc. アジェンダ詳細 7 Pythonコードがどの ようにして実⾏されるか ASTの構造

    astモジュールの使い⽅ astモジュールの活⽤事例 Pythonを実⾏したとき、ソースコードがどのように変換されて 最終的に実⾏されるかを簡単に概観します。またその中での ASTの位置付けについても説明します。 ASTの基本的な構造や、ASTを構成するノードの種類、ASTを可 視化できるツールについて紹介します。 astモジュールの想定する、「検査する」と「変更を加える」の 2つのユースケースについて、astモジュールをどのようにして 使えばよいか、紹介します。 astモジュールを使って、⾃作linterやtestingフレームワークの プロトタイプを書いてみます。また実際のOSSなどでの活⽤事 例についても紹介します。
  8. ⓒ 2022 atama plus Inc. アジェンダ 1. ⾃⼰紹介 2. 本セッションの概要説明

    3. Pythonコードはどのようにして実⾏されるか 4. ASTの構造 5. astモジュールについて 6. astモジュールの活⽤事例 7. まとめ 8
  9. ⓒ 2022 atama plus Inc. 実⾏までのプロセス • pythonコードを実⾏した時、内部では以下のようなプロセスが⾛ります。 9 Python

    コード CST (Concrete Syntax Tree) AST (Abstract Syntax Tree) バイト コード Lexer Parser Runtime Compiler 字句解析器 構⽂解析器
  10. ⓒ 2022 atama plus Inc. 実⾏までのプロセス • pythonコードを実⾏した時、内部では以下のようなプロセスが⾛ります。 10 Python

    コード CST (Concrete Syntax Tree) AST (Abstract Syntax Tree) バイト コード Lexer Parser Runtime Compiler 字句解析器 構⽂解析器
  11. ⓒ 2022 atama plus Inc. CSTとAST • CST(Concrete Syntax Tree)とAST(Abstract

    Syntax Tree)は⼀般⽤語であり、 ソースコードをParse(構⽂解析)して、その結果を、機械が解釈・処理しやすい ように、⽊構造として表現したものです。 11 CST • ⽂脈を考慮せず、⽂法に基づいてソー スコードを⽊構造に変換したもの。 • 例えば、CSTでは、a / 2 といった⽂の 「/」を「SLASH」というノードで保 持する。 AST • ソースコードを、⽂脈を考慮した上で、 ⽊構造に変換したもの。 • 例えば、ASTでは、a / 2 といった⽂の 「/」は、「ast.Div」というノードで 保持する。 参考 https://eli.thegreenplace.net/2009/02/16/abstract-vs-concrete-syntax-trees/
  12. ⓒ 2022 atama plus Inc. CSTとAST 12 CST AST https://instagram-engineering.com/static-analysis-at-scale-an-instagram-story-8f498ab71a0c

  13. ⓒ 2022 atama plus Inc. CSTはどんなもの? • Pythonでは、parserモジュールを使うことによって、構⽂解析後の結果を確認 することができます。 13

    • 例えば、「a/1」という式は、構⽂ 解析によって右のような形の⽊構造 に変換されます。 • ここで数字は、tokenとsymbolのID を表しているのですが、このままだ と分かりにくいので、IDから名前に 変換してみます。
  14. ⓒ 2022 atama plus Inc. CSTはどんなもの? • tokenとsymbolを名前に変換して みました。少しは分かりやすく なったでしょうか、、?

    • ⼤⽂字で表現されたものが token(SLASH, NUMBERなど)、⼩ ⽂字で表現されたもの(atom, term など)がsymbolとなります。 • これだけだと、扱いづらいですね。 14 「/」は「SLASH」と して表現されている
  15. ⓒ 2022 atama plus Inc. ASTはどんなもの? • Pythonでは、astモジュールを使うこと によって、作成されたASTを確認するこ とができます。

    • 例えば、a=1; b=a+2というコードは、 右のような形の⽊構造に変換されます。 • 各ノードには、「Assign」「BinOp」 「Constant」というように、⼈が⾒ても 想像しやすい名前がついているのが分か ります。 15 「/」は「Div()」と して表現されている
  16. ⓒ 2022 atama plus Inc. 実⾏までのプロセス • pythonコードを実⾏した時、内部では以下のようなプロセスが⾛ります。 16 Python

    コード CST (Concrete Syntax Tree) AST (Abstract Syntax Tree) バイト コード Lexer Parser Runtime Compiler 字句解析器 構⽂解析器
  17. ⓒ 2022 atama plus Inc. バイトコードはどんなもの? • Pythonでは、disモジュールを使うことに よって、バイトコードを確認することが できます。

    • dis.disは、バイトコードを逆アセンブル した結果を表⽰するものです。 • LOAD_CONST、BINARY_ADDというの が、バイトコード命令を表しています。 • Pythonのランタイムは、このバイトコー ドを解釈して実⾏します。 17 命令(オペコード) オペランド
  18. ⓒ 2022 atama plus Inc. 実⾏までのプロセス(振り返り) • これで⼀通りのプロセスと、その中でのASTの位置付けについて、⾒てきまし た。次は本題の、ASTの詳細に⼊っていきたいと思います。 18

    Python コード CST (Concrete Syntax Tree) AST (Abstract Syntax Tree) バイト コード Lexer Parser Runtime Compiler 字句解析器 構⽂解析器
  19. ⓒ 2022 atama plus Inc. アジェンダ 1. ⾃⼰紹介 2. 本セッションの概要説明

    3. Pythonコードはどのようにして実⾏されるか 4. ASTの構造 5. astモジュールについて 6. astモジュールの活⽤事例 7. まとめ 19
  20. ⓒ 2022 atama plus Inc. (改めて)ASTとは? • ASTは、ソースコードを、⽂脈を考慮した上で、⽊構造に変換したもの。 • ⽊構造のノードには、式や制御⽂、リテラル(⽂字列、数字)、変数などが含

    まれます(完全なリストについては、公式ドキュメントを参照) • ノードの種類によって、保持するプロパティが異なる。 • 例えば、「+」のようなものはBinOpに分類されますが、left, op, rightの3 つの属性が存在しており、leftが左辺、opがオペレーションの種別(Add, Subなど)、rightが右辺を表す属性となります。 • 具体的に⾒ていきましょう! 20
  21. ⓒ 2022 atama plus Inc. ASTの例(1/3) • まずトップのノードはModuleとなり、そ の属性のbodyの中に、コード中に記述さ れた全statementが配列として⼊ります。

    • 「b = a+1」のstatementで何が起きるか は、⽊構造のLeaf側から⾒ていくと分か りやすいです。 21 1. aの変数をロードする(Load) 2. 定数「1」を準備する(Constant) 3. 1, 2の値の加算演算(Add/BinOp)を⾏う 4. 3の結果を変数bに代⼊する(Assign) 5. 変数bの値を保存する(Store) (1) (2) (3) (4) (5)
  22. ⓒ 2022 atama plus Inc. ASTの例(2/3) • 次は、制御構造が⼊るケースを⾒てみ ましょう。 •

    Ifのノードには、test、body、orelseの 属性が存在します。 • testにはIf⽂の条件が⼊り、bodyにはIf ⽂の中のブロックの内容が⼊ります。 • orelseはelse句を書いた際、そのブロッ クの内容が⼊ります。この例ではelse 句を書いてないため、空配列となって います。 22
  23. ⓒ 2022 atama plus Inc. ASTの例(3/3) • 今度は、関数を呼び出すケースを⾒てみま しょう。 •

    関数定義は、FunctionDefというノードで 記述されます。引数がargsに、関数の中⾝ がbody以下に格納されているのが分かるか と思います。 • 次に関数を呼び出すのは、Callノードです。 args, keywordsに引数を格納します。 ちょっと興味深いのは、関数もまたLoad() される対象である、ということです。 23
  24. ⓒ 2022 atama plus Inc. ASTを確認するためのGUIツール • Instaviz 24 ASTをグラフィカルに表⽰

    できるほか、逆アセンブル の結果なども併せて⾒るこ とができる。 https://github.com/tonybaloney/instaviz
  25. ⓒ 2022 atama plus Inc. アジェンダ 1. ⾃⼰紹介 2. 本セッションの概要説明

    3. Pythonコードはどのようにして実⾏されるか 4. ASTの構造 5. astモジュールについて 6. astモジュールの活⽤事例 7. まとめ 25
  26. ⓒ 2022 atama plus Inc. ASTモジュール • astモジュールには、主に、前章で説明したノード・クラスの他に、ASTノード を検査する、または、操作する(変更を加える)ためのユーティリティ関数や クラスが定義されています。

    • ここでは、以下の2つのユースケース毎に、使い⽅を簡単に説明していきます。 • ASTを検査する • ASTの構造に変更を加える 26
  27. ⓒ 2022 atama plus Inc. ASTの検査: (1)要素アクセス • parse後のASTに対して、属性 アクセスをチェーンさせるこ

    とで、⽬的となる属性を取得 することができます。 27 この値に アクセスしたい
  28. ⓒ 2022 atama plus Inc. ASTの検査: (2)ast.walkを使う • ast.walk関数は、ASTのノードを取り込んでジェネレータを返します。 •

    ただし、順序は不定です。(あまり使い道がなさそう?😖) 28
  29. ⓒ 2022 atama plus Inc. ASTの検査: (3)ast.NodeVisitor • ast.NodeVisitorクラスを継承したクラスの中で、visit_XXXという関数を overrideします。

    • すると、XXXのノードを訪問した際に、その関数の中⾝が実⾏されます。 29 visit_Constant関数を定義すると、 Constantノードを訪問したとき に呼ばれるようになります generic_visitは、visit_XXX関数が定 義されていないときに呼ばれる関数 です。 overrideした場合は、最後に明⽰的 に呼んでおかないと、その⼦ノード が⾛査されなくなってしまいます。
  30. ⓒ 2022 atama plus Inc. ASTの検査: (3)ast.NodeVisitor • NodeVisitorを使った⾛査がどのような 順番に⾏われるかをみてみましょう。

    30 Module body Assign Assign Name Constant Store Name BinOp Store Name Load Add Constant
  31. ⓒ 2022 atama plus Inc. ASTの検査: (3)ast.NodeVisitor • NodeVisitorを使った⾛査がどのような 順番に⾏われるかをみてみましょう。

    31 Module body Assign Assign Name Constant Store Name BinOp Store Name Load Add Constant あらゆるNodeに対して呼ばれる generic_visit関数をフック NodeVisitorが辿る 順番を確認できる
  32. ⓒ 2022 atama plus Inc. ASTの検査: (3)ast.NodeVisitor • Moduleノードからスタートして、深さ 優先探索によって⾛査されるのが分か

    ります。 32 Module body Assign Assign Name Constant Store Name BinOp Store Name Load Add Constant
  33. ⓒ 2022 atama plus Inc. ASTの検査: (4)その他 • これまでに紹介したもの以外に、以下のようなhelper関数が⽤意されています (詳細は公式doc参照)

    33 関数名 概要 ast.iter_fields() ノードの属性に対するイテレーターを返します。 ast.iter_child_nodes() ノードの直接の⼦ノードに対するイテレーターを 返します。 ast.get_docstring() FunctionDef, ClassDef, Moduleノードに対して 呼ぶことができ、docstringの⽂字列を返します。
  34. ⓒ 2022 atama plus Inc. (余談)get_docstringを使ったOSSの例 • Darglint • docstringの説明と実際の関数の実装が合っているかどうかをチェックする

    linter。 34 ┌──────────────────────────────┬──────────────────┬────────────────┬──────────────────┐ │ Docstring │ short │ long │ full │ ├──────────────────────────────┼──────────────────┼────────────────┼──────────────────┤ │ """Doubles the argument.""" │ None │ None │ Missing argument │ │ │ │ │ Missing return │ │ │ │ │ │ │ │ │ │ │ ├──────────────────────────────┼──────────────────┼────────────────┼──────────────────┤ │ """Doubles the argument. │ Missing argument │ None │ Missing argument │ │ │ Missing return │ │ Missing return │ │ Not very pythonic. │ │ │ │ │ │ │ │ │ │ """ │ │ │ │ │ │ │ │ │ ├──────────────────────────────┼──────────────────┼────────────────┼──────────────────┤ │ """Doubles the argument. │ Missing return │ Missing return │ Missing return │ │ │ │ │ │ │ Args: │ │ │ │ │ x: The number to double. │ │ │ │ │ │ │ │ │ │ """ │ │ │ │ └──────────────────────────────┴──────────────────┴────────────────┴──────────────────┘ def double(x): # <docstring> return x * 2 https://github.com/terre ncepreilly/darglint
  35. ⓒ 2022 atama plus Inc. ASTに変更を加える • ASTに変更を加える際は、ast.NodeTransformerクラスを⽤います。 • 前に説明したNodeVisitorと同じように、NodeTransformerをサブクラス化し

    た上で、visit_XXX関数をoverrideして使⽤します。 • この関数はノードを返す必要があり、何を返すかによって、以下のように振る 舞います。 • オリジナルのノードを返す→何もしない • 変更を加えたノードを返す→ASTに変更を加えることができる • Noneを返す→そのノードがAST上から削除される 35
  36. ⓒ 2022 atama plus Inc. NodeTransformerの使⽤例 36 a = 1

    b = a + 2 print(b) ソースコード AST 実⾏結果 【ユースケース】 ソースコード中に現れる +(プラス)の演算をすべて -(マイナス)に置き換える
  37. ⓒ 2022 atama plus Inc. カスタムTransformerの定義と変換 37 カスタムTransformerの定義 カスタムTransformerを使ったASTの変換 NodeTransformerを継承

    visit_{変換対象node名}をoverride Add()ノードの代わりにSub()ノードを返す • ⼿動で加えたノードはlineno, col_offsetが⼊っておらず、そのままだと エラーになります。 • よって、何らか埋める必要があり、それを補助するのが fix_missing_locations関数です(基本的には親ノードと同じ値で埋める)。
  38. ⓒ 2022 atama plus Inc. 変更を加えたASTの実⾏結果 38 a = 1

    b = a + 2 print(b) a = 1 b = a - 2 print(b) 正しく変換されたことが確認できました🎉 ast.unparseを⽤いると、コードに逆変換することもできます *) ただし、コメントや空⽩、空⾏などの情報はASTに変換した 段階でドロップしてしまうので、完全な逆変換ではありません。
  39. ⓒ 2022 atama plus Inc. (余談)libCST • CSTとASTの両⽅の情報に加え、空⽩ やコメントなどの情報も付与した構⽂ ⽊を構築するためのライブラリ。

    • Instagramの開発チームによって開発 された。 • これを使えば、可逆変化が担保され、 かつASTに変更を加えることができる (凄い、、!)。 39 https://github.com/Instagram/LibCST
  40. ⓒ 2022 atama plus Inc. (余談)libCST 40 https://libcst.readthedocs.io/en/latest/why_libcst.html より抜粋して要約 元のコード

    AST CST libCST 意味的理解はしやすいがdetailが抜け落ちる detailは保持されるが、意味的理解はしづらい ASTのレベルで の抽象性を持ち つつ、CSTのレ ベルでのdetail も保持する
  41. ⓒ 2022 atama plus Inc. アジェンダ 1. ⾃⼰紹介 2. 本セッションの概要説明

    3. Pythonコードはどのようにして実⾏されるか 4. ASTの構造 5. astモジュールについて 6. astモジュールの活⽤事例 7. まとめ 41 (1) ⾃作linterを書いてみる (2) ⾃作testingフレームワークを書いてみる
  42. ⓒ 2022 atama plus Inc. ⾃作linterを書いてみる • 今回は例として、importを多重に定義してしまった場合に、警告を出すような linterを書いてみようと思います。 42

    import io as osio import os, csv from random import random as rand import pandas as pd from numpy.random import rand print(rand()) • randが2重に定義されてしまっている。 • このような場合、最後に定義された ものが有効となり、実⾏時に特にエ ラーは出ない。 実⾏時エラーにならないためバグを⽣み やすく、linterでチェックできるように したい!
  43. それ、ASTで解決しましょう!

  44. ⓒ 2022 atama plus Inc. ⾃作linterのコード 44 class CustomVisitor(ast.NodeVisitor): def

    __init__(self): self.imported_module_names = set() # import済みのmodule名を格納する # import xxx 形式でのモジュールインポートをサポート def visit_Import(self, node): self._detect_duplicated_import(node) self.generic_visit(node) # from xxx import yyy 形式でのモジュールインポートをサポート def visit_ImportFrom(self, node): self._detect_duplicated_import(node) self.generic_visit(node) def _detect_duplicated_import(self, node): for alias in node.names: # as句で別名をつけた場合は、その名前を格納し、そうでない場合はmodule名をそのまま⽤いる name = getattr(alias, "asname", None) if name is None: name = alias.name # 既にimport済みのmodule名と同名のmoduleをimportしようとした場合、警告を出す if name in self.imported_module_names: print(f"Duplicated import detected. name:{name}, module:{node.module} @ L{node.lineno}") self.imported_module_names.add(name) tree = ast.parse(code) visitor = CustomVisitor() visitor.visit(tree)
  45. ⓒ 2022 atama plus Inc. 実⾏結果 45 ~/ ❯ python

    sample_linter.py Duplicated import detected. name:rand, module:numpy.random @ L7 code = """ import io as osio import os, csv from random import random as rand import pandas as pd from numpy.random import rand print(rand()) """ 7⾏⽬でrandが再定義されていることを 検知できている 検知対象コード 検知結果(実⾏結果)
  46. ⓒ 2022 atama plus Inc. コード説明 46 class CustomVisitor(ast.NodeVisitor): def

    __init__(self): self.imported_module_names = set() # import済みのmodule名を格納する # import xxx 形式でのモジュールインポートをサポート def visit_Import(self, node): self._detect_duplicated_import(node) self.generic_visit(node) # from xxx import yyy 形式でのモジュールインポートをサポート def visit_ImportFrom(self, node): self._detect_duplicated_import(node) self.generic_visit(node) def _detect_duplicated_import(self, node): for alias in node.names: # as句で別名をつけた場合は、その名前を格納し、そうでない場合はmodule名をそのまま⽤いる name = getattr(alias, "asname", None) if name is None: name = alias.name # 既にimport済みのmodule名と同名のmoduleをimportしようとした場合、警告を出す if name in self.imported_module_names: print(f"Duplicated import detected. name:{name}, module:{node.module} @ L{node.lineno}") self.imported_module_names.add(name) 実装⽅針: ASTを辿る際に、 import済みのモジュール名を記 憶しておくことで、重複を検知 ①:NodeVisitorを継承したクラ スを作成 ②:import xxxと、from xxx import yyyは、別のノードタイ プとなるため、両⽅に対する visti関数の定義が必要 ③:as句で別名をつけた場合は、 asnameというフィードが追加 されるので、そちらを参照する 必要がある ① ② ② ③
  47. ⓒ 2022 atama plus Inc. アジェンダ 1. ⾃⼰紹介 2. 本セッションの概要説明

    3. Pythonコードはどのようにして実⾏されるか 4. ASTの構造 5. astモジュールについて 6. astモジュールの活⽤事例 7. まとめ 47 (1) ⾃作linterを書いてみる (2) ⾃作testingフレームワークを書いてみる
  48. ⓒ 2022 atama plus Inc. ⾃作testingフレームワークを書いてみる • 今回は、pytestのように、assert⽂での評価が失敗した場合に、通常よりも多 くの情報を出すようなフレークワークを書いてみたいと思います。 48

    assert [1, 4, 3] == [1, 2, 3] Traceback (most recent call last): File "/.../assert_sample.py", line 4, in <module> assert [1, 4, 3] == [1, 2, 3] AssertionError assert [1, 2] == [1, 2, 3] Traceback (most recent call last): File "/.../assert_sample.py", line 4, in <module> assert [1, 2] == [1, 2, 3] AssertionError 2番⽬のインデックスの値だけが異なる そもそも配列の⻑さが異なる いずれのケースも同じエラーが返ってきてしまう。 →もう少し細かい原因(差分の詳細)を出したい!
  49. それ、ASTで解決しましょう!

  50. ⓒ 2022 atama plus Inc. ⾃作testingフレームワークのコード 50 class AssertCmpTransformer(ast.NodeTransformer): def

    visit_Assert(self, node): if isinstance(node.test, ast.Compare) and len(node.test.ops) == 1 and isinstance(node.test.ops[0], ast.Eq): call = ast.Call( func=ast.Name( id='customized_assert', ctx=ast.Load()), args=[node.test.left, node.test.comparators[0], ast.Constant(value=node.lineno)], keywords=[] ) expr = ast.Expr(value=call) return expr return node def customized_assert(left, right, lineno): assertion_msg = None if isinstance(left, list) and isinstance(right, list): if len(left) != len(right): assertion_msg = f"length of the list is different. left:{len(left)} right:{len(right)}" else: if left != right: indexs_with_diff = [] for i, (l, r) in enumerate(zip(left, right)): if l != r: indexs_with_diff.append(i) assertion_msg = f"left:{left}, right:{right}, index {indexs_with_diff} is different" else: if left != right: assertion_msg = f"left: {left}, right: {right}" if assertion_msg is not None: print(f"Assertion Error: {assertion_msg} @ L{lineno}")
  51. ⓒ 2022 atama plus Inc. 実⾏結果 51 検知対象コード 検知結果(実⾏結果) ~/work/pycon

    ❯ python sample_testframework.py Assertion Error: left: 1, right: 2 @ L3 Assertion Error: length of the list is different. left:1 right:2 @ L4 Assertion Error: left:[1, 4, 3], right:[1, 2, 3], index [1] is different @ L5 code = """ assert 1 == 1 assert 1 == 2 assert [1] == [1, 2] assert [1, 4, 5] == [1, 2, 3] “”” 配列については、より詳細な差分情報を表⽰できている
  52. ⓒ 2022 atama plus Inc. コード説明 52 class AssertCmpTransformer(ast.NodeTransformer): def

    visit_Assert(self, node): if isinstance(node.test, ast.Compare) and len(node.test.ops) == 1 and isinstance(node.test.ops[0], ast.Eq): call = ast.Call( func=ast.Name( id='customized_assert', ctx=ast.Load()), args=[node.test.left, node.test.comparators[0], ast.Constant(value=node.lineno)], keywords=[] ) expr = ast.Expr(value=call) return expr return node def customized_assert(left, right, lineno): assertion_msg = None if isinstance(left, list) and isinstance(right, list): if len(left) != len(right): assertion_msg = f"length of the list is different. left:{len(left)} right:{len(right)}" else: if left != right: indexs_with_diff = [] for i, (l, r) in enumerate(zip(left, right)): if l != r: indexs_with_diff.append(i) assertion_msg = f"left:{left}, right:{right}, index {indexs_with_diff} is different" else: if left != right: assertion_msg = f"left: {left}, right: {right}" if assertion_msg is not None: print(f"Assertion Error: {assertion_msg} @ L{lineno}") 実装⽅針: assert関数を、より 詳細な情報を出⼒できる別の関数 に差し替える。 ①:NodeTransformerを継承した クラスを作成。 ②:差し替え対象が、2値間の同 値⽐較であることをチェックする (今回はそれ以外はサポートせず)。 ③:assert差し替え先のノードを 作成し、これを返す。 ④:型をチェックしてlist型なら、 詳細な情報を表⽰する。 ① ② ③ ④
  53. ⓒ 2022 atama plus Inc. (余談)pytestにおけるASTの書き換え • pytestでも、ASTの書き換えを ⾏うことで、詳細情報を出⼒で きるようにしているようです。

    53 https://github.com/pytest-dev/pytest/blob/main/src/_pytest/assertion/rewrite.py#L585
  54. ⓒ 2022 atama plus Inc. (余談)pytestにおけるASTの書き換え • pytest-ast-back-to-pythonと呼ばれるライブラリを使うと、pytestがどのよう な変更を加えているかを⾒ることができるようです。 •

    pytest内で変換した後のastをunparseしてそう? 54 https://github.com/tomviner/pytest-ast-back-to-python
  55. ⓒ 2022 atama plus Inc. アジェンダ 1. ⾃⼰紹介 2. 本セッションの概要説明

    3. Pythonコードはどのようにして実⾏されるか 4. ASTの構造 5. astモジュールについて 6. astモジュールの活⽤事例 7. まとめ 55
  56. ⓒ 2022 atama plus Inc. まとめ • Pythonのソースコードは、途中、CST、ASTに変換され、最終的にはバイトコードと なって、実⾏されます。 •

    その中でもASTは、抽象度の⾼い⽂脈に変換された構⽂⽊であり、Moduleノードをルー トとして、さまざまなタイプのノードが再帰的に接続された構造を持っています。 • ASTを検査する⽅法としてNodeVisitorが、ASTに変更を加える⽅法として NodeTransformerが⽤意されています。これらを継承し、振る舞いを変更したいノード タイプのvisitメソッドをoverrideすることで特定のノードに簡単にアクセスできます。 • NodeVisitorやNodeTransformerを使うことで、linterやtest frameworkを簡単に書くこ とができるようになります。また、実際に世の中に存在するpytestなどのライブラリも、 この仕組みを利⽤していることが知られています。 56
  57. ⓒ 2022 atama plus Inc. 参考にした情報 • Pythonの動作全般について • CPYTHON

    INTERNALS (Amazon URL) • ASTに興味を持ったきっかけの本。最初の⽅の説明は、ほぼこの本の受け売りです。 • CPythonの内部動作について、詳しく、かつ分かりやすく書かれているので是⾮。 • Static Analysis at Scale: An Instagram Story (URL) • LibCSTの開発の経緯をまとめた記事。CST/ASTの違いが分かりやすく書かれています。 • ASTについて • 公式ドキュメント (URL) • Green Tree Snakes - the missing Python AST docs (URL) • ASTを詳しく解説しており、活⽤例もいくつか掲載されている。ASTを使ったtesting frameworkの作り⽅はこのサイトの例を参考にしました。 • Learn Python ASTs by building your own linter (URL) • こちらも同じく、ASTの解説記事。linterの作り⽅の参考にさせて頂きました。 57
  58. ご清聴ありがとうございました!

  59. Q&A

  60. Wow students. ⽣徒が熱狂する学びを。 勉強をワクワクするもの、⾃分からやりたいものに変え、 ⽣徒⼀⼈ひとりの可能性を広げる。 私たちのあらゆる⾏動は、ただ、そのためにあります。

  61. もっと知りたい⽅はこちら @atamaplus_dev プロダクト開発に関する情報発信を⾒る atama plusのニュースレターに登録