A 3rd party library we use contains a rather long function that uses a nested function inside it. Our use of that library triggers a bug in that function, and we very much would like to solve that bug.
Unfortunately, the library maintainers are somewhat slow with fixes, but we don't want to have to fork the library. We also cannot hold our release until they have fixed the issue.
We would prefer to use monkey-patching to fix this issue here as that is easier to track than patching the source. However, to repeat a very large function where just replacing the inner function would be enough feels overkill, and makes it harder for others to see what exactly we changed. Are we stuck with a static patch to the library egg?
The inner function relies on closing over a variable; a contrived example would be:
def outerfunction(*args):
def innerfunction(val):
return someformat.format(val)
someformat = 'Foo: {}'
for arg in args:
yield innerfunction(arg)
where we would want to replace just the implementation of innerfunction()
. The actual outer function is far, far longer. We'd reuse the closed-over variable and maintain the function signature, of course.
I needed this, but in a class and python2/3. So I extended @MartijnPieters's solution some
This should now work for functions, bound classmethods and unbound class methods. (The class_class argument is only needed for python3 for unbound methods). Thanks @MartijnPieters for doing most of the work! I never would have figured this out ;)
Martijn's answer is good, but there is one drawback that would be nice to remove:
This isn't a particularly difficult constraint for the normal case, but it isn't pleasant to be dependent on undefined behaviours like name ordering and when things go wrong there are potentially really nasty errors and possibly even hard crashes.
My approach has its own drawbacks, but in most cases I believe the drawback above would motivate using it. As far as I can tell, it should also be more portable.
The basic approach is to load the source with
inspect.getsource
, change it and then evaluate it. This is done at AST level in order to keep things in order.Here is the code:
A quick walkthrough.
AstReplaceInner
is anast.NodeTransformer
, which just allows you to modify ASTs by mapping certain nodes to certain other nodes. In this case, it takes areplacement
node to replace anast.FunctionDef
node with whenever names match.ast_replace_inner
is the function we really care about, which takes two functions and optionally a name. The name is used to allow replacing the inner function with another function of a different name.The ASTs are parsed:
The transformation is made:
The code is evaluated and the function extracted:
Here's an example of use. Assume this old code is in
buggy.py
:You want to replace
innerfunction
withYou write:
Alternatively, you could write:
The main disadvantage of this technique is that one requires
inspect.getsource
to work on both the target and replacement. This will fail if the target is "built-in" (written in C) or compiled to bytecode before distributing. Note that if it's built-in, Martijn's technique will not work either.Another major disadvantage is that the line numbers from the inner function are completely screwy. This is not a big problem if the inner function is small, but if you have a large inner function this is worth thinking about.
Other disadvantages come from if the function object is not specified in the same way. For example, you could not patch
the same way; a different AST transformation would be needed.
You should decide which tradeoff makes the most sense for your particular circumstance.
Yes, you can replace an inner function, even if it is using a closure. You'll have to jump through a few hoops though. Please take into account:
You need to create the replacement function as a nested function too, to ensure that Python creates the same closure. If the original function has a closure over the names
foo
andbar
, you need to define your replacement as a nested function with the same names closed over. More importantly, you need to use those names in the same order; closures are referenced by index.Monkey patching is always fragile and can break with the implementation changing. This is no exception. Retest your monkey patch whenever you change versions of the patched library.
To understand how this will work, I'll first explain how Python handles nested functions. Python uses code objects to produce function objects as needed. Each code object has an associated constants sequence, and the code objects for nested functions are stored in that sequence:
The
co_consts
sequence is an immutable object, a tuple, so we cannot just swap out the inner code object. I'll show later on how we'll produce a new function object with just that code object replaced.Next, we need to cover closures. At compile time, Python determines that a)
someformat
is not a local name ininnerfunction
and that b) it is closing over the same name inouterfunction
. Python not only then generates the bytecode to produce the correct name lookups, the code objects for both the nested and the outer functions are annotated to record thatsomeformat
is to be closed over:You want to make sure that the replacement inner code object only ever lists those same names as free variables, and does so in the same order.
Closures are created at run-time; the byte-code to produce them is part of the outer function:
The
LOAD_CLOSURE
bytecode there creates a closure for thesomeformat
variable; Python creates as many closures as used by the function in the order they are first used in the inner function. This is an important fact to remember for later. The function itself looks up these closures by position:The
LOAD_DEREF
opcode picked the closure at position0
here to gain access to thesomeformat
closure.In theory this also means you can use entirely different names for the closures in your inner function, but for debugging purposes it makes much more sense to stick to the same names. It also makes verifying that the replacement function will slot in properly easier, as you can just compare the
co_freevars
tuples if you use the same names.Now for the swapping trick. Functions are objects like any other in Python, instances of a specific type. The type isn't exposed normally, but the
type()
call still returns it. The same applies to code objects, and both types even have documentation:We'll use these type objects to produce a new
code
object with updated constants, and then a new function object with updated code object:The above function validates that the new inner function (which can be passed in as either a code object or as a function) will indeed use the same closures as the original. It then creates new code and function objects to match the old
outer
function object, but with the nested function (located by name) replaced with your monkey patch.To demonstrate that the above all works, lets replace
innerfunction
with one that increments each formatted value by 2:The new inner function is created as a nested function too; this is important as it ensures that Python will use the correct bytecode to look up the
someformat
closure. I used areturn
statement to extract the function object, but you could also look atcreate_inner._co_consts
to grab the code object.Now we can patch the original outer function, swapping out just the inner function:
The original function echoed out the original values, but the new returned values incremented by 2.
You can even create new replacement inner functions that use fewer closures:
So, to complete the picture:
replace_inner_function()
to produce a new outer function