1

Please consider the code below.

a = []

def func1(x):
    return x

for i in range(3):
    def func2():
        return func1(i)

    a.append(func2)

for k in range(3):
    print(a[k]())

This prints out

2
2
2

From 'The use of aliases' in http://gestaltrevision.be/wiki/python/aliases (last section) and in 'Scope' section in http://gestaltrevision.be/wiki/python/functions_basics, I learnt that function parameters are actually aliases of arguments that are passed.

So according to that, in

def func1(x): return x

for i in range(3):
    def func2(): return func1(i)

I reasoned since x would be stored as an alias to i, even though i is reassigned each time the loop is executed, it would not matter to its alias, x.

So I expected the first three lines to output 0, 1, 2 instead of 2, 2, 2.

Can you explain what I did wrong here? Thanks

Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153
  • The way that Python variables work *in general* is totally orthogonal to the problem, but please read https://nedbatchelder.com/text/names1.html separately to understand that. Function parameters are not particularly special; they're just local variables that get assigned by the function call. The behaviour you see in this code is because of a completely different phenomenon. – Karl Knechtel Aug 19 '22 at 11:44

3 Answers3

2

You create here a closure func2 that uses variable i from the enclosing scope. The func2's instances are created by DEF statement, at the time of the FOR loop execution.

Then you execute the func2's after the FOR loop is exited. In python a loop variable doesn't destroyed after the loop exit. So your closure uses the current value of the i in enclosing scope, at the moment of exiting from the loop.

So in this code func1 changes nothing, the result will be the same without it.

1

If you want your code to work the way you want, do as follows

def func2(i): 
    def func1():
        return i
    return func1

a = [func2(i) for i in range(3)]
for k in range(3): 
    print(a[k]()) # prints 0 1 2

Now, why didn't your code work? Well it has to do with when objects are bound in to names in a closure, which func1 is. In your code the parameter x to func1 is being bound at runtime. Hence as each function in a has func1(i) and the value of i at printing time is 2, you get all 2. So the solution is to bind it at compile time i.e when func2 returns func1, i is already bound in func1.

C Panda
  • 3,297
  • 2
  • 11
  • 11
  • Oooh. So you mean it has to do with object of i being bound to x (as a different name, of course) at runtime? (When I print the values?) –  Apr 16 '16 at 07:05
  • @NamanJain In your code the body of `func1` doesn't know about `i` untill runtime. It's due to **Lazy Evaluation**. So at the last moment when `func1(i)` is to be evaluated `func1` uses whatever value it finds for `i` which is 2. – C Panda Apr 16 '16 at 07:10
  • But what about 'for i in range(3): def func2(k=i): return func1(k)'? I tried this and this one printed 0 1 2. –  Apr 16 '16 at 07:17
  • By lazy evaluation, wouldn't k be assigned to the latest i value? –  Apr 16 '16 at 07:18
  • 1
    @NamanJain Well, that worked because you are channeling `i` via the default argument `k`, and I did it via normal argument due to scoping reasons. Other than that both are exactly the same. – C Panda Apr 16 '16 at 07:37
  • I understand. So in conclusion basically all of this happened due to Python's Lazy Evaluation where it looked for the i value when the functions were actually called –  Apr 16 '16 at 07:47
  • 1
    @NamanJain Your conclusion is *almost* fully correct. It's just that unlike Haskell, Python gives this superb feature optionally i.e while using closures. If you use generators extensively like I do, you will get a different flavor of that and that's quite efficient and wonderful. – C Panda Apr 16 '16 at 07:57
0

When you do this:

for i in range(3):
    def func2():
        return func1(i)

You're "redefining" your func2 for every i in [0, 1, 2]. The final definition that lives is:

def func2():
    return func1(2)

It's as simple as that. Unfortunately it does not behave the way you expect.

th3an0maly
  • 3,360
  • 8
  • 33
  • 54