Pythonの構文に新しいステートメント(、、など)print
をraise
追加できますか?with
言う、許可する。
mystatement "Something"
または、
new_if True:
print "example"
必要な場合はそれほど多くはありませんが、可能であれば(Pythonインタープリターコードを変更する以外)
Pythonの構文に新しいステートメント(、、など)print
をraise
追加できますか?with
言う、許可する。
mystatement "Something"
または、
new_if True:
print "example"
必要な場合はそれほど多くはありませんが、可能であれば(Pythonインタープリターコードを変更する以外)
これは役に立つかもしれません - Python internals: adding a new statement to Python、ここで引用:
この記事は、Python のフロントエンドがどのように機能するかをよりよく理解するための試みです。ドキュメントとソース コードを読むだけでは少し退屈かもしれないので、ここでは実践的なアプローチをとっています。Python にuntil
ステートメントを追加します。
この記事のすべてのコーディングは、 Python Mercurial リポジトリ ミラーの最先端の Py3k ブランチに対して行われました。
until
ステートメント_Ruby などの一部の言語には、 (は と同等) をuntil
補完するステートメントがあります。Ruby では、次のように記述できます。while
until num == 0
while num != 0
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 の内部動作についての洞察を得ることだけです。
Python は、 という名前のカスタム パーサー ジェネレーターを使用しますpgen
。これは、Python ソース コードを解析ツリーに変換する LL(1) パーサーです。パーサー ジェネレーターへの入力はファイルGrammar/Grammar
[1]です。これは、Python の文法を指定する単純なテキスト ファイルです。
[1] : これ以降、Python ソース内のファイルへの参照は、ソース ツリーのルート (Python をビルドするために configure と make を実行するディレクトリ) に対して相対的に与えられます。
文法ファイルに 2 つの変更を加える必要があります。until
1 つ目は、ステートメントの定義を追加することです。while
ステートメントが定義されている場所を見つけ( )、 [2]の下while_stmt
に追加しました。until_stmt
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] : これは、私が慣れていないソース コードを変更するときに使用する一般的な手法を示しています: work by similarity。この原則ですべての問題が解決するわけではありませんが、プロセスを確実に緩和できます。のためにしなければならないことはすべて のためにwhile
もしなければならないのでuntil
、これはかなり良いガイドラインとして役立ちます。
else
の定義から節を除外することにしたことに注意してください。これはuntil
、少し異なるものにするためです (率直に言ってelse
、ループの節が嫌いで、Zen of Python にうまく適合するとは思わないからです)。
2 番目の変更は、上記のスニペットでわかるように、 のルールcompound_stmt
を includeに変更することです。until_stmt
の直後while_stmt
です。
make
を変更した後に実行すると、プログラムが実行されて と が再生成され、いくつかのファイルが再コンパイルされるGrammar/Grammar
ことに注意してください。pgen
Include/graminit.h
Python/graminit.c
Python パーサーが解析ツリーを作成した後、このツリーは AST に変換されます。これは、コンパイル プロセスの後続の段階でAST を使用する方がはるかに簡単であるためです。
そこで、Python の 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
) は、プログラミングを簡素化するためにミニ言語 (つまり、DSL) を使用した Python ソースコードの別の例です。また、Parser/asdl_c.py
は Python スクリプトであるため、これは一種のブートストラップであることに注意してください。Python をゼロから構築するには、Python が既に利用可能である必要があります。
新しく定義した AST ノードを管理するコードをParser/asdl_c.py
(ファイルInclude/Python-ast.h
と にPython/Python-ast.c
) 生成しましたが、関連する解析ツリー ノードをそれに変換するコードを手動で記述する必要があります。これはファイルで行われますPython/ast.c
。そこでは、という名前の関数がast_for_stmt
ステートメントの解析ツリー ノードを AST ノードに変換します。繰り返しになりますが、古くからの友人while
である に導かれて、複合ステートメントを処理するための大きな部分に飛び込みswitch
、節 for を追加し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;
}
繰り返しますが、これは同等の を詳しく調べながらコーディングされましたが、 for句をサポートしないことにast_for_while_stmt
したという違いがあります。予想どおり、AST は、条件式やステートメントの本体など、他の AST 作成関数を使用して再帰的に作成されます。最後に、という名前の新しいノードが返されます。until
else
ast_for_expr
ast_for_suite
until
Until
や などn
のマクロを使用して解析ツリー ノードにアクセスすることに注意してください。これらは理解する価値があります - それらのコードは.NCH
CHILD
Include/node.h
ステートメント用に新しいタイプの AST を作成することにしましたuntil
が、実際にはこれは必要ありません。次の理由から、いくつかの作業を節約し、既存の AST ノードの構成を使用して新しい機能を実装できたはずです。
until condition:
# do stuff
機能的には次のものと同等です。
while not condition:
# do stuff
Until
でノードを作成する代わりに、ノードを子として持つノードをast_for_until_stmt
作成することもできました。AST コンパイラはこれらのノードの処理方法を既に認識しているため、プロセスの次のステップをスキップできます。Not
While
次のステップは、AST を Python バイトコードにコンパイルすることです。コンパイルには CFG (制御フロー グラフ) である中間結果がありますが、同じコードがそれを処理するため、ここではこの詳細を無視して、別の記事に譲ります。
次に見ていくコードはPython/compile.c
. の先頭に続いて、ステートメントをバイトコードにコンパイルする役割を担うwhile
関数 を見つけます。compiler_visit_stmt
次の句を追加しUntil
ます。
case While_kind:
return compiler_while(c, s);
case Until_kind:
return compiler_until(c, s);
どういうことかと思ったら、これは AST 定義ファイルから自動的に生成されUntil_kind
た定数 (実際には列挙型の値) です。とにかく、もちろん、まだ存在しないと呼びます。少し話します。_stmt_kind
Include/Python-ast.h
compiler_until
私のように好奇心旺盛な人なら、それcompiler_visit_stmt
が独特であることに気付くでしょう。grep
ソースツリーが呼び出された場所を明らかにする -ping の量はありません。この場合、残るオプションは 1 つだけです。それは、C マクロ fu です。実際、簡単な調査により、次のように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 バイトコードの深い理解に基づいて書かれたものではありません。記事の残りの部分と同様に、これは kincompiler_while
関数を模倣して行われました。ただし、Python VM がスタックベースであることを念頭に置いて注意深く読むことで、説明付きの Python バイトコードのリストをdis
含むモジュールのドキュメントをちらりと見ることで、何が起こっているのかを理解することができます。
すべての変更を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 番です。条件が true の場合、ループの後にジャンプします。これは の正しいセマンティクスです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'
おっと... これは良くありません。それで、何がうまくいかなかったのですか?
AST をコンパイルするときに Python コンパイラが実行する手順の 1 つは、コンパイルするコードのシンボル テーブルを作成することです。PySymtable_Build
inへの呼び出しは、コード生成関数と同様の方法で AST をウォークするPyAST_Compile
シンボル テーブル モジュール ( ) を呼び出します。Python/symtable.c
スコープごとにシンボル テーブルがあると、コンパイラは、どの変数がグローバルで、どの変数がスコープに対してローカルであるかなど、いくつかの重要な情報を把握するのに役立ちます。
symtable_visit_stmt
この問題を解決するには、 の関数を変更して、ステートメントの同様のコードの後にステートメントをPython/symtable.c
処理するコードを追加する必要があります[3] :until
while
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
、文句を言います。コンパイラの警告を確認することは常に重要です。
これで本当に完了です。この変更後にソースをコンパイルすると、myfoo(3)
期待どおりに作業が実行されます。
この記事では、Python に新しいステートメントを追加する方法を示しました。Python コンパイラのコードにかなりの調整が必要でしたが、この変更を実装するのは難しくありませんでした。これは、ガイドラインとして同様の既存のステートメントを使用したためです。
Python コンパイラは洗練されたソフトウェアの塊であり、私はその専門家であるとは主張していません。しかし、私は Python の内部、特にそのフロントエンドに非常に興味があります。したがって、この演習は、コンパイラの原理とソース コードの理論的研究に非常に役立つことがわかりました。これは、コンパイラーをさらに深く掘り下げる今後の記事のベースとして機能します。
この記事の作成には、いくつかの優れた参考文献を使用しました。以下は、順不同です。
このようなことを行う 1 つの方法は、ソースを前処理して変更し、追加したステートメントを Python に変換することです。このアプローチにはさまざまな問題が生じるため、一般的な使用にはお勧めしませんが、言語の実験や特定目的のメタプログラミングでは、時折役立つことがあります。
たとえば、画面に出力する代わりに特定のファイルにログを記録する「myprint」ステートメントを導入したいとします。すなわち:
myprint "This gets logged to file"
と同等です
print >>open('/tmp/logfile.txt','a'), "This gets logged to file"
正規表現の置換から AST の生成、構文が既存の python にどれだけ近いかに応じて独自のパーサーを作成するなど、置換の方法にはさまざまなオプションがあります。適切な中間アプローチは、トークナイザー モジュールを使用することです。これにより、ソースをPythonインタープリターと同様に解釈しながら、新しいキーワード、制御構造などを追加できるようになり、粗い正規表現ソリューションが引き起こす破損を回避できます。上記の「myprint」の場合、次の変換コードを記述できます。
import tokenize
LOGFILE = '/tmp/log.txt'
def translate(readline):
for type, name,_,_,_ in tokenize.generate_tokens(readline):
if type ==tokenize.NAME and name =='myprint':
yield tokenize.NAME, 'print'
yield tokenize.OP, '>>'
yield tokenize.NAME, "open"
yield tokenize.OP, "("
yield tokenize.STRING, repr(LOGFILE)
yield tokenize.OP, ","
yield tokenize.STRING, "'a'"
yield tokenize.OP, ")"
yield tokenize.OP, ","
else:
yield type,name
(これは事実上 myprint をキーワードにするので、他の場所で変数として使用すると問題が発生する可能性があります)
問題は、コードを python から使用できるようにする方法です。1 つの方法は、独自のインポート関数を作成し、それを使用してカスタム言語で記述されたコードをロードすることです。すなわち:
import new
def myimport(filename):
mod = new.module(filename)
f=open(filename)
data = tokenize.untokenize(translate(f.readline))
exec data in mod.__dict__
return mod
ただし、これには、カスタマイズされたコードを通常の python モジュールとは異なる方法で処理する必要があります。つまり、" some_mod = myimport("some_mod.py")
" ではなく " import some_mod
"
このレシピが示すように、別のかなりきちんとした (ハックではありますが) 解決策は、カスタム エンコーディング ( PEP 263を参照)を作成することです。これを次のように実装できます。
import codecs, cStringIO, encodings
from encodings import utf_8
class StreamReader(utf_8.StreamReader):
def __init__(self, *args, **kwargs):
codecs.StreamReader.__init__(self, *args, **kwargs)
data = tokenize.untokenize(translate(self.stream.readline))
self.stream = cStringIO.StringIO(data)
def search_function(s):
if s!='mylang': return None
utf8=encodings.search_function('utf8') # Assume utf8 encoding
return codecs.CodecInfo(
name='mylang',
encode = utf8.encode,
decode = utf8.decode,
incrementalencoder=utf8.incrementalencoder,
incrementaldecoder=utf8.incrementaldecoder,
streamreader=StreamReader,
streamwriter=utf8.streamwriter)
codecs.register(search_function)
このコードが実行されると (たとえば、.pythonrc または site.py に配置できます)、コメント「#coding: mylang」で始まるすべてのコードは、上記の前処理ステップによって自動的に変換されます。例えば。
# coding: mylang
myprint "this gets logged to file"
for i in range(10):
myprint "so does this : ", i, "times"
myprint ("works fine" "with arbitrary" + " syntax"
"and line continuations")
警告:
C プリプロセッサを使用したことがあれば、おそらくおなじみのように、プリプロセッサ アプローチには問題があります。主なものはデバッグです。Python が認識するのは前処理されたファイルだけです。つまり、スタック トレースなどに出力されるテキストはそれを参照します。大幅な翻訳を行った場合、これはソース テキストとは大きく異なる可能性があります。上の例は行番号などは変えていないのであまり変わらないのですが、変えれば変えるほどわかりづらくなります。
はい、ある程度可能です。「キーワード」を実装するために使用するモジュールがあります。sys.settrace()
goto
comefrom
from goto import goto, label
for i in range(1, 10):
for j in range(1, 20):
print i, j
if j == 3:
goto .end # breaking out from nested loop
label .end
print "Finished"
ソース コードの変更と再コンパイル (オープン ソースでは可能) を除けば、ベース言語の変更は実際には不可能です。
ソースを再コンパイルしたとしても、それは Python ではなく、バグを持ち込まないように細心の注意を払う必要があるハッキングされた変更されたバージョンです。
ただし、なぜそうしたいのかわかりません。Python のオブジェクト指向機能により、現状の言語で同様の結果を得ることが非常に簡単になります。
一般的な答え:ソースファイルを前処理する必要があります。
より具体的な回答:EasyExtendをインストールし、次の手順を実行します
i)新しいラングレット(拡張言語)を作成します
import EasyExtend
EasyExtend.new_langlet("mystmts", prompt = "my> ", source_ext = "mypy")
追加の仕様がない場合、EasyExtend / langlets /mystmts/の下に一連のファイルを作成する必要があります。
ii)mystmts / parsedef / Grammar.extを開き、次の行を追加します
small_stmt: (expr_stmt | print_stmt | del_stmt | pass_stmt | flow_stmt |
import_stmt | global_stmt | exec_stmt | assert_stmt | my_stmt )
my_stmt: 'mystatement' expr
これは、新しいステートメントの構文を定義するのに十分です。small_stmt非終端記号はPython文法の一部であり、新しいステートメントがフックされる場所です。パーサーは新しいステートメントを認識します。つまり、それを含むソースファイルが解析されます。ただし、有効なPythonに変換する必要があるため、コンパイラはそれを拒否します。
iii)ここで、ステートメントのセマンティクスを追加する必要があります。このためには、msytmts / langlet.pyを編集し、my_stmtノードビジターを追加する必要があります。
def call_my_stmt(expression):
"defines behaviour for my_stmt"
print "my stmt called with", expression
class LangletTransformer(Transformer):
@transform
def my_stmt(self, node):
_expr = find_node(node, symbol.expr)
return any_stmt(CST_CallFunc("call_my_stmt", [_expr]))
__publish__ = ["call_my_stmt"]
iv)langlets/mystmtsにcdして入力します
python run_mystmts.py
これでセッションが開始され、新しく定義されたステートメントを使用できます。
__________________________________________________________________________________
mystmts
On Python 2.5.1 (r251:54863, Apr 18 2007, 08:51:08) [MSC v.1310 32 bit (Intel)]
__________________________________________________________________________________
my> mystatement 40+2
my stmt called with 42
ささいな声明に到達するためのかなりの数のステップでしょ?文法を気にせずに簡単なことを定義できるAPIはまだありません。しかし、EEはいくつかのバグを除いて非常に信頼できます。したがって、プログラマーが便利なオブジェクト指向プログラミングを使用して、中置演算子や小さなステートメントなどの便利なものを定義できるAPIが登場するのは時間の問題です。ラングレットを作成してPythonに言語全体を埋め込むなど、より複雑なことについては、完全な文法アプローチを回避する方法はありません。
これは、解釈モードのみで新しいステートメントを追加するための非常に単純ですが、くだらない方法です。sys.displayhook のみを使用して遺伝子注釈を編集するための小さな 1 文字のコマンドに使用していますが、この質問に答えることができるように、構文エラーにも sys.excepthook を追加しました。後者は、readline バッファから生のコードをフェッチするという、本当に醜いものです。利点は、この方法で新しいステートメントを簡単に追加できることです。
jcomeau@intrepid:~/$ cat demo.py; ./demo.py
#!/usr/bin/python -i
'load everything needed under "package", such as package.common.normalize()'
import os, sys, readline, traceback
if __name__ == '__main__':
class t:
@staticmethod
def localfunction(*args):
print 'this is a test'
if args:
print 'ignoring %s' % repr(args)
def displayhook(whatever):
if hasattr(whatever, 'localfunction'):
return whatever.localfunction()
else:
print whatever
def excepthook(exctype, value, tb):
if exctype is SyntaxError:
index = readline.get_current_history_length()
item = readline.get_history_item(index)
command = item.split()
print 'command:', command
if len(command[0]) == 1:
try:
eval(command[0]).localfunction(*command[1:])
except:
traceback.print_exception(exctype, value, tb)
else:
traceback.print_exception(exctype, value, tb)
sys.displayhook = displayhook
sys.excepthook = excepthook
>>> t
this is a test
>>> t t
command: ['t', 't']
this is a test
ignoring ('t',)
>>> ^D
新しいステートメントを追加するためのガイドを見つけました。
https://troeger.eu/files/teaching/pythonvm08lab.pdf
基本的に、新しいステートメントを追加するには、Python/ast.c
(とりわけ)Pythonバイナリを編集して再コンパイルする必要があります。
可能ですが、しないでください。関数とクラスを介してほとんどすべてを達成できます(スクリプトを実行するためだけにPythonを再コンパイルする必要はありません)。
EasyExtendを使用してこれを行うことが可能です:
EasyExtend(EE)は、純粋なPythonで記述され、CPythonと統合されたプリプロセッサージェネレーターおよびメタプログラミングフレームワークです。EasyExtendの主な目的は、拡張言語の作成、つまりPythonにカスタム構文とセマンティクスを追加することです。
言語構文に新しいステートメントを正確に追加するわけではありませんが、マクロは強力なツールです: https://github.com/lihaoyi/macropy
そのようなことを行うことができるLogixと呼ばれる Python ベースの言語があります。しばらく開発されていませんが、ご要望の機能は最新バージョンで動作します。
デコレータでできることもあります。たとえば、Python にはwith
ステートメントがなかったとします。次に、次のような同様の動作を実装できます。
# ====== Implementation of "mywith" decorator ======
def mywith(stream):
def decorator(function):
try: function(stream)
finally: stream.close()
return decorator
# ====== Using the decorator ======
@mywith(open("test.py","r"))
def _(infile):
for l in infile.readlines():
print(">>", l.rstrip())
ただし、ここで行われているように、これはかなり汚れたソリューションです。特に、デコレータが関数を呼び出してに設定_
する動作None
は予想外です。明確にするために:このデコレータは書き込みと同等です
def _(infile): ...
_ = mywith(open(...))(_) # mywith returns None.
デコレーターは通常、関数を実行するのではなく、変更することが期待されます。
以前、いくつかの関数の作業ディレクトリを一時的に設定しなければならなかったスクリプトで、このような方法を使用しました。
インタープリターを変更することなくではありません。過去数年間、多くの言語が「拡張可能」であると説明されてきましたが、あなたが説明している方法ではありません。関数とクラスを追加して Python を拡張します。
10年前はできませんでしたが、それが変わったとは思えません。ただし、Pythonを再コンパイルする準備ができていれば、当時の構文を変更するのはそれほど難しくありませんでした。それも変更されたとは思えません。