Welcome, guest | Sign In | My Account | Store | Cart

This is a hack to get around the read-only nature of __closure__ on function objects. Watch your step!

Python, 39 lines
 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

2 comments

s_h_a_i_o 12 years, 7 months ago  # | flag

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) ?

Eric Snow (author) 12 years, 7 months ago  # | flag

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.

Created by Eric Snow on Fri, 17 Jun 2011 (MIT)
Python recipes (4591)
Eric Snow's recipes (39)

Required Modules

  • (none specified)

Other Information and Tasks