私たちが使用するサードパーティライブラリには、その中にネストされた関数を使用するかなり長い関数が含まれています。そのライブラリを使用すると、その関数にバグが発生します。そのバグを解決したいと考えています。
残念ながら、ライブラリのメンテナは修正に多少時間がかかりますが、ライブラリをフォークする必要はありません。また、問題が修正されるまでリリースを保留することはできません。
ここでは、ソースにパッチを適用するよりも追跡が簡単なため、モンキーパッチを使用してこの問題を修正することをお勧めします。ただし、内部関数を置き換えるだけで十分な非常に大きな関数を繰り返すと、やり過ぎになり、他の人が正確に何を変更したかを確認するのが難しくなります。ライブラリの卵への静的パッチで立ち往生していますか?
内部関数は、変数を閉じることに依存しています。不自然な例は次のとおりです。
_def outerfunction(*args):
def innerfunction(val):
return someformat.format(val)
someformat = 'Foo: {}'
for arg in args:
yield innerfunction(arg)
_
ここで、innerfunction()
の実装だけを置き換えたいと思います。実際の外部機能ははるかに長いです。もちろん、閉じた変数を再利用し、関数のシグネチャを維持します。
はい、クロージャを使用している場合でも、内部関数を置き換えることができます。ただし、いくつかのフープをジャンプする必要があります。以下を考慮してください:
Pythonが同じクロージャを作成するようにするには、置換関数もネストされた関数として作成する必要があります。元の関数の名前がfoo
とbar
である場合は、を定義する必要があります。同じ名前が閉じられた入れ子関数としての置換。さらに重要なことに、これらの名前を使用する必要があります同じ順序で;クロージャはインデックスによって参照されます。
モンキーパッチは常に壊れやすく、実装を変更すると壊れることがあります。これも例外ではありません。パッチを適用したライブラリのバージョンを変更するたびに、モンキーパッチを再テストしてください。
これがどのように機能するかを理解するために、最初に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)someformat
がinnerfunction
のローカル名ではなく、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
だから、絵を完成させるために:
replace_inner_function()
を使用してnew外部関数を生成します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]
簡単なウォークスルー。 AstReplaceInner
はast.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変換が必要になります。
特定の状況に最も適したトレードオフを決定する必要があります。
私はこれが必要でしたが、クラスと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に感謝します!私はこれを理解していなかっただろう;)