[解決済み] Pythonの構文に新しいステートメントを追加することはできますか?
質問
新しいステートメントを追加することはできますか?
print
,
raise
,
with
) をPythonの構文に置き換えるか?
例えば、許可するために...
mystatement "Something"
または
new_if True:
print "example"
もしあなたが べきである というより、それが可能かどうか(python インタープリタのコードを変更することなく)。
どのように解決するのですか?
次のページが参考になります。 Python内部:Pythonに新しいステートメントを追加する , こちらを引用しています。
この記事は、Pythonのフロントエンドがどのように動作するかをよりよく理解するための試みです。ドキュメントやソースコードを読むだけでは少し退屈かもしれませんので、ここでは実践的なアプローチを取っています。この記事では
until
をPythonに追加してみます。
この記事のすべてのコーディングは、最先端のPy3kブランチに対して行われました。 Python Mercurial リポジトリのミラー .
は
until
ステートメント
Rubyのように、いくつかの言語では
until
ステートメントがあり、これは
while
(
until num == 0
は
while num != 0
). Rubyでは、書ける。
num = 3
until num == 0 do
puts num
num -= 1
end
と印刷されます。
3
2
1
そこで、Pythonにも同じような機能を追加したいと思います。つまり、書けるようになることです。
num = 3
until num == 0:
print(num)
num -= 1
言語擁護のための余談
この記事は、言語擁護のために
until
文の追加を推奨するものではありません。そのような文はいくつかのコードをより明確にすると思いますし、この記事はそれがいかに簡単に追加できるかを示していますが、私はPythonの最小主義の哲学を完全に尊重しています。私がここでやろうとしていることは、本当に、Pythonの内部動作についていくらかの洞察を得ることです。
文法を変更する
Python はカスタムパーサージェネレータである
pgen
. これは Python のソースコードを解析木に変換する LL(1) パーサーです。パーサジェネレータへの入力は、ファイル
Grammar/Grammar
[1]
. これは、Pythonの文法を指定する簡単なテキストファイルです。
[1] : これ以降、Pythonソース中のファイルへの参照は、ソースツリーのルート(Pythonをビルドするためにconfigureやmakeを実行したディレクトリ)を相対的に指定することになります。
文法ファイルには、2つの修正が必要です。1つ目は
until
文の定義を追加することです。私は、どこに
while
ステートメントが定義されていた場所 (
while_stmt
) を追加し、さらに
until_stmt
の下に
[2]
:
compound_stmt: if_stmt | while_stmt | until_stmt | for_stmt | try_stmt | with_stmt | funcdef | classdef | decorated
if_stmt: 'if' test ':' suite ('elif' test ':' suite)* ['else' ':' suite]
while_stmt: 'while' test ':' suite ['else' ':' suite]
until_stmt: 'until' test ':' suite
[2]
: これは、私がよく知らないソースコードを修正するときに使用する一般的なテクニックを示しています。
類似性による作業
. この原則はすべての問題を解決するわけではありませんが、間違いなくプロセスを容易にすることができます。のために行わなければならないことは、すべて
while
に対しても行わなければならないので
until
も行わなければならないが、これはかなり良いガイドラインになる。
を除外することにしたことに注意してください。
else
の定義から
until
という節がありますが、これは少し違います。
else
節が嫌いで、PythonのZenにうまく適合しないと思うからです)。
2つ目の変更点は
compound_stmt
を含むように変更することです。
until_stmt
を含めるようにしました。これは
while_stmt
の直後です。
を実行すると
make
を変更した後
Grammar/Grammar
を変更すると
pgen
を再 生成するために実行されます。
Include/graminit.h
と
Python/graminit.c
で、いくつかのファイルが再コンパイルされます。
AST生成コードの修正
Pythonパーサーが解析木を作成した後、この木はASTに変換されます。ASTは を扱うのがより簡単だからです。 であるためです。
そこで
Parser/Python.asdl
にアクセスし、ASTノードを追加して、新しい
until
文のすぐ下に、やはり
while
:
| While(expr test, stmt* body, stmt* orelse)
| Until(expr test, stmt* body)
もし今、あなたが
make
を実行すると、ファイルの束をコンパイルする前に
Parser/asdl_c.py
が実行され、AST定義ファイルからCコードを生成していることに注意してください。これは(例えば
Grammar/Grammar
のように)Pythonのソースコードがプログラミングを簡単にするためにミニ言語(言い換えればDSL)を使用するもう一つの例です。また
Parser/asdl_c.py
はPythonスクリプトであり、これは一種の
ブートストラップ
- であり、Pythonをゼロからビルドするために、Pythonはすでに利用可能でなければなりません。
一方
Parser/asdl_c.py
は新しく定義された AST ノードを管理するためのコードを生成しました (ファイル
Include/Python-ast.h
と
Python/Python-ast.c
など)であっても、関連するパースツリーのノードをそれに変換するコードを手作業で書かなければなりません。これは、ファイル
Python/ast.c
. という名前の関数があります。
ast_for_stmt
という関数がステートメントのパースツリーノードをASTノードに変換しています。ここでも、古い友人である
while
に導かれ、私たちはすぐに大きな
switch
に飛び込んで、複合文の処理のために
until_stmt
:
case while_stmt:
return ast_for_while_stmt(c, ch);
case until_stmt:
return ast_for_until_stmt(c, ch);
次に
ast_for_until_stmt
. ここにあります。
static stmt_ty
ast_for_until_stmt(struct compiling *c, const node *n)
{
/* until_stmt: 'until' test ':' suite */
REQ(n, until_stmt);
if (NCH(n) == 4) {
expr_ty expression;
asdl_seq *suite_seq;
expression = ast_for_expr(c, CHILD(n, 1));
if (!expression)
return NULL;
suite_seq = ast_for_suite(c, CHILD(n, 3));
if (!suite_seq)
return NULL;
return Until(expression, suite_seq, LINENO(n), n->n_col_offset, c->c_arena);
}
PyErr_Format(PyExc_SystemError,
"wrong number of tokens for 'until' statement: %d",
NCH(n));
return NULL;
}
これもまた、相当する
ast_for_while_stmt
をよく見ながらコーディングされています。
until
をサポートしないことにしました。
else
節をサポートしないことにしました。予想通り、ASTは再帰的に作成され、他のAST作成関数である
ast_for_expr
といった他のAST作成関数を用いて再帰的に作成されます。
ast_for_suite
の本文は
until
ステートメントの本文に使用します。最後に、新しいノードである
Until
という名前の新しいノードが返されます。
解析木ノードにアクセスすることに注意してください。
n
のようないくつかのマクロを使って
NCH
と
CHILD
. これらは理解する価値があります - それらのコードは
Include/node.h
.
余談ですが ASTの構成
のために新しいタイプのASTを作成することにしました。
until
文のために新しいタイプの AST を作成することにしましたが、実はこれは必要ではありません。私はいくつかの作業を節約し、既存の AST ノードの合成を使用して新しい機能を実装することができました。
until condition:
# do stuff
と機能的に同等である。
while not condition:
# do stuff
を作成する代わりに
Until
ノードを
ast_for_until_stmt
を作成することができました。
Not
ノードに
While
ノードを子として持つ。ASTコンパイラはこれらのノードをどのように扱うか既に知っているので、次のステップはスキップすることができます。
ASTをバイトコードにコンパイルする
次のステップは、ASTをPythonのバイトコードにコンパイルすることです。コンパイルにはCFG(Control Flow Graph)という中間結果がありますが、同じコードで処理するため、この詳細は今は無視し、別の記事で紹介します。
次に見ていくコードは
Python/compile.c
. のリードに続いて
while
を導くと、関数
compiler_visit_stmt
この関数はステートメントをバイトコードにコンパイルする役割を担っています。に対する節を追加します。
Until
:
case While_kind:
return compiler_while(c, s);
case Until_kind:
return compiler_until(c, s);
もし、あなたが
Until_kind
は何かというと、これは定数です(実際には
_stmt_kind
の列挙の値) で、AST 定義ファイルから自動的に
Include/Python-ast.h
. とにかく、私たちは
compiler_until
を呼び出します。もちろん、これはまだ存在しません。これはもちろんまだ存在しません。
もしあなたが私のように好奇心が旺盛なら、次のことに気づくでしょう。
compiler_visit_stmt
が特殊であることに気づくでしょう。いくら
grep
-をいくら探しても、それが呼び出される場所はわかりません。この場合、残る選択肢はただ一つ、Cのマクロフーです。実際、少し調べると
VISIT
で定義されたマクロに行き着きます。
Python/compile.c
:
#define VISIT(C, TYPE, V) {\
if (!compiler_visit_ ## TYPE((C), (V))) \
return 0; \
を呼び出すために使われます。
compiler_visit_stmt
で
compiler_body
. しかし、我々の仕事に戻ると....
約束通り、ここで
compiler_until
:
static int
compiler_until(struct compiler *c, stmt_ty s)
{
basicblock *loop, *end, *anchor = NULL;
int constant = expr_constant(s->v.Until.test);
if (constant == 1) {
return 1;
}
loop = compiler_new_block(c);
end = compiler_new_block(c);
if (constant == -1) {
anchor = compiler_new_block(c);
if (anchor == NULL)
return 0;
}
if (loop == NULL || end == NULL)
return 0;
ADDOP_JREL(c, SETUP_LOOP, end);
compiler_use_next_block(c, loop);
if (!compiler_push_fblock(c, LOOP, loop))
return 0;
if (constant == -1) {
VISIT(c, expr, s->v.Until.test);
ADDOP_JABS(c, POP_JUMP_IF_TRUE, anchor);
}
VISIT_SEQ(c, stmt, s->v.Until.body);
ADDOP_JABS(c, JUMP_ABSOLUTE, loop);
if (constant == -1) {
compiler_use_next_block(c, anchor);
ADDOP(c, POP_BLOCK);
}
compiler_pop_fblock(c, LOOP, loop);
compiler_use_next_block(c, end);
return 1;
}
白状すると、このコードはPythonバイトコードの深い理解に基づいて書かれたものではありません。記事の他の部分と同じように、それはキンの模倣で行われました。
compiler_while
関数を真似たものです。しかし、注意深く読むことで、PythonのVMはスタックベースであることを念頭に置き、また、dis
モジュールのドキュメントをちらっと見るだけで、そのモジュールには
Python バイトコードのリスト
と記述することで、何が起こっているのかを理解することができます。
これで終わり...?そうでしょう?
すべての変更と
make
を実行した後、新しくコンパイルされた Python を実行し、新しい
until
ステートメントを試すことができます。
>>> until num == 0:
... print(num)
... num -= 1
...
3
2
1
ほら、うまくいきましたね。新しいステートメントに対応するバイトコードを
dis
モジュールを使って新しいステートメントのために作られたバイトコードを次のように見てみましょう。
import dis
def myfoo(num):
until num == 0:
print(num)
num -= 1
dis.dis(myfoo)
その結果がこちらです。
4 0 SETUP_LOOP 36 (to 39)
>> 3 LOAD_FAST 0 (num)
6 LOAD_CONST 1 (0)
9 COMPARE_OP 2 (==)
12 POP_JUMP_IF_TRUE 38
5 15 LOAD_NAME 0 (print)
18 LOAD_FAST 0 (num)
21 CALL_FUNCTION 1
24 POP_TOP
6 25 LOAD_FAST 0 (num)
28 LOAD_CONST 2 (1)
31 INPLACE_SUBTRACT
32 STORE_FAST 0 (num)
35 JUMP_ABSOLUTE 3
>> 38 POP_BLOCK
>> 39 LOAD_CONST 0 (None)
42 RETURN_VALUE
最も興味深いのは12番の処理です。条件が真であれば、ループの後にジャンプします。これは、正しいセマンティクスで
until
. もしジャンプが実行されなければ、ループ本体は操作35で条件にジャンプして戻るまで走り続けます。
いい感じに変更できたので、次にこの関数を実行してみました(実行中の
myfoo(3)
を実行)してみました。その結果は、あまり心強いものではありませんでした。
Traceback (most recent call last):
File "zy.py", line 9, in
myfoo(3)
File "zy.py", line 5, in myfoo
print(num)
SystemError: no locals when loading 'print'
おっと...これはマズいぞ。で、何が悪かったんだ?
シンボルテーブルがない件
PythonコンパイラがASTをコンパイルする際に行うステップの1つは、コンパイルするコードのシンボルテーブルを作成することです。の呼び出しは
PySymtable_Build
で
PyAST_Compile
は、シンボル・テーブル・モジュール (
Python/symtable.c
) を呼び出します。このモジュールはコード生成関数と同じような方法で AST を走査します。各スコープのシンボルテーブルを持つことで、コンパイラはどの変数がグローバルでどの変数がスコープにローカルであるかといった重要な情報を把握することができます。
この問題を解決するために、私たちは
symtable_visit_stmt
の関数を修正する必要があります。
Python/symtable.c
を処理するコードを追加し、さらに
until
ステートメントを処理するコードを追加しています。
while
ステートメント
[3]
:
case While_kind:
VISIT(st, expr, s->v.While.test);
VISIT_SEQ(st, stmt, s->v.While.body);
if (s->v.While.orelse)
VISIT_SEQ(st, stmt, s->v.While.orelse);
break;
case Until_kind:
VISIT(st, expr, s->v.Until.test);
VISIT_SEQ(st, stmt, s->v.Until.body);
break;
[3]
: ちなみに、このコードがないと、コンパイラの警告で
Python/symtable.c
. コンパイラは
Until_kind
の switch ステートメントで扱われていないことに気づきます。
symtable_visit_stmt
の switch 文で処理されず、文句を言われます。コンパイラの警告を確認することは常に重要です!
そして、これで本当に完了です。この変更後のソースをコンパイルすると
myfoo(3)
が期待通りに動作するようになります。
結論
この記事では、Pythonに新しいステートメントを追加する方法を示しました。Pythonコンパイラのコードにかなり手を加える必要があるものの、類似した既存のステートメントをガイドラインとして使用したため、この変更を実装するのは難しくありませんでした。
Python コンパイラーはソフトウェアの洗練された塊であり、私はその専門家であると主張するつもりはありません。しかし、私は Python の内部、特にそのフロントエンドに本当に興味があります。そのため、この演習はコンパイラの原理やソースコードの理論的な勉強に非常に役立つと思います。これは、コンパイラーをより深く理解するための将来の記事のベースとして役立つでしょう。
リファレンス
この記事を書くにあたり、いくつかの優れた参考文献を使用しました。以下、順不同で紹介します。
- PEP 339: CPython コンパイラの設計 - の最も重要かつ包括的な部分です。 公式 の公式ドキュメントの中で最も重要で包括的なものです。非常に短いので、Python の内部に関する良い文書が少ないことを痛感させられます。
- Python Compiler Internals" - Thomas Lee による記事です。
- "Python。Design and Implementation" - Guido van Rossum によるプレゼンテーション。
- Python (2.5) Virtual Machine, A guided tour - Peter Tröger による発表。
関連
-
[解決済み] Pythonで辞書に新しいキーを追加するにはどうすればよいですか?
-
[解決済み] 複数行の長い文字列を作成するためのPythonicな方法
-
[解決済み] Pythonスクリプトのプロファイリングはどのように行うのですか?
-
[解決済み] pandasを使った "大量データ "ワークフロー【終了しました
-
[解決済み] Pythonのsuper()は多重継承でどう動くのか?
-
[解決済み] 空のPandas DataFrameを作成し、それを埋める?
-
[解決済み] モジュール名を文字列で指定してインポートするには?
-
[解決済み】典型的なテストディレクトリ構造でunittestを実行する
-
[解決済み】Python 3.6+で辞書は順番に並びますか?
-
[解決済み] IPythonの終了確認を無効にする
最新
-
nginxです。[emerg] 0.0.0.0:80 への bind() に失敗しました (98: アドレスは既に使用中です)
-
htmlページでギリシャ文字を使うには
-
ピュアhtml+cssでの要素読み込み効果
-
純粋なhtml + cssで五輪を実現するサンプルコード
-
ナビゲーションバー・ドロップダウンメニューのHTML+CSSサンプルコード
-
タイピング効果を実現するピュアhtml+css
-
htmlの選択ボックスのプレースホルダー作成に関する質問
-
html css3 伸縮しない 画像表示効果
-
トップナビゲーションバーメニュー作成用HTML+CSS
-
html+css 実装 サイバーパンク風ボタン
おすすめ
-
[解決済み] Jupyterノートブックでenv変数を設定する方法
-
[解決済み] タプルのリストを複数のリストに変換するには?
-
[解決済み] あるオブジェクトが数であるかどうかを確認する、最もパイソン的な方法は何でしょうか?
-
[解決済み] 異なる順序で同じ要素を持つ2つのJSONオブジェクトを等しく比較するには?
-
[解決済み] Pythonによる一対のクロスプロダクト [重複] (英語)
-
[解決済み] models.pyを複数のファイルに分割する
-
[解決済み] asyncio.ensure_future vs. BaseEventLoop.create_task vs. simple coroutine?
-
[解決済み] リストスライスの割り当てはどのように行われるのですか?
-
[解決済み] Seleniumから要素の属性を取得するには?
-
[解決済み] ネストした辞書の項目からpandasのDataFrameを構築する