This is a hack to get around the read-only nature of __closure__ on function objects. Watch your step!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | """inject_closure module"""
INJECTEDKEY = "injected_{}"
OUTERLINE = " outer_{0} = injected_{0}"
INNERLINE = " inner_{0} = outer_{0}"
SOURCE= ("def not_important():",
" def also_not_important():",
" return also_not_important")
def inject_closure(f, *args):
"""Return a copy of f, with a new closure.
The new closure will be derived from args, in the same
order. This requires that the caller have knowledge
of the existing closure.
"""
# build the source to exec
injected = {}
source = list(SOURCE)
for i in range(len(args)):
source.insert(1, OUTERLINE.format(i))
source.insert(-1, INNERLINE.format(i))
injected[INJECTEDKEY.format(i)] = args[i]
# exec the source and pull the new closure
exec("\n".join(source), injected, injected)
closure = injected["not_important"]().__closure__
# build the new function object
func = type(f)(f.__code__, f.__globals__, f.__name__,
f.__defaults__, closure)
func.__annotations__ = f.__annotations__
func.__doc__ = f.__doc__
func.__kwdefaults__ = f.__kwdefaults__
func.__module__ = f.__module__
return func
|
Like I said, this is a hack...
Example
def g():
a = 1
def f():
print(a)
return f
f1 = g()
f1()
# 1
x1 = inject_closure(f1, 2)
x1()
# 2
f1()
# 1
Seems funny. I have a few questions, please:
Is it not possible to provide a dict {free_variable:value}, rather a list that need be in the same order as the original closure ?
Would you have exemple when changing the closure is useful (having two functions sharing the same closure maybe) ?
What are the dangers of the approach (why should we watch your step) ?
Good questions! I'll do my best to answer. :)
1) First of all, keep in mind that we're not changing anything but the closure. That means that we have to keep the order the same and the length of the closure the same. The values in the closure should match up to the names in the code object's co_freevars attribute.
That said, I'm not exactly sure what you mean about using a dict instead of a list. If you are talking about the "args" tuple, then yes you could change the code to take a dict and sort it all out. Otherwise the hack is pretty much how it has to be when using dynamic code generation to build a new closure.
2) In reality, this recipe isn't going to buy you a lot. It is more of a demonstration that even explicitly read-only attributes like __closure__ can be manipulated. I suppose it could be useful in the case that you have two different functions sharing the same code object. You could also use it to manipulate function locals from outside the function. However, you can do this with __defaults__ and with regular globals as well.
3) The foremost concern is that there's a reason that __closure__ is read-only. There is no reason to encourage the hacking of function execution locals.
So if you use this recipe, it is with the understanding that the language does not support this behavior and it could change with the next patch. The recipe likely also exercises untested (and unsupported) corner cases that, if they break, would likely solicit no support on the bug tracker.
For instance, if you pass the wrong number of parameters to inject_closure(), it will freak out. You may even get a segfault. The length of __closure__ matching the length of co_freevars is an invariant that the interpreter always assumes to hold.