新しい文(などを追加することができprint
、raise
、with
Pythonの構文に)?
許可してください。
mystatement "Something"
または、
new_if True:
print "example"
必要な場合はそれほどではありませんが、可能であれば(Pythonインタープリターのコードを変更しない限り)
新しい文(などを追加することができprint
、raise
、with
Pythonの構文に)?
許可してください。
mystatement "Something"
または、
new_if True:
print "example"
必要な場合はそれほどではありませんが、可能であれば(Pythonインタープリターのコードを変更しない限り)
回答:
これが便利な場合があります-Python内部:Pythonに新しいステートメントを追加します。
この記事は、Pythonのフロントエンドがどのように機能するかをよりよく理解するための試みです。ドキュメントとソースコードを読むだけでは少し退屈な場合があるので、ここでは実践的なアプローチをとります。Pythonにuntil
ステートメントを追加します。
この記事のすべてのコーディングは、Python Mercurialレポジトリミラーの最先端のPy3kブランチに対して行われました。
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の内部の仕組みを理解することです。
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
、その後、いくつかのファイルが再コンパイルされます。
Pythonパーサーが解析ツリーを作成した後、このツリーはASTに変換されます。これは、AST がコンパイルプロセスの後続の段階で処理するのがはるかに簡単だからです。
それでは、Parser/Python.asdl
PythonのASTの構造を定義するにアクセスして、新しいuntil
ステートメントのASTノードを追加します。これも、のすぐ下にあり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がすでに利用可能である必要があります。
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_for_expr
、条件式やステートメントのast_for_suite
本文などの他のAST作成関数を使用して再帰的に作成されuntil
ます。最後に、という名前の新しいノードUntil
が返されます。
となどのn
マクロを使用して解析ツリーノードにアクセスすることに注意してください。これらは理解する価値があります。コードはにあります。NCH
CHILD
Include/node.h
until
ステートメントに新しいタイプのASTを作成することを選択しましたが、実際にはこれは必要ありません。いくつかの作業を保存し、既存の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);
何だろうと思ったらUntil_kind
、それ_stmt_kind
はAST定義ファイルからに自動的に生成される定数(実際には列挙の値)です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バイトコードの深い理解に基づいて書かれていません。記事の他の部分と同様に、それは親族compiler_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
で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
、文句を言います。コンパイラの警告を確認することは常に重要です!
そして今、私たちは本当に終わりました。この変更後にソースをコンパイルすると、myfoo(3)
期待どおりに作業が実行されます。
この記事では、Pythonに新しいステートメントを追加する方法を示しました。Pythonコンパイラーのコードにはかなり手を加える必要がありますが、同様の既存のステートメントをガイドラインとして使用したため、変更の実装は難しくありませんでした。
Pythonコンパイラーは洗練された一連のソフトウェアであり、その専門家であるとは主張していません。ただし、私はPythonの内部、特にそのフロントエンドに本当に興味があります。したがって、この演習は、コンパイラの原理とソースコードの理論的研究に非常に役立つものであることがわかりました。これは、コンパイラーに深く入り込む将来の記事のベースとして機能します。
この記事の構成には、いくつかの優れたリファレンスを使用しました。ここに、それらは順不同です:
until
is isa
/ isan
as if something isa dict:
またはif something isan int:
このようなことを行う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が見るのは前処理されたファイルです。つまり、スタックトレースなどに出力されるテキストはそれを参照します。重要な翻訳を行った場合、これは原文とは大きく異なる場合があります。上記の例では、行番号などは変更されないため、それほど大きな違いはありませんが、変更するほど、把握が難しくなります。
myimport
だけが生成さprint 1
れるため、単純に含まれているモジュールで使用しようとしています=1 ... SyntaxError: invalid syntax
b=myimport("b.py")
"と" "、および "。print 1
"。。エラーには他に何かあります(スタックトレースetc)?
import
は組み込みを使用している__import__
ため、それを上書きする場合(変更されたインポートを必要とするモジュールをインポートする前に)は不要ですmyimport
はい、ある程度は可能です。と「キーワード」を実装するために使用するモジュールがそこにあります: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
デコレータでできることもある。たとえば、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.
また、デコレータは通常、関数を実行するのではなく、変更することが期待されています。
以前は、いくつかの関数の作業ディレクトリを一時的に設定する必要があったスクリプトで、このような方法を使用しました。
10年前はそれができませんでしたが、それが変わったとは思えません。ただし、Pythonを再コンパイルする準備ができていれば、構文を変更することはそれほど難しくありませんでした。それも変更されたとは思えません。