web-dev-qa-db-ja.com

ネストされた関数にクロージャを*適用*することはできますか、それとも外部関数全体を繰り返す必要がありますか?

私たちが使用するサードパーティライブラリには、その中にネストされた関数を使用するかなり長い関数が含まれています。そのライブラリを使用すると、その関数にバグが発生します。そのバグを解決したいと考えています。

残念ながら、ライブラリのメンテナは修正に多少時間がかかりますが、ライブラリをフォークする必要はありません。また、問題が修正されるまでリリースを保留することはできません。

ここでは、ソースにパッチを適用するよりも追跡が簡単なため、モンキーパッチを使用してこの問題を修正することをお勧めします。ただし、内部関数を置き換えるだけで十分な非常に大きな関数を繰り返すと、やり過ぎになり、他の人が正確に何を変更したかを確認するのが難しくなります。ライブラリの卵への静的パッチで立ち往生していますか?

内部関数は、変数を閉じることに依存しています。不自然な例は次のとおりです。

_def outerfunction(*args):
    def innerfunction(val):
        return someformat.format(val)

    someformat = 'Foo: {}'
    for arg in args:
        yield innerfunction(arg)
_

ここで、innerfunction()の実装だけを置き換えたいと思います。実際の外部機能ははるかに長いです。もちろん、閉じた変数を再利用し、関数のシグネチャを維持します。

48
Martijn Pieters

はい、クロージャを使用している場合でも、内部関数を置き換えることができます。ただし、いくつかのフープをジャンプする必要があります。以下を考慮してください:

  1. Pythonが同じクロージャを作成するようにするには、置換関数もネストされた関数として作成する必要があります。元の関数の名前がfoobarである場合は、を定義する必要があります。同じ名前が閉じられた入れ子関数としての置換。さらに重要なことに、これらの名前を使用する必要があります同じ順序で;クロージャはインデックスによって参照されます。

  2. モンキーパッチは常に壊れやすく、実装を変更すると壊れることがあります。これも例外ではありません。パッチを適用したライブラリのバージョンを変更するたびに、モンキーパッチを再テストしてください。

これがどのように機能するかを理解するために、最初にPythonがネストされた関数を処理する方法を説明します。Pythonはコードオブジェクトを使用して関数を生成します各コードオブジェクトには定数シーケンスが関連付けられており、ネストされた関数のコードオブジェクトはそのシーケンスに格納されます。

>>> def outerfunction(*args):
...     def innerfunction(val):
...         return someformat.format(val)
...     someformat = 'Foo: {}'
...     for arg in args:
...         yield innerfunction(arg)
... 
>>> outerfunction.__code__
<code object outerfunction at 0x105b27ab0, file "<stdin>", line 1>
>>> outerfunction.__code__.co_consts
(None, <code object innerfunction at 0x10f136ed0, file "<stdin>", line 2>, 'outerfunction.<locals>.innerfunction', 'Foo: {}')

co_constsシーケンスは不変オブジェクトであるタプルであるため、内部コードオブジェクトを単に交換することはできません。 justそのコードオブジェクトが置き換えられた新しい関数オブジェクトを生成する方法については、後で説明します。

次に、クロージャについて説明する必要があります。コンパイル時に、Pythonは、a)someformatinnerfunctionのローカル名ではなく、b)outerfunctionの同じ名前を閉じていることを判別します。 Pythonは、正しい名前ルックアップを生成するためのバイトコードを生成するだけでなく、ネストされた関数と外部関数の両方のコードオブジェクトに注釈を付けて、someformatが閉じられることを記録します。

>>> outerfunction.__code__.co_cellvars
('someformat',)
>>> outerfunction.__code__.co_consts[1].co_freevars
('someformat',)

置換内部コードオブジェクトが自由変数と同じ名前のみをリストし、同じ順序でリストするようにする必要があります。

クロージャは実行時に作成されます。それらを生成するためのバイトコードは、外部関数の一部です。

>>> import dis
>>> dis.dis(outerfunction)
  2           0 LOAD_CLOSURE             0 (someformat)
              2 BUILD_Tuple              1
              4 LOAD_CONST               1 (<code object innerfunction at 0x10f136ed0, file "<stdin>", line 2>)
              6 LOAD_CONST               2 ('outerfunction.<locals>.innerfunction')
              8 MAKE_FUNCTION            8
             10 STORE_FAST               1 (innerfunction)

# ... rest of disassembly omitted ...

そこでのLOAD_CLOSUREバイトコードは、someformat変数のクロージャを作成します。 Pythonは、関数で使用されるのと同じ数のクロージャを作成します内部関数で最初に使用される順序で。これは、後で覚えておくべき重要な事実です。関数それ自体がこれらのクロージャを位置によって検索します。

>>> dis.dis(outerfunction.__code__.co_consts[1])
  3           0 LOAD_DEREF               0 (someformat)
              2 LOAD_METHOD              0 (format)
              4 LOAD_FAST                0 (val)
              6 CALL_METHOD              1
              8 RETURN_VALUE

LOAD_DEREFオペコードは、ここで0の位置にあるクロージャを選択して、someformatクロージャにアクセスしました。

理論的には、これは内部関数のクロージャにまったく異なる名前を使用できることも意味しますが、デバッグの目的では、同じ名前に固執する方がはるかに理にかなっています。また、同じ名前を使用している場合はco_freevarsタプルを比較できるため、置換関数が適切にスロットインされることを確認するのも簡単になります。

さて、スワッピングのトリックです。関数は、Pythonの他のオブジェクトと同様に、特定のタイプのインスタンスです。型は正常に公開されませんが、type()呼び出しはそれでもそれを返します。同じことがコードオブジェクトにも当てはまり、どちらのタイプにもドキュメントがあります。

>>> type(outerfunction)
<type 'function'>
>>> print(type(outerfunction).__doc__)
Create a function object.

  code
    a code object
  globals
    the globals dictionary
  name
    a string that overrides the name from the code object
  argdefs
    a Tuple that specifies the default argument values
  closure
    a Tuple that supplies the bindings for free variables
>>> type(outerfunction.__code__)
<type 'code'>
>>> print(type(outerfunction.__code__).__doc__)
code(argcount, kwonlyargcount, nlocals, stacksize, flags, codestring,
      constants, names, varnames, filename, name, firstlineno,
      lnotab[, freevars[, cellvars]])

Create a code object.  Not for the faint of heart.

これらの型オブジェクトを使用して、定数が更新された新しいcodeオブジェクトを生成し、次にコードオブジェクトが更新された新しい関数オブジェクトを生成します。

def replace_inner_function(outer, new_inner):
    """Replace a nested function code object used by outer with new_inner

    The replacement new_inner must use the same name and must at most use the
    same closures as the original.

    """
    if hasattr(new_inner, '__code__'):
        # support both functions and code objects
        new_inner = new_inner.__code__

    # find original code object so we can validate the closures match
    ocode = outer.__code__
    function, code = type(outer), type(ocode)
    iname = new_inner.co_name
    orig_inner = next(
        const for const in ocode.co_consts
        if isinstance(const, code) and const.co_name == iname)
    # you can ignore later closures, but since they are matched by position
    # the new sequence must match the start of the old.
    assert (orig_inner.co_freevars[:len(new_inner.co_freevars)] ==
            new_inner.co_freevars), 'New closures must match originals'
    # replace the code object for the inner function
    new_consts = Tuple(
        new_inner if const is orig_inner else const
        for const in outer.__code__.co_consts)

    # create a new function object with the new constants
    return function(
        code(ocode.co_argcount, ocode.co_kwonlyargcount, ocode.co_nlocals,
             ocode.co_stacksize, ocode.co_flags, ocode.co_code, new_consts,
             ocode.co_names, ocode.co_varnames, ocode.co_filename,
             ocode.co_name, ocode.co_firstlineno, ocode.co_lnotab,
             ocode.co_freevars,
             ocode.co_cellvars),
        outer.__globals__, outer.__name__, outer.__defaults__,
        outer.__closure__)

上記の関数は、新しい内部関数(コードオブジェクトまたは関数として渡すことができます)が実際に元の関数と同じクロージャを使用することを検証します。次に、古いouter関数オブジェクトと一致する新しいコードと関数オブジェクトを作成しますが、ネストされた関数(名前で配置)はモンキーパッチに置き換えられます。

上記のすべてが機能することを示すために、innerfunctionを、フォーマットされた各値を2ずつインクリメントするものに置き換えてみましょう。

>>> def create_inner():
...     someformat = None  # the actual value doesn't matter
...     def innerfunction(val):
...         return someformat.format(val + 2)
...     return innerfunction
... 
>>> new_inner = create_inner()

新しい内部関数も入れ子関数として作成されます。これは、Pythonが正しいバイトコードを使用してsomeformatクロージャを検索することを保証するため重要です。returnステートメントを使用して関数オブジェクトを抽出しましたが、create_inner.__code__.co_constsを参照してコードオブジェクト。

これで、元の外部関数にパッチを適用して、内部関数をスワップアウトできますjust

>>> new_outer = replace_inner_function(outerfunction, new_inner)
>>> list(outerfunction(6, 7, 8))
['Foo: 6', 'Foo: 7', 'Foo: 8']
>>> list(new_outer(6, 7, 8))
['Foo: 8', 'Foo: 9', 'Foo: 10']

元の関数は元の値をエコーアウトしましたが、新しい戻り値は2ずつ増加しました。

少ないクロージャを使用する新しい置換内部関数を作成することもできます。

>>> def demo_outer():
...     closure1 = 'foo'
...     closure2 = 'bar'
...     def demo_inner():
...         print(closure1, closure2)
...     demo_inner()
...
>>> def create_demo_inner():
...     closure1 = None
...     def demo_inner():
...         print(closure1)
...
>>> replace_inner_function(demo_outer, create_demo_inner.__code__.co_consts[1])()
foo

だから、絵を完成させるために:

  1. モンキーパッチの内部関数を、同じクロージャを持つ入れ子関数として作成します
  2. replace_inner_function()を使用してnew外部関数を生成します
  3. 手順2で作成した新しい外部関数を使用するために、元の外部関数にモンキーパッチを適用します。
44
Martijn Pieters

Martijnの答えは良いですが、削除するとよい欠点が1つあります。

置換内部コードオブジェクトが自由変数と同じ名前のみをリストし、同じ順序でリストするようにする必要があります。

これは通常の場合は特に難しい制約ではありませんが、名前の順序などの未定義の動作に依存するのは快適ではありません。問題が発生すると、非常に厄介なエラーが発生し、場合によってはハードクラッシュが発生する可能性があります。

私のアプローチには独自の欠点がありますが、ほとんどの場合、上記の欠点がそれを使用する動機になると思います。私の知る限り、それはよりポータブルであるべきです。

基本的なアプローチは、ソースにinspect.getsourceをロードし、それを変更してから評価することです。これは、物事を整然と保つためにASTレベルで行われます。

コードは次のとおりです。

import ast
import inspect
import sys

class AstReplaceInner(ast.NodeTransformer):
    def __init__(self, replacement):
        self.replacement = replacement

    def visit_FunctionDef(self, node):
        if node.name == self.replacement.name:
            # Prevent the replacement AST from messing
            # with the outer AST's line numbers
            return ast.copy_location(self.replacement, node)

        self.generic_visit(node)
        return node

def ast_replace_inner(outer, inner, name=None):
    if name is None:
        name = inner.__name__

    outer_ast = ast.parse(inspect.getsource(outer))
    inner_ast = ast.parse(inspect.getsource(inner))

    # Fix the source lines for the outer AST
    outer_ast = ast.increment_lineno(outer_ast, inspect.getsourcelines(outer)[1] - 1)

    # outer_ast should be a module so it can be evaluated;
    # inner_ast should be a function so we strip the module node
    inner_ast = inner_ast.body[0]

    # Replace the function
    inner_ast.name = name
    modified_ast = AstReplaceInner(inner_ast).visit(outer_ast)

    # Evaluate the modified AST in the original module's scope
    compiled = compile(modified_ast, inspect.getsourcefile(outer), "exec")
    outer_globals = outer.__globals__ if sys.version_info >= (3,) else outer.func_globals
    exec_scope = {}

    exec(compiled, outer_globals, exec_scope)
    return exec_scope.popitem()[1]

簡単なウォークスルー。 AstReplaceInnerast.NodeTransformerであり、特定のノードを特定の他のノードにマッピングすることでASTを変更できます。この場合、名前が一致するたびにast.FunctionDefノードを置き換えるにはreplacementノードが必要です。

ast_replace_innerは、私たちが本当に気にかけている関数であり、2つの関数と、オプションで名前を取ります。この名前は、内部関数を別の名前の別の関数に置き換えるために使用されます。

ASTが解析されます。

    outer_ast = ast.parse(inspect.getsource(outer))
    inner_ast = ast.parse(inspect.getsource(inner))

変換が行われます:

    modified_ast = AstReplaceInner(inner_ast).visit(outer_ast)

コードが評価され、関数が抽出されます。

    exec(compiled, outer_globals, exec_scope)
    return exec_scope.popitem()[1]

使用例を次に示します。この古いコードがbuggy.pyにあると仮定します。

def outerfunction():
    numerator = 10.0

    def innerfunction(denominator):
        return denominator / numerator

    return innerfunction

innerfunctionを次のように置き換えます

def innerfunction(denominator):
    return numerator / denominator

あなたが書く:

import buggy

def innerfunction(denominator):
    return numerator / denominator

buggy.outerfunction = ast_replace_inner(buggy.outerfunction, innerfunction)

または、次のように書くこともできます。

def divide(denominator):
    return numerator / denominator

buggy.outerfunction = ast_replace_inner(buggy.outerfunction, divide, "innerfunction")

この手法の主な欠点は、ターゲットと置換の両方で機能するためにinspect.getsourceが必要になることです。ターゲットが「組み込み」(Cで記述)であるか、配布前にバイトコードにコンパイルされている場合、これは失敗します。組み込みの場合、Martijnの手法も機能しないことに注意してください。

もう1つの大きな欠点は、内部関数の行番号が完全にねじれていることです。内部関数が小さい場合、これは大きな問題ではありませんが、内部関数が大きい場合は、これを検討する価値があります。

関数オブジェクトが同じ方法で指定されていない場合、他の欠点が生じます。たとえば、パッチを適用できませんでした

def outerfunction():
    numerator = 10.0

    innerfunction = lambda denominator: denominator / numerator

    return innerfunction

同じ方法;別のAST変換が必要になります。

特定の状況に最も適したトレードオフを決定する必要があります。

19
Veedrac

私はこれが必要でしたが、クラスとpython2/3で。だから私は@MartijnPietersのソリューションをいくつか拡張しました

import types, inspect, six

def replace_inner_function(outer, new_inner, class_class=None):
    """Replace a nested function code object used by outer with new_inner

    The replacement new_inner must use the same name and must at most use the
    same closures as the original.

    """
    if hasattr(new_inner, '__code__'):
        # support both functions and code objects
        new_inner = new_inner.__code__

    # find original code object so we can validate the closures match
    ocode = outer.__code__

    iname = new_inner.co_name
    orig_inner = next(
        const for const in ocode.co_consts
        if isinstance(const, types.CodeType) and const.co_name == iname)
    # you can ignore later closures, but since they are matched by position
    # the new sequence must match the start of the old.
    assert (orig_inner.co_freevars[:len(new_inner.co_freevars)] ==
            new_inner.co_freevars), 'New closures must match originals'
    # replace the code object for the inner function
    new_consts = Tuple(
        new_inner if const is orig_inner else const
        for const in outer.__code__.co_consts)

    if six.PY3:
        new_code = types.CodeType(ocode.co_argcount, ocode.co_kwonlyargcount, ocode.co_nlocals, ocode.co_stacksize,
             ocode.co_flags, ocode.co_code, new_consts, ocode.co_names,
             ocode.co_varnames, ocode.co_filename, ocode.co_name,
             ocode.co_firstlineno, ocode.co_lnotab, ocode.co_freevars,
             ocode.co_cellvars)
    else:
    # create a new function object with the new constants
        new_code = types.CodeType(ocode.co_argcount, ocode.co_nlocals, ocode.co_stacksize,
             ocode.co_flags, ocode.co_code, new_consts, ocode.co_names,
             ocode.co_varnames, ocode.co_filename, ocode.co_name,
             ocode.co_firstlineno, ocode.co_lnotab, ocode.co_freevars,
             ocode.co_cellvars)

    new_function= types.FunctionType(new_code, outer.__globals__, 
                                     outer.__name__, outer.__defaults__,
                                     outer.__closure__)

    if hasattr(outer, '__self__'):
        if outer.__self__ is None:
            if six.PY3:
                return types.MethodType(new_function, outer.__self__, class_class)
            else:
                return types.MethodType(new_function, outer.__self__, outer.im_class)
        else:
            return types.MethodType(new_function, outer.__self__, outer.__self__.__class__)

    return new_function

これは、関数、バインドされたクラスメソッド、およびバインドされていないクラスメソッドで機能するはずです。 (class_class引数は、バインドされていないメソッドのpython3にのみ必要です)。ほとんどの作業をしてくれた@MartijnPietersに感謝します!私はこれを理解していなかっただろう;)

3
Andy