Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scope Variable with a Loop

Tags:

python

I read a sentence in the official handbook of Python.

If a lambda or def defined within a function is nested inside a loop, and the >nested function references an enclosing scope variable that is changed by that >loop, all functions generated within the loop will have the same value—the value >the referenced variable had in the last loop iteration. In such cases, you must >still use defaults to save the variable’s current value instead.

As for the script:

def makeActions():
     acts=[]
     for i in range(5):
         acts.append(lambda x: i**x)
     return acts
acts=makeActions()
print(acts)

I know how the problem looks like and how to solve it. But I just can not understand reason. please have a look at my interpretations:

def makeActions():
    acts=[]
    for i in range(5):
        acts.append(lambda x: i**x) #step2-step6: each lambda would be stored 
                                    #in the list, however, with its own "i"
    return acts

acts=makeActions() #step1: when the function is called, the list would be 
                   #created. And lambda would be called as well.
print(acts)

My point is that in each iteration, each lambda would be called even if the parameter x is not given. Thus, in the list "acts", each element (namely each lambda) has different "i".

I know I am wrong, but could you tell me why? Thx!

like image 958
Harry Avatar asked Jul 01 '26 16:07

Harry


2 Answers

To clarify a bit more on the subject, the reference to i does not change within the lambda - you see that each iteration of i generates a new object:

def makeActions():
  acts=[]
  for i in range(5):
    print('i:', i, 'object id:', id(i))
    acts.append(lambda: id(i))
  print('final i:', i, 'object id:', id(i))
  return acts
acts=makeActions()
print([fn() for fn in acts])

Returns

i: 0 object id: 140418781832928
i: 1 object id: 140418781832960
i: 2 object id: 140418781832992
i: 3 object id: 140418781833024
i: 4 object id: 140418781833056
final i: 4 object id: 140418781833056
[140418781833056, 140418781833056, 140418781833056, 140418781833056, 140418781833056]

The reference from i always points to its last assigned object, thus the id of i will not change after its last alteration.

If you want to keep the value at the time of the lambda creation, you need to "trap" the value of the reference at the desired place and time, for instance you could delegate the lambda creation to a function:

def delegated(d):
  return lambda: id(d)

def makeDelegatedActions():
  acts=[]
  for i in range(5):
    print('i:', i, 'object id:', id(i))
    acts.append(delegated(i))
  print('final i:', i, 'object id:', id(i))
  return acts
acts=makeDelegatedActions()
print([fn() for fn in acts])

Which returns

i: 0 object id: 140418781832928
i: 1 object id: 140418781832960
i: 2 object id: 140418781832992
i: 3 object id: 140418781833024
i: 4 object id: 140418781833056
final i: 4 object id: 140418781833056
[140418781832928, 140418781832960, 140418781832992, 140418781833024, 140418781833056]

Online demo here

like image 171
wiesion Avatar answered Jul 03 '26 07:07

wiesion


Each lambda will just know to fetch i from its enclosing scope, which is the makeActions call. They don't actually do so until they're called themselves. i is the same name within the same scope, it just happens to take different values during the loop, and will simply hold the last value after the loop finishes. So the lambdas just ended up creating 5 identical functions.

A variation that can work:

def expgen(n):
    def myexp(x):
        return n**x
    return myexp

acts = [expgen(i) for i in range(5)]

In this case, the scope that n is fetched from is that of expgen, which had distinct calls in the list comprehension, and each function will work independently.

like image 39
Yann Vernier Avatar answered Jul 03 '26 05:07

Yann Vernier



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!