Local variables in nested Python functions

Well, bear with me on this, I know that it will look terribly confusing, but please help me understand what is happening.

from functools import partial class Cage(object): def __init__(self, animal): self.animal = animal def gotimes(do_the_petting): do_the_petting() def get_petters(): for animal in ['cow', 'dog', 'cat']: cage = Cage(animal) def pet_function(): print "Mary pets the " + cage.animal + "." yield (animal, partial(gotimes, pet_function)) funs = list(get_petters()) for name, f in funs: print name + ":", f() 

gives:

 cow: Mary pets the cat. dog: Mary pets the cat. cat: Mary pets the cat. 

So basically, why am I not getting three different animals? Isn't cage 'packed' into a local area of โ€‹โ€‹a nested function? If not, how does a call to a nested function look for local variables?

I know that executing such problems usually means that one of them โ€œdoes it wrongโ€, but I would like to understand what is happening.

+93
scope closures python nested-function
Sep 14 '12 at 11:28
source share
3 answers

A nested function looks for variables from the parent scope when it is executed, and not when defined.

The body of the function is compiled, and the "free" variables (not defined in the function itself for the purpose) are checked and then linked as trailing cells to the function, and the code uses the index to refer to each cell. pet_function thus, pet_function has one free variable ( cage ), which is then referenced through the closure cell, index 0. The closure itself points to the local cage variable in the get_petters function.

When you actually call a function, this closure is then used to view the cage value in the surrounding area at the time the function was called. This is where the problem lies. By the time you call your functions, the get_petters function get_petters already running, evaluating it. At one point during this execution, the local cage variable was assigned to each of the lines 'cow' , 'dog' and 'cat' , but at the end of this function, cage contains the last value of 'cat' . Thus, when you call each of the dynamically returned functions, you get the value 'cat' .

A workaround is not to rely on closure. Instead, you can use a partial function, create a new function area, or bind a variable as the default value for a keyword parameter.

  • An example of a partial function using functools.partial() :

     from functools import partial def pet_function(cage=None): print "Mary pets the " + cage.animal + "." yield (animal, partial(gotimes, partial(pet_function, cage=cage))) 
  • An example of creating a new area:

     def scoped_cage(cage=None): def pet_function(): print "Mary pets the " + cage.animal + "." return pet_function yield (animal, partial(gotimes, scoped_cage(cage))) 
  • Binding a variable as the default value for a keyword parameter:

     def pet_function(cage=cage): print "Mary pets the " + cage.animal + "." yield (animal, partial(gotimes, pet_function)) 

There is no need to define the scoped_cage function in a loop; compilation is performed only once, and not at each iteration of the loop.

+98
Sep 14 '12 at 11:37
source share

My understanding is that a cell is looked up in the namespace of the parent functions when the called function pet_function is actually called, and not earlier.

So when you do

 funs = list(get_petters()) 

You create 3 functions that will find the last created cell.

If you replace your last loop as follows:

 for name, f in get_petters(): print name + ":", f() 

In fact you will get:

 cow: Mary pets the cow. dog: Mary pets the dog. cat: Mary pets the cat. 
+11
Sep 14 '12 at 11:39
source share

It comes from the following

 for i in range(2): pass print i is 1 

after iteration, the value of i lazily stored as its final value.

As a generator, the function will work (i.e. print each value in turn), but when converted to a list that it runs through the generator , therefore all calls to cage ( cage.animal ) will return the cats.

+5
Sep 14
source share



All Articles