それでも、実行時のバイトコードの変更は非常に危険で信頼性の低い操作です。 確かに、代替のPythonインタープリターではサポートされていません。
この欠点を修正するために、これをはるかに意図しており、他の多くの言語で同様の目的に使用されています(LispまたはErlangで会ったばかりです)。 このメソッドは、抽象構文ツリー(AST)プログラムの修正版です。
手始めに-ASTとは? ASTは、コンパイル中のプログラムコードの中間表現であり、パーサーの出口で取得されます。
たとえば、このコード
def func(who): print "Hello, %s!" % who func()
次のASTに変換されます。
FunctionDef( name='func', # args=arguments( # args=[Name(id='who', ctx=Param())], vararg=None, kwarg=None, defaults=[]), body=[ # Print(dest=None, values=[ BinOp(left=Str(s='Hello %s!'), op=Mod(), right=Name(id='who', ctx=Load()))], nl=True)], decorator_list=[]), # Expr(value=Call( # func=Name(id='func', ctx=Load()), # args=[], # keywords=[], # kv starargs=None, # *args kwargs=None)) # **kwargs
一見、何も明確ではありませんが、よく見ると、このツリーの要素の目的を推測できます。 AST(astモジュールの標準ライブラリで利用可能)を操作するための要素とツールの完全なドキュメントはこちらです。
竜巻に戻りましょう。 元の記事と同じ表記法を使用してみましょう。
@shortgen
という名前のデコレータとバイナリシフト演算子
<<
。
元の記事と同じコード例を使用します。
準備する
トルネードをインストールする
mkdir tornado-shortgen cd tornado-shortgen/ virtualenv .env source .env/bin/activate pip install tornado
Tornadoを書きましょう-アプリケーション
import tornado.ioloop import tornado.web import tornado.gen import os class Handler(web.RequestHandler): @asynchronous @gen.engine @shortgen def get_short(self): (result, status) << self.db.posts.find_e({'name': 'post'}) @asynchronous @gen.engine def get(self): (result, status) = yield gen.Task(self.db.posts.find_e, {'name': 'post'}) application = tornado.web.Application([ (r"/", Handler), ]) if __name__ == "__main__": application.listen(8888) tornado.ioloop.IOLoop.instance().start()
shortgen_test.pyファイルに保存します
変換の実装
モジュールのASTを取得してみましょう。
$ python >>> import ast >>> print ast.dump(ast.parse(open("shortgen_test.py").read()))
書式化されていない長いテキストのフットクロスが表示されます。そこから、
get_short
関数と
get
関数の定義にのみ関心があります。
get_short
バイナリシフトおよびデコレータを使用したソース関数
FunctionDef( name='get_short', args=arguments(args=[Name(id='self', ctx=Param())], vararg=None, kwarg=None, defaults=[]), body=[ Expr(value=BinOp( # 2- left=Tuple( # - elts=[Name(id='result', ctx=Load()), Name(id='status', ctx=Load())], ctx=Load()), op=LShift(), # right=Call( # - self.db.posts.find_e func=Attribute( value=Attribute( value=Attribute( value=Name(id='self', ctx=Load()), attr='db', ctx=Load()), attr='posts', ctx=Load()), attr='find_e', ctx=Load()), args=[Dict(keys=[Str(s='name')], values=[Str(s='post')])], # keywords=[], starargs=None, kwargs=None)))], decorator_list=[ # Attribute(value=Name(id='web', ctx=Load()), attr='asynchronous', ctx=Load()), Attribute(value=Name(id='gen', ctx=Load()), attr='engine', ctx=Load()), Name(id='shortgen', ctx=Load())]) # !
get
望ましい結果
FunctionDef( name='get', args=arguments(args=[Name(id='self', ctx=Param())], vararg=None, kwarg=None, defaults=[]), body=[ Assign( # targets=[ Tuple(elts=[ # = - tuple, ctx Store() Name(id='result', ctx=Store()), Name(id='status', ctx=Store())], ctx=Store())], value=Yield( # - yield value=Call( # gen.Task func=Attribute( value=Name(id='gen', ctx=Load()), attr='Task', ctx=Load()), args=[Attribute( # - self.db.posts.find_e value=Attribute( value=Attribute( value=Name(id='self', ctx=Load()), attr='db', ctx=Load()), attr='posts', ctx=Load()), attr='find_e', ctx=Load()), Dict(keys=[Str(s='name')], values=[Str(s='post')])], keywords=[], # starargs=None, kwargs=None)))], decorator_list=[ Name(id='asynchronous', ctx=Load()), Attribute(value=Name(id='gen', ctx=Load()), attr='engine', ctx=Load())]) # shortgen
怪しいように見えますが、なんと柔軟性があります! 実際、すべてがシンプルです。
違いを見てみましょう。
-
Expr
完全になくなった -
BinOp(left, op, right)
代わりにAssign(targets, value)
- 右オペランドの
ctx
値がLoad
からStore
変更されました - 呼び出し
self.db.posts.find_e(...)
gen.Task(self.db.posts.find_e, ...)
置き換えられました - 関数呼び出しの周りにyieldを追加
- デコレータがありません
@shortgen
したがって、最初から2番目を取得するには、
-
decorator_list
で@shortgen
decorator_list
を持つ関数を見つけます - このデコレータを削除
- 関数本体でバイナリシフト演算子
BinOp
を見つけます - 左右のオペランドを保存します。 左側で、
ctx
をStore
Load
に置き換え、右側のオペランドから関数名とその引数(positional、kw、および "star"-*、**)を抽出します。 - 関数の名前(
self.db.posts.find_e
)に最初の位置引数を追加します(つまり、この例では位置引数[self.db.posts.find_e, {'name': 'post'}]
を取得し、残りはすべて空です。 - 新しい
Call
作成しますが、これらの引数で既にgen.Task
関数を作成します - 収量で包む
-
Assign(targets, value)
を作成し、ターゲットとして以前に左のオペランドBinOp
を取り、値として-作成したばかり -
Expr
ソースツリーで、新しくExpr
Assign
置き換えます
複雑に聞こえますが、コードでは50行強かかりました。 不明な点がある場合は、そこを見てください。
これを実装する方法は? 何らかの種類のwhileループまたは再帰関数を使用して、額にソリューションを書き込むことができます。 ただし、Visitorパターンとその適応ast.NodeTransformerを使用します
これは、
visit_FunctionDef
や
visit_Expr
などの
visit_[NodeType]
ようなメソッドを継承および作成できるクラス
visit_Expr
。 メソッドが返す値は、AST要素の新しい値になります。 そして、Visitor自体は単純に再帰的にツリーを走査し、対応する要素がツリー内で発生したときにメソッドを呼び出します。 これにより、コードをより便利に整理できます。
- 装飾された関数をキャッチするための
visit_FunctionDef
メソッドを作成します。 その中で、関数がデコレータでラップされていることを確認し、ラップされている場合はデコレータを削除してマークself.decorated
-
visit_Expression
メソッドを作成して、バイナリシフトをキャッチします。 その中で、self.decorated
フラグがself.decorated
ていることと、Expr
が正確にバイナリシフトであることを確認します。 残りの操作(Expr
からAssign
への変換)は手動で実行します。 幸いなことに、必要なデータはすべて近くにあります。
実際にコード
要点
# -*- coding: utf-8 -*- ''' Created on 2012-10-07 @author: Sergey <me@seriyps.ru> http://habrahabr.ru/post/153595/ AST ''' import ast import marshal import py_compile import time import os.path class RewriteGenTask(ast.NodeTransformer): def __init__(self, *args, **kwargs): self.on_decorator = [] self.on_assign = [] super(RewriteGenTask, self).__init__(*args, **kwargs) def shortgen_deco_pos(self, decorator_list): # , # shortgen . for pos, deco in enumerate(decorator_list): # Name(id='shortgen', ctx=Load()) if isinstance(deco, ast.Name) and deco.id == 'shortgen': return pos return -1 def visit_FunctionDef(self, node): """ , shortgen. , . FunctionDef( name='get_short', args=arguments(...), body=[...], decorator_list=[ Attribute(value=Name(id='web', ...), attr='asynchronous', ...), Attribute(value=Name(id='gen', ...), attr='engine', ...), Name(id='shortgen', ctx=Load())]) """ deco_pos = self.shortgen_deco_pos(node.decorator_list) if deco_pos >= 0: # shortgen , , # Visitor # self.on_decorator.append(True) node.decorator_list.pop(deco_pos) self.generic_visit(node) # self.on_decorator.pop() return node def visit_Expr(self, expr): """ == == result2 << func(arg, k=v, *args, **kwargs) result2 = gen.Task(func, arg, k=v, *args, **kwargs) AST "stmt << func(...)" ( ): Expr(value=BinOp(left=Name(id='result', ctx=Load()), op=LShift(), right=Call( func=Name(id='fetch', ctx=Load()), args=[Num(n=1)], keywords=[keyword(arg='k', value=Num(n=2))], starargs=Tuple(elts=[Num(n=3)], ctx=Load()), kwargs=Dict(keys=[Str(s='k2')], values=[Num(n=4)]))))) ---- vvvvvvvvvvv ---- AST "stmt = yield func(...)" (): Assign(targets=[Name(id='result', ctx=Store())], value=Yield(value=Call( func=Attribute(value=Name(id='gen', ctx=Load()), attr='Task', ctx=Load()), args=[Name(id='fetch', ctx=Load()), Num(n=1)], keywords=[keyword(arg='k', value=Num(n=2))], starargs=Tuple(elts=[Num(n=3)], ctx=Load()), kwargs=Dict(keys=[Str(s='k2')], values=[Num(n=4)])))) """ node = expr.value # BinOp if not (self.on_decorator and isinstance(expr.value, ast.BinOp) and isinstance(node.op, ast.LShift)): # (on_decorator ), # return expr # , LShift, , # gen.Task() # (stmt <<) # (stmt =). ctx=Load # ctx=Store ( self.visit_Load()) self.on_assign.append(True) assign_target = self.visit(node.left) self.on_assign.pop() # ... = ... (new_node, ) = ast.Assign( targets = [assign_target], value = ast.Yield( value=self.construct_gen_task_call(node.right))), # new_node = ast.fix_missing_locations(ast.copy_location(new_node, expr)) return new_node def construct_gen_task_call(self, func_call): """ gen.Task func(arg, k=v, *args, **kwargs) gen.Task(func, arg, k=v, *args, **kwargs) AST "func(...)": Call( func=Name(id='fetch', ctx=Load()), args=[Num(n=1)], keywords=[keyword(arg='k', value=Num(n=2))], starargs=Tuple(elts=[Num(n=3)], ctx=Load()), kwargs=Dict(keys=[Str(s='k2')], values=[Num(n=4)]))) ---- vvvvvvvvv ---- AST "gen.Task(func, ...)": Call( func=Attribute(value=Name(id='gen', ctx=Load()), attr='Task', ctx=Load()), args=[Name(id='fetch', ctx=Load()), Num(n=1)], keywords=[keyword(arg='k', value=Num(n=2))], starargs=Tuple(elts=[Num(n=3)], ctx=Load()), kwargs=Dict(keys=[Str(s='k2')], values=[Num(n=4)])) """ # gen.Task gen_task = ast.Attribute( value=ast.Name(id='gen', ctx=ast.Load()), attr='Task', ctx=ast.Load()) # gen.Task(func, ...) call = ast.Call( func=gen_task, # - 1- : args=[func_call.func] + func_call.args, keywords=func_call.keywords, starargs=func_call.starargs, kwargs=func_call.kwargs) return self.visit(call) def visit_Load(self, node): # Load() Store() if self.on_assign: return ast.copy_location(ast.Store(), node) return node def shortgen(f): raise RuntimeError("ERROR! file must be compiled with yield_ast!") def compile_file(filepath): path, filename = os.path.split(filepath) with open(filepath) as src: orig_ast = ast.parse(src.read()) new_ast = RewriteGenTask().visit(orig_ast) code = compile(new_ast, filename, 'exec') pyc_filename = os.path.splitext(filename)[0] + '.pyc' pyc_filepath = os.path.join(path, pyc_filename) with open(pyc_filepath, 'wb') as fc: fc.write(py_compile.MAGIC) py_compile.wr_long(fc, long(time.time())) marshal.dump(code, fc) fc.flush() if __name__ == '__main__': import sys if len(sys.argv) < 2: print "Usage: %s file_to_compile1.py [file2.py] ..." % sys.argv[0] for filename in sys.argv[1:]: compile_file(filename)
結果のASTは実行できます:
with open(filepath) as src: orig_ast = ast.parse(src.read()) new_ast = RewriteGenTask().visit(orig_ast) code = compile(new_ast, filename, 'exec') exec code
または.pyoファイルに保存します
stackoverflow.com/questions/8627835/generate-pyc-from-python-ast
gist.github.com/3849217#L172
そして、インポートするか、
python my_module.pyo
呼び出します
おわりに
AST変換は、プログラムコードを変換するためのより信頼性の高い移植可能な方法です。 このような変換の記述は、バイトコードを変更するよりもはるかに簡単です。 このメソッドは、LispやErlangなど、多くの言語で広く使用されています。
2番目のプラス-パッチを適用する必要はありません。変換は、外部コードでも外部コードでも同じように機能します。
残りの長所と短所は、元の記事に関する私の解説で説明されています 。 繰り返しになりますが、主な欠点は、AST変換をその場で適用することが問題になることです。 コンパイル段階で.pycファイルに実装する必要があります。 (もちろん、そのようなハッキングを使用する場合は、これを適切に文書化する必要があります)。
この収量がいくつかの場所で書かれている小さなプロジェクトでは、そのような砂糖はあまり意味がありません。さらに、開発が複雑になります。 ファイルコンパイルの別の段階が表示されます。 しかし、大規模なトルネードプロジェクトでは、試すことができます。
参照資料
すべての要点コード
ASTドキュメント
tornado.genのドキュメント
ASTから.pycファイルを生成する
これがすべて怖い松葉杖のように見える場合、xDがあります
宿題
- この3人のデコレータのリストが不気味に見えるのは本当ですか?
@asynchronous @gen.engine @shortgen
@shortgen
だけでASTを使用する方法ですが、ASTはどうですか? - 現在の実装では、デコレータを
@my_module.shortgen
同じように使用する必要があり、@my_module.shortgen
なくなります。 さらに、tornado.genモジュールfrom tornado import gen
からimport tornado.gen
かfrom tornado.gen import Task
かfrom tornado.gen import Task
必要がなくなります。 修正方法 - shortgenを使用してTornadoWEBサーバーを介して大きなファイルを配布する記事からサーバーを書き直してみて、コンパイルして実行してください。