トルネードv2またはASTパッチでの非同期呼び出しの最短記録

私は記事「トルネードでの非同期呼び出しの最も短い記録」またはデコレータのパッチバイトコードに非常に興味がありましたが、実用的な観点からではなく、実装の観点からです。

それでも、実行時のバイトコードの変更は非常に危険で信頼性の低い操作です。 確かに、代替の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 
      
      





怪しいように見えますが、なんと柔軟性があります! 実際、すべてがシンプルです。

違いを見てみましょう。

  1. Expr



    完全になくなった
  2. BinOp(left, op, right)



    代わりにAssign(targets, value)



  3. 右オペランドのctx



    値がLoad



    からStore



    変更されました
  4. 呼び出しself.db.posts.find_e(...)



    gen.Task(self.db.posts.find_e, ...)



    置き換えられました
  5. 関数呼び出しの周りにyieldを追加
  6. デコレータがありません@shortgen





したがって、最初から2番目を取得するには、

  1. decorator_list



    @shortgen



    decorator_list



    を持つ関数を見つけます
  2. このデコレータを削除
  3. 関数本体でバイナリシフト演算子BinOp



    を見つけます
  4. 左右のオペランドを保存します。 左側で、 ctx



    Store



    Load



    に置き換え、右側のオペランドから関数名とその引数(positional、kw、および "star"-*、**)を抽出します。
  5. 関数の名前( self.db.posts.find_e



    )に最初の位置引数を追加します(つまり、この例では位置引数[self.db.posts.find_e, {'name': 'post'}]



    を取得し、残りはすべて空です。
  6. 新しいCall



    作成しますが、これらの引数で既にgen.Task



    関数を作成します
  7. 収量で包む
  8. Assign(targets, value)



    を作成し、ターゲットとして以前に左のオペランドBinOp



    を取り、値として-作成したばかり
  9. Expr



    ソースツリーで、新しくExpr



    Assign



    置き換えます




複雑に聞こえますが、コードでは50行強かかりました。 不明な点がある場合は、そこを見てください。



これを実装する方法は? 何らかの種類のwhileループまたは再帰関数を使用して、額にソリューションを書き込むことができます。 ただし、Visitorパターンとその適応ast.NodeTransformerを使用します



これは、 visit_FunctionDef



visit_Expr



などのvisit_[NodeType]



ようなメソッドを継承および作成できるクラスvisit_Expr



。 メソッドが返す値は、AST要素の新しい値になります。 そして、Visitor自体は単純に再帰的にツリーを走査し、対応する要素がツリー内で発生したときにメソッドを呼び出します。 これにより、コードをより便利に整理できます。

  1. 装飾された関数をキャッチするためのvisit_FunctionDef



    メソッドを作成します。 その中で、関数がデコレータでラップされていることを確認し、ラップされている場合はデコレータを削除してマークself.decorated



  2. 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があります



宿題






All Articles