arunmj
arunmj

Reputation: 187

how does python scope work in this snippet?

I was trying to understand python scope with functions.Following snippet gave me an unexpected result.

def foo(val):
    return  val*val

flist = []

for i in range(3):
    flist.append(lambda : foo(i))

def bar1():
    for j in range(3,6):
        flist.append(lambda : foo(j))

def bar2():
    for k in range(6,9):
        a = lambda n: (lambda:foo(n))
        flist.append(a(k))

bar1()
bar2()

print([f() for f in flist])

Expected result was:

[0, 1, 4, 9, 16, 25, 36, 49, 64]

But got:

[4, 4, 4, 25, 25, 25, 36, 49, 64]

instead. In first two cases, last value of loop variable is passed to foo function. How does this code work?

Upvotes: 2

Views: 76

Answers (3)

quamrana
quamrana

Reputation: 39354

I have managed to understand what is going on after I rearranged the original code like this:

def foo(val):
    return  val*val

def make_foo(val):
    return lambda : foo(val)


flist = []

for i in range(3):
    flist.append(make_foo(i))

def bar1():
    for j in range(3,6):
        flist.append(lambda : foo(j))

def bar2():
    a = lambda n: (lambda:foo(n))
    for k in range(6,9):
        flist.append(a(k))

bar1()
bar2()

print([f() for f in flist])

Output:

[0, 1, 4, 25, 25, 25, 36, 49, 64]

Notice how the output has only changed slightly: [4,4,4 -> [0,1,4

The reason is that any lambda is also a closure which means that it closes over its surrounding context, which is the local stack frame.

The lambda in make_foo only has the stack frame of the inside of make_foo which only contains val.

In the first loop, make_foo is called three times, so each time a different stack frame is created, with val referring to different values.

In bar1, as before, there is only one lambda and only one stack frame (bar1 is only called once) and the stack frame, containing j ends up with j referring to the value 5.

In bar2, I have explicitly shown that a refers to a single lambda, but that lambda, although it refers to the local stack frame, does not refer to any local variables. During the loop the a lambda is actually called, but that returns another lambda, which, in turn refers to another stack frame. That stack frame is a new one inside a, and each of those stack frames contains and n which, like make_foo, refers to different values.

Another important thing to note is that each lambda is stored in flist. Since flist refers to all the lambdas, all the things that they refer to also still exist. This means that all those stack frames still exist, along with any local variables that the stack frames refer to.

Upvotes: 1

DSCH
DSCH

Reputation: 2366

The principle here is clousres. Here is quite an easy read about it https://www.programiz.com/python-programming/closure

And here is your code snippet with some comments to try and explain the process and the "unexpected" output:

def foo(val):
    return val*val

flist = []

for i in range(3):  # This loop runs first and i stops at 2 (as range defaults to start at 0 and stops before 3)
    flist.append(lambda : foo(i)) # we append a lambda expression that invokes foo with value of i three times
    # So so far flist contains three identical lambda expression in the first three indexes.
    # However the foo() function is being called only on the last print call and then and when it goes to evaluate i -
    # it's 2 for as it last stoped there.

def bar1():
    for j in range(3,6):
        # same principle applies here: three insertions to the flist all with j, that by the end of the loop will be 5
        flist.append(lambda : foo(j))

def bar2():
    for k in range(6,9):
        a = lambda n: (lambda: foo(n))
        # here it is deferent as we append the evaluated value of the lambda expression while we are still in the loop -
        # and we are not waiting to run foo() at the end. That means that flist will get the values of foo(6), foo(7), -
        # and foo(8) and just foo(8) three times if it was to be evaluated in the the print expression below.
        flist.append(a(k))

bar1()
bar2()

print([f() for f in flist])

Upvotes: 0

Scott Hunter
Scott Hunter

Reputation: 49803

In the first loop, each lambda you append is using the same variable, so all 3 instances use its final value; same thing for in bar1 (but it is using a different variable than the first loop used).

Upvotes: 1

Related Questions