Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

JVM上で動くPython処理系実装のススメ

4,053 views

Published on

JJUG CCC 2017 Fallの登壇資料。

Published in: Engineering
  • Be the first to comment

JVM上で動くPython処理系実装のススメ

  1. 1. JVM上で動く Python処理系実装のススメ yotchang4s JJUG CCC 2017 Fall 2017/11/18
  2. 2. お前誰よ ❖ yotchang4s (よっちゃん) ➢ 澁谷 典明 (Yoshiaki Shibutani) https://twitter.com/yotcang4s ❖ Python歴 ➢ 6ヵ月くらいの初心者 ❖ 所属 ➢ 株式会社エフ・コード ギョームでScala書いてますフゥー
  3. 3. 本日のゴール Pythonの基礎知識を説明(かなり偏っている) 言語処理系実装の動機とモチベーションについて Python処理系をどのように作り上げているのかを追体験 今後の展望
  4. 4. JVM上で動くPython処理系? cafebabepy https://github.com/yotchang4s/cafebabepy
  5. 5. cafebabepyとは? JVM上で動くPython 3の処理系。 名前の由来は皆さんご存じの通り、Javaのクラスファイルのマジッ クナンバーであるcafebabeから来ている。 開発開始からちょうど6ヵ月ほど。 yotchang4sが1人で作っている。 ※まだまだ実装途中であり、Pythonが完全に  動くわけではない。
  6. 6. 開発の動機 1. PyCon JP 2017にCfPを出そう! 2. Jythonの近況を調査してそれを発表しよう! 3. えっ2015年から更新が止まっている。。。 4. Python 3にも対応していない。。。 5. ならば…
  7. 7. 自分で作ればいい!肝心のCfPは落ちました
  8. 8. 開発のモチベーション Python初心者だからPythonわかんないです>< Python処理系作ったらPythonがわかるはず! 楽しい\(^o^)/
  9. 9. Pythonの特徴 ブロックはインデントで表現する。 誰が書いても同じようなコードになる。 ※異論は認める The Zen of Pythonの思想。REPLで以下を実行すると見ることが出来る。 $ python >>> import this The Zen of Python, by Tim Peters Beautiful is better than ugly. Explicit is better than implicit. Simple is better than complex. …
  10. 10. Pythonの基礎知識(1/6) ● ブロックはインデントで表現 ● クラス定義は「class」 ● メソッド定義は「def」 ● コンストラクタは「__init__」
  11. 11. Pythonの基礎知識(2/6) ● クラスの継承は >>> class Dog(Animal): と書く ● 多重継承が可能 メソッド解決順序(MRO)はC3アルゴリズ ムに従うためダイヤモンド継承も可能
  12. 12. Pythonの基礎知識(3/6) ● メソッドの第一引数には自分自身のオブ ジェクトが渡ってくる ● 慣習的に「self」という名前にする。 ● Javaのthisキーワードは存在せず、明 示的に扱う ようするにPythonのメソッドはオブジェク トを束縛した関数なので >>> Dog.say(animal) とも書ける
  13. 13. Pythonの基礎知識(4/6) ● 変数に型が無い動的型付けである (型アノテーションとtypingモジュールで 型を付けられるが割愛) ● 型や引数の数の違いによるメソッドの オーバーロードはできない
  14. 14. Pythonの基礎知識(5/6) ● 全てはオブジェクト ● 関数もメソッドもオブジェクト >>> say = Dog.say >>> say(animal) も可能
  15. 15. Pythonの基礎知識(6/6) ● __xxx__は特殊メソッドでありランタイム やコード上で特別扱いされる。 ● Callableという概念があり__call__があ るオブジェクトは呼び出し可能。 ● >>> animal = Dog("ポチ") はDogの__call__を呼んでいる。 Dogの__call__ではオブジェクトの生成 や__init__の呼び出しが行われる。 ● 意味的にはオブジェクトの生成も関数呼 び出しもメソッド呼び出しもすべて統一し て扱える。
  16. 16. Pythonの特徴まとめ ● ブロックはインデントで表す ● 動的型付け ● シンプルな言語仕様 ● 特殊メソッドによる統一的な操作
  17. 17. Pythonが動くざっくりとした仕組み 1. ソースコードを字句解析して字句のストリームを作る 2. 字句のストリームから構文解析をして抽象構文木(AST)を作る 3. 抽象構文木をevalする(単純なインタプリタ) (CPythonだとバイトコードを生成してVMで動かす)
  18. 18. cafebabepyの歴史 ここからTwitterベースでcafebabepyの歴史を追体験してみます。 どのように考えてどのように実装をしていったのかを解説をいれながら説明していきま す。キモの部分は詳細に説明していきますが、細かいところは https://github.com/yotchang4s/cafebabepy を参照。
  19. 19. cafebabepy開発の歴史(創世記) 2017年5月1日 全てはここから始まった。
  20. 20. cafebabepy開発の歴史(創世記) 2017年5月8日 Jythonの状況を知ってしまう。
  21. 21. cafebabepy開発の歴史(創世記) 2017年5月10日 やっていきを表明。
  22. 22. cafebabepy開発の歴史(創世記) 2017年5月12日 ANTLRを使い具象構文木が 作れるようになった。
  23. 23. 具象構文木を作るには?(1/4) cafebabepyはANTLR4を使って具象構文木を作っている。 ANTLR(http://www.antlr.org/)とはLL(*)構文解析を行うパーサジェネレータ。 ANTLRのGrammarファイルからJavaのパーサを作成する。 ANTLRのGrammarファイルはEBNFによく似ている。 PythonのドキュメントにEBNFに近い物があるのでほぼそのまま移植。 https://docs.python.jp/3/reference/grammar.html
  24. 24. 具象構文木を作るには?(2/4) Pythonの文法はLL(1)を保つように定められている。(PEP 3099参照) LL(1)とは再帰下向き構文解析で、1個のトークンを先読みすれば 生成規則を決定することができる文法。カンタン!
  25. 25. 具象構文木を作るには?(3/4) cafebabepyのGrammarの一部分 file_input : ( NEWLINE | stmt )* EOF; stmt: simple_stmt | compound_stmt; simple_stmt : small_stmt (SEMI_COLON small_stmt)* SEMI_COLON? NEWLINE; ... スタート 改行とstmt(文)が0個以上ありEOFがある 文とは simple_stmtかcompoud_stmt のどちらか1つ simple_stmtとは small_stmtの次に「;」とsmall_stmtが0個以上あり、「;」が 0個または1個あり、改行がある つまり「;」で区切ると1行に複数文を書ける
  26. 26. 具象構文木を作るには?(4/4) 出来上がった具象構文木 コード # test test def test(a): return 1
  27. 27. cafebabepy開発の歴史(創世記) 2017年5月13日 清水川さん(@shimizukawa)等々から 色々教えて貰うようになった。 MRO(メソッド解決順序)を知る。 mroをmiro...
  28. 28. cafebabepy開発の歴史(創世記) 2017年5月16日 段々全貌が見え始めて辛く楽しくなっ てきたところ。 ランタイムをJavaで作り始めた。 Pythonのbuiltinsモジュールと_astモ ジュールの作成を開始。 AST(抽象構文木)の作成はできてい ない。
  29. 29. cafebabepyのランタイム(1/3) PythonのランタイムはJavaで書いている。 Pythonのランタイム上にPythonの世界が構築されている。 Javaで書かれたPythonの疑似コードを読み込んでPythonの世界で動かせるようにして いる。Javaの世界とPythonの世界を繋げている。
  30. 30. cafebabepyのランタイム(2/3) builtinsモジュールのobjectクラ スを定義している。 また、 __new__ __init__ __getattribute__ などの特殊メソッドを定義して いる。
  31. 31. cafebabepyのランタイム(3/3) @DefinePyType/@DefinePyModule/@DefinePyFunction アノテーションによりPythonとJavaを結びつけている。 ランタイムのコアではJavaのリフレクションを使って @DefinePyType/@DefinePyModuleアノテーションが付いているクラスを探し出して Pythonの型のオブジェクト(後述)を作成し、ランタイムに登録する。
  32. 32. cafebabepy開発の歴史(創世記) 2017年5月17日 Javaでいうsuperキーワードは存 在せず代わりにsuperという名前 のクラスが存在することを知る。 Pythonにはちょくちょくこのような ものが存在する。
  33. 33. cafebabepy開発の歴史(創世記) 2017年6月1日 Pythonistaに罠をしかけられる。 Twitterとかでつぶやいていると色ん な人からアドバイスをもらえるぞ! ※PyPyとはPythonで作られた   Python処理系
  34. 34. cafebabepy開発の歴史(創世記) 2017年6月3日 関数もオブジェクトであり変数を生やす ことができることが判明。とても動的。 def hoge(): pass hoge.huga = 1 ←変数を生やせる!
  35. 35. cafebabepy開発の歴史(創世記) 2017年6月13日 Pythonのクラスの多重継承を実 現するためにC3アルゴリズムにつ いて実装を始める。
  36. 36. C3アルゴリズムとは? Python2.3から導入されたMRO(Method Resolution Order)を決めるアルゴリズム。多 重継承をよしなにしてくれる。矛盾していたらエラー。 詳細は以下を参照。 https://www.python.org/download/releases/2.3/mro/
  37. 37. cafebabepy開発の歴史(創世記) 2017年6月16日 Pythonのselfとの戦い。 JavaにはthisがあるがPythonで はメソッドで明示的にselfを受け取 るためPythonメソッドの呼び出し 方について迷走。
  38. 38. cafebabepy開発の歴史(創世記) 2017年6月17日 Javaで書かれたPython擬似コード 上で1+1をすると3になる。 1+2は5になる。 2つ目を2回足していたのが原因。
  39. 39. cafebabepy開発の歴史(創世記) 2017年6月19日 PyObjectインターフェースと型のオ ブジェクト、オブジェクトの設計がほ ぼ完了。 ここまでの1ヵ月ほぼPythonの言語 仕様の調査に時間をかけていた。
  40. 40. PyObjectとJavaの結びつき PyObject インタフェース 型のクラス/ モジュール 型のオブジェクト (Pythonのクラス) Pythonの世界 オブジェクト Javaの世界 モジュールの オブジェクト (Pythonのモジュール) Pythonは全てがオブジェクト。 Javaのクラス定義ではPythonの型のオブジェク トとして扱いづらいのでJavaのオブジェクトを Pythonのクラスとして扱っている。 全てのPythonのオブジェクトはJava側では PyObjectとして統一的に扱っている。
  41. 41. cafebabepy開発の歴史(出エジプト記) 2017年6月19日 AST(抽象構文木)の作成、構築とイ ンタプリタの作成を開始。 今まではJavaの疑似Pythonコードし か実行できなかったがPythonのコー ドを実行できるように実装を開始。
  42. 42. 抽象構文木を作るには?(1/2) 具象構文木をVisitorパターンで巡って抽象構文木(AST)を作成する。 ANTLR側で構文ルールに従ったVisitorが作成されるので visitXXXメソッドをオーバーライドする。 ASTはPythonの世界で構築している。ランタイムで各ASTクラスのオブジェクト (PyObject)を作って後続に渡している。 PythonのASTはドキュメント化されている。 32.2.2. 抽象文法 (Abstract Grammar) https://docs.python.jp/3/library/ast.html#abstract-grammar
  43. 43. 抽象構文木を作るには?(2/2) visitFile_inputの中でvisitStmtを呼び出し ている。具象構文木のノードの最後までた どり、戻り値でASTを返す。 具象構文木をvisitしていき、ASTを構築し ていく。 Module(AST) stmt(AST) visitFile_input Module(AST) visitStmt stmt(AST) visitSimple_stmt visitCompound_stmt visitXXX visitXXX Expr(AST) ... ………
  44. 44. インタプリタの概要(1/3) 抽象構文木(AST)を実行するにあたってインタプリタを実装する。 evalを再帰的に呼び出すことで処理を実行する。 しかしこの方式は1行1行実行するので非常に遅い。ただ作るのは簡単なのでとっかか りとしてインタプリタから始めると、処理系が動いている感覚がつかみやすくモチベーショ ンを維持しやすい。
  45. 45. インタプリタの概要(2/3) contextは実行しているブロック、 nodeはASTを指す。 PyObjectは必ずスコープを持つの で、例えば変数への代入が発生し たらcontextのスコープに変数を入 れる。 evalではASTの種類によって各 evalXXXメソッドを呼び出して結果 を戻り値とする。
  46. 46. インタプリタの概要(3/3) BinOp(2項演算)のeval leftのAST、rightのASTを取 得して更にevalを呼んでい る。 evalしたleftとrightをASTの 種類によってaddしたりsub したりしている。 このようにevalを再帰的に 呼び出してASTを評価して いく。
  47. 47. cafebabepy開発の歴史(出エジプト記) 2017年6月21日 1+1が2になる。 一つのターニングポイント。 Pythonでは演算子があると特殊メ ソッドを呼ぶ仕様になっている。つま りintに__add__、__radd__などがあ ればよく、evalの実装も各特殊メソッ ドを呼ぶようにすればいい。 フィボナッチ数列の計算のための実 装を進めている。
  48. 48. cafebabepy開発の歴史(出エジプト記) 2017年6月22日 Pythonの比較はJavaと違い 1 < 2 < 3が許されることを知る。 Python処理系を実装することによっ てPythonの仕様を知るという目的通 り。
  49. 49. cafebabepy開発の歴史(出エジプト記) 2017年6月23日 1 < 2 < 3がPythonで許されることが わかったが実装するにはどうすれば いいのかを思考。 1 < 2 && 2 < 3 になるように実装した。 言語処理系の実装はこういった パズルのような側面もある。
  50. 50. cafebabepy開発の歴史(出エジプト記) 2017年6月24日 if elif else を実装。 プログラミング言語っぽくなってきた。
  51. 51. if elif else文のAST if文の条件式 条件を満たした時に実行される文 条件を満たしていない時に実行される文 if文のASTは条件式、条件を満たした時に実 行される文の集合、満たしていない時に実行 される文の集合の3つを持つ。 elifはifのネストとして扱われる。
  52. 52. cafebabepy開発の歴史(出エジプト記) 2017年6月26日 つらいときもある。
  53. 53. cafebabepy開発の歴史(出エジプト記) 2017年6月30日 変数が扱えるようになった。 evalのcontextのスコープに変数を保 存する。
  54. 54. cafebabepy開発の歴史(出エジプト記) 2017年6月30日 関数呼び出しを実装。 print関数の呼び出しが可能になっ た。print関数自体はJavaで書いてい る。 スコープから「print」に紐付いた関数 オブジェクト(PyObject)を取得して callを呼び出す。callの引数には「a + 1」をevalで評価し、その値を入れる。
  55. 55. cafebabepy開発の歴史(出エジプト記) 2017年6月30日 関数の定義を実装中。 関数の定義自体は単純で、context に関数オブジェクト(Callableな PyObject)を入れるだけ。 インタプリタなので関数オブジェクト で関数本体のASTを保持する。
  56. 56. cafebabepy開発の歴史(出エジプト記) 2017年6月30日 1つの目標であるフィボナッチ数列の 計算ができるようになった。 これでプログラミング言語の処理系と 呼べるぐらいにはなった。 が、実は項が1つズレており、 if n > 2: return fib(n - 1) + fib(n - 2) が正解。
  57. 57. cafebabepy開発の歴史(民数記) 2017年7月5日 Pythonのアンパック代入を知る。 処理系がいい感じに代入してくれる。 ここではbが先頭、dが最後、cが真ん 中となっている。 これがのちに大誤算に…
  58. 58. cafebabepy開発の歴史(民数記) 2017年7月5日 アンパック代入の闇を見る。しかし Pythonの仕様なので実装しないとい けない。
  59. 59. cafebabepy開発の歴史(民数記) 2017年7月7日 アンパック代入の実装完了。
  60. 60. cafebabepy開発の歴史(民数記) 2017年7月7日 アンパック代入の闇っぽいものも動 く。 多重にアンパックをするために再帰 を繰り返して実装している。 しかし Pythonista曰く「*」は普段使わない、 更にツイートにあるようなことは絶対 にしないしキモイとのこと… 何のために実装したのか
  61. 61. cafebabepy開発の歴史(民数記) 2017年7月8日 そんなこともありました(´・_・`)
  62. 62. cafebabepy開発の歴史(民数記) 2017年7月11日 リスト内包表記の実装を開始。 Pythonといったらリスト内包表記とい う思いもあった。 CPythonで実行すると [1, 2, 3, 4, 5, 6, 7, 8, 9] が出来上がる。
  63. 63. cafebabepy開発の歴史(民数記) 2017年7月11日 リスト内包表記の構文ルールが思い のほか複雑だった。 forを多重に書けてさらにフィルター 条件も複数かける。
  64. 64. cafebabepy開発の歴史(民数記) 2017年7月11日 リスト内包表記を実装。 多重forでも動いている。 フィルターも動いている。 ここら辺はif文等を実装した時のの応 用。まさにパズル。
  65. 65. cafebabepy開発の歴史(民数記) 2017年7月13日 リスト内包表記だけでfizzbuzzする 変態コードも動く。 fizzbuzz = [('fizzbuzz' if x % 15 == 0 else ('fizz' if x % 3 == 0 else ('buzz' if x % 5 == 0 else x))) for x in range(1, 101)] for x in fizzbuzz: print(x) はい、意味わかりませんね!
  66. 66. cafebabepy開発の歴史(民数記) 2017年7月13日 for文が動いた。 Pythonのforはelseをつけることがで きる。
  67. 67. cafebabepy開発の歴史(民数記) 2017年7月14日 Python mini Hack-a-thon夏山合宿 にて REPL(Read Eval Print Loop) の実装をした。 jlineを使って1行読み込んでパースし てASTを作ってevalしているだけ。
  68. 68. cafebabepy開発の歴史(民数記) 2017年7月16日 1行ずつ処理する単純なREPLはで きたが、複数行に渡る処理が実行で きない。 >>> if 1 < 2: . . . print(3) ここが複数行 . . . 3 >>>
  69. 69. cafebabepy開発の歴史(民数記) 2017年7月17日 REPLの第2プロンプトで迷走中。長 い道のりの始まり。
  70. 70. cafebabepy開発の歴史(民数記) 2017年7月19日 迷走している。
  71. 71. cafebabepy開発の歴史(民数記) 2017年7月20日 Lexer(字句解析器)まで戻った… ここからひたすらLexerを自力で実装 している。あまりにも泥臭く単調な作 業。 構文ルールにそうような字句の羅列 を作り出す作業。 しばらく戦いは続く。
  72. 72. 字句解析器から構文解析器へのストリーム 字句解析器 (Lexer) if 1 == 1 : <NEWLINE> <INDENT> print ( "hello" <DEDENT> <NEWLINE> 構文解析器 (Parser) if 1 == 1: print("hello") ) ポイントは論理的な字句である<INDENT>と<DEDENT>。 Pythonでは字句解析でインデントの始まりと終わりを作る。
  73. 73. cafebabepy開発の歴史(民数記) 2017年7月23日 ふとParserを分ければいけるのでは ないかと気づく。
  74. 74. 入力があった場合、パースする。成功したらそのままASTを作成してevalする。 パスに失敗したら検証パースを行う。検証パースに成功したら第二プロンプトを表示し て終了。検証パースにも失敗したら構文エラー。 検証パースではREPLを通すために条件を緩くしている。 通常Parser single_input: NEWLINE | simple_stmt | compound_stmt NEWLINE; suite: simple_stmt | NEWLINE INDENT stmt+ DEDENT; simple_stmt: small_stmt (SEMI_COLON small_stmt)* SEMI_COLON? NEWLINE; 検証Parser single_input: NEWLINE | simple_stmt | compound_stmt NEWLINE? EOF; suite: simple_stmt | NEWLINE (EOF | DEDENT+ EOF | INDENT stmt+ (DEDENT | EOF)); simple_stmt: small_stmt (SEMI_COLON small_stmt)* SEMI_COLON? (NEWLINE | EOF); 第二プロンプトに対応したREPLの仕組み(1/2)
  75. 75. 第二プロンプトに対応したREPLの仕組み(2/2) if 1 < 2: NEWLINE→通常NG 検証OK INDENT print(3) NEWLINE→通常NG 検証OK DEDENT NEWLINE→通常OK
  76. 76. cafebabepy開発の歴史(民数記) 2017年7月24日 第二プロンプト込みのREPLを実装し た。 プログラミング言語処理系としての大 きな一歩。 確認もしやすい。
  77. 77. ターミナルで動かしてみる
  78. 78. cafebabepy開発の歴史(その後) REPLの実装が処理系としての大きなターニングポイントで、その 後はクラスを実装したり色々ありますが、1つ1つ実装していくだけ です。 以上が最低限の言語処理系を作るまでです。
  79. 79. 動かしてみるかも?(デモ) if 1 == 1: print("hello")
  80. 80. 動かした結果
  81. 81. 今後の展望 ● Python 3の文法を全て実装 ● 速度改善 ○ invokedynamic命令を使った高速化 ○ リフレクションを極力少なく ● PythonのコードからJavaのコードを実行 ● C拡張の実行 ○ NumPyとかSciPyがJavaから呼べるとアツい。 ○ JRubyで出来ていそうなので参考にできるか?
  82. 82. 言語処理系の作成について見てきましたが、 ド素人でもここまで出来ます。 自分ならもっとうまく出来ると思ったりしたかと思います。 言語仕様から作るのも楽しいでしょうし、 既存の言語のJVM実装を作るのも楽しいでしょう。
  83. 83. さぁ言語処理系を作ってみよう!
  84. 84. ご静聴ありがとうございました。

×