Pythonでの関数バイトコードの変更

少し前に、私はかなり珍しい問題を解決する必要がありました。つまり、Pythonに非標準の演算子を追加しました。 このタスクは、goto演算子を含むアセンブラーに似た擬似コードを使用してPythonコードを生成することでした。 複雑なレキシカルアナライザーを記述したくありませんでした。擬似コードのgoto演算子を使用してループと条件付き遷移を整理しましたが、Pythonにはない類似物をPythonで使用したかったのです。



ジョークとして4月1日を記念してレイアウトされたモジュールがいくつかありますが、うまくいきませんでした。 この演算子を使用することの欠点を認識していることをすぐに予約したいのですが、場合によっては、コードを自動生成するときに、その使用によりプログラマーの生活が大幅に簡素化されます。 さらに、説明したアプローチでは、goto演算子を追加する例により、必要に応じて必要なコード変更を追加できます。これについては後で説明します。



そのため、Pythonにいくつかの新しいコマンドを追加する方法と、それらを正しく解釈する方法(必要なアドレスに移動する方法)に問題があります。 これを行うには、goto演算子を使用してラベルを追加する関数にフックするデコレーターを作成し、disモジュールを使用してPythonバイトコードを操作できるようにし、newを使用して内部pythonオブジェクトを動的に作成できるようにします。



まず、コマンドの形式を決めましょう。 pythonには多くの構文制限があるため、次の形式のコマンド



a: goto a
      
      







成功しません。 ただし、Pythonでは、次の形式の構造を追加できます。



 label .a goto .a
      
      







ここで、ポイントが重要な役割を果たすことに注意してください。 pythonはスペースをスキップし、クラス属性の呼び出しに減らします。 ドットなしで記録すると、構文エラーメッセージが表示されます。 したがって、これらのコマンドのバイトコードを考慮してください。 これを行うには、次のコードを実行します。



 >>> def f(): >>> label .a >>> goto .a >>> import dis >>> dis.dis( f ) 2 0 LOAD_GLOBAL 0 (label) 3 LOAD_ATTR 1 (a) 6 POP_TOP 3 7 LOAD_GLOBAL 2 (goto) 10 LOAD_ATTR 1 (a) 13 POP_TOP 14 LOAD_CONST 0 (None) 17 RETURN_VALUE
      
      







したがって、ラベル宣言およびラベル遷移コマンドは、LOAD_GLOBAL、LOAD_ATTR、POP_TOPの3つの操作に削減されます。メインは最初の2つです。 disモジュールを使用すると、opmap辞書を使用してこれらのコマンドのバイトコードを判別し、opname辞書のバイトコードを使用してそれらのシンボリック表現を取得できます。



 >>> dis.opmap[ 'LOAD_GLOBAL' ] 116 >>> dis.opmap[ 'LOAD_ATTR' ] 105
      
      







関数fのバイト表現はf.func_code.co_codeに保存され、変数のシンボリック表現はf.func_code.co_namesに保存されます。



 >>> f.func_code.co_names ('label', 'a', 'goto')
      
      







次に、興味のあるコマンドのバイト表現について少し説明します。 逆アセンブラーの一部は、LOAD_GLOBALおよびLOAD_ATTRコマンドが3バイトで表されることを示しています(オフセットは左側に示されています)。宣言する変数または属性に対応するf.func_code.co_names。



dis.HAVE_ARGUMENTと比較することで、コマンドに引数があるかどうか(したがってコマンドの長さ(バイト))を判別できます。 指定された定数以上の場合、引数がありますが、そうでない場合は引数がありません。 したがって、関数バイトコードを解析するための関数を取得します。 次に、ラベルコードをNOP操作に、gotoステートメントコードをJUMP_ABSOLUTEに置き換えます。JUMP_ABSOLUTEは、関数内のオフセットをパラメーターとして受け取ります。 それが事実上すべてです。 デコレータのコードと使用例を以下に示します。



 import dis, new class MissingLabelError( Exception ): pass class ExistingLabelError( Exception ): pass def goto( function ): labels_dict = {} gotos_list = [] command_name = '' previous_operation = '' i = 0 while i < len( function.func_code.co_code ): operation_code = ord( function.func_code.co_code[ i ] ) operation_name = dis.opname[ operation_code ] if operation_code >= dis.HAVE_ARGUMENT: lo_byte = ord( function.func_code.co_code[ i + 1 ] ) hi_byte = ord( function.func_code.co_code[ i + 2 ] ) argument_position = ( hi_byte << 8 ) ^ lo_byte if operation_name == 'LOAD_GLOBAL': command_name = function.func_code.co_names[ argument_position ] if operation_name == 'LOAD_ATTR' and previous_operation == 'LOAD_GLOBAL': if command_name == 'label': label = function.func_code.co_names[ argument_position ] if labels_dict.has_key( label ): raise ExistingLabelError( 'Label redifinition: %s' % label ) labels_dict.update( { label : i - 3 } ) elif command_name == 'goto': gotos_list += [ ( function.func_code.co_names[ argument_position ], i - 3 ) ] i += 3 else: i += 1 previous_operation = operation_name codebytes_list = list( function.func_code.co_code ) for label, index in labels_dict.items(): codebytes_list[ index : index + 7 ] = [ chr( dis.opmap[ 'NOP' ] ) ] * 7 #  7     LOAD_GLOBAL, LOAD_ATTR  POP_TOP  NOP for label, index in gotos_list: if label not in labels_dict: raise MissingLabelError( 'Missing label: %s' % label ) target_index = labels_dict[ label ] + 7 codebytes_list[ index ] = chr( dis.opmap[ 'JUMP_ABSOLUTE' ] ) codebytes_list[ index + 1 ] = chr( target_index & 0xFF ) codebytes_list[ index + 2 ] = chr( ( target_index >> 8 ) & 0xFF ) #  -    code = function.func_code new_code = new.code( code.co_argcount, code.co_nlocals, code.co_stacksize, code.co_flags, str().join( codebytes_list ), code.co_consts, code.co_names, code.co_varnames, code.co_filename, code.co_name, code.co_firstlineno, code.co_lnotab ) #    new_function = new.function( new_code, function.func_globals ) return new_function
      
      







使用例:



 @goto def test_function( n ): goto .label1 label .label2 print n goto .label3 label .label1 print n n -= 1 if n != 0: goto .label1 else: goto .label2 label .label3 print 'the end' test_function( 10 )
      
      







例の結果:



 10 9 8 7 6 5 4 3 2 1 0 the end
      
      







結論として、このソリューションはPythonの一般的なスタイルに完全に対応していないことを付け加えます:インタープリターのバージョンに強く依存しているため(この場合、インタープリター2.7が使用されましたが、2のすべてのバージョンで動作するはずです)、この問題の解決策ここでも、言語の優れた柔軟性と、必要な新しい機能を追加できることが証明されています。



All Articles