2
def outer(l):
    def inner(n):
        return l * n
    return inner

l = [1, 2, 3]
f = outer(l)
print(f(3))  # => [1, 2, 3, 1, 2, 3, 1, 2, 3]
l.append(4)
print(f(3))  # => [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4]

Because function outer returns the function inner, inner() is now bound to the name f. (f is a closure)

When f() is called, since l can't be found in the local namespace, but can be found in the global namespace, l(=[1, 2, 3]) is passed to f().

The first f(3) duplicates the list l 3 times, therefore, returns [1, 2, 3, 1, 2, 3, 1, 2, 3].

However, right before the second f(3), integer 4 is appended to the list l. Now, l = [1, 2, 3, 4].

As a result, the second f(3) duplicates the updated list l 3 times, therefore, returns [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4].

Am I correct? Or am I wrong?

Thank You.

Mario
  • 561
  • 3
  • 18
Happy Yoon
  • 67
  • 6
  • @joanis **no**. Arguments *are never passed by reference in python*. Python only supports a *sinlge evaluation strategy* that *doesn't depend on the type*. It is *neither* call by reference nor call by value\ – juanpa.arrivillaga Oct 02 '22 at 19:02
  • Alright, I'm not saying it well, then, but the reality remains that we have two variables that point to the same object, and I believe that's what's confusing OP. – joanis Oct 02 '22 at 19:10
  • @joanis yes, people *very frequently* misudnerstand what call-by-reference means. Python doesn't support it at all. Call by reference would work like this (making up an imaginary syntax to denote that a parameter is call by reference, similar to languages that do support it): `def foo(&x): x = 99; y = 10; foo(y); print(y)` would print `99`, not `10` like it does in Python. In call by reference, the parameter is *just an alias to the variable in the caller*! – juanpa.arrivillaga Oct 02 '22 at 19:12
  • 1
    Well, OK, you're right, strictly speaking. C++ has call by reference, Python does not. And I agree it's important to be careful to use the right words, so thank you for the clarification. – joanis Oct 02 '22 at 19:14
  • @joanis yes, it is programming after all! C++, Fortran, C# are examples of languages that support call-by-reference. – juanpa.arrivillaga Oct 02 '22 at 19:15
  • But I expect your answer might be a bit difficult to follow for a beginner. I might start with saying why it's the same list in both cases (in a more precise way that I did above) and then explain as you do why it's not the same variable. Strictly speaking, that's not what OP asked, but clearly the fact that it's the same list confused them and lead them to their incorrect understanding of the language. – joanis Oct 02 '22 at 19:18

1 Answers1

3

You are incorrect about this:

When f() is called, since l can't be found in the local namespace, but can be found in the global namespace, l(=[1, 2, 3]) is passed to f().

When f is called, it does a lookup in its closure. It is closed over the l that is local to outer.

Consider:

>>> def outer(l):
...     def inner(n):
...         return l * n
...     return inner
...
>>> l = [1, 2, 3]
>>> f = outer(l)
>>> f.__closure__
(<cell at 0x7fb3ddb1af10: list object at 0x7fb3ddaa51c0>,)

Consider what happens when you change what the global l is bound to:

>>> l = ["hello"]
>>> f(2)
[1, 2, 3, 1, 2, 3]
>>>

In this case, the global l and the local l in outer are no longer referring to the same object, but the variable still correctly finds the name that it is closed over.

And also:

>>> import dis
>>> dis.dis(f)
  3           0 LOAD_DEREF               0 (l)
              2 LOAD_FAST                0 (n)
              4 BINARY_MULTIPLY
              6 RETURN_VALUE

Note, it uses LOAD_DEREF to find l, this is the op-code that is use to lookup names in a closure (LOAD_FAST is for local variable lookups).

This is what you would see if it were using a global l:

>>> def foo():
...     print(l)
...
>>> foo()
['hello']
>>> dis.dis(foo)
  2           0 LOAD_GLOBAL              0 (print)
              2 LOAD_GLOBAL              1 (l)
              4 CALL_FUNCTION            1
              6 POP_TOP
              8 LOAD_CONST               0 (None)
             10 RETURN_VALUE
>>> l.append("world")
>>> foo()
['hello', 'world']
>>> l = ["something", "else", "entirely"]
>>> foo()
['something', 'else', 'entirely']

More explicitly:

>>> def with_closure(l):
...     def inner():
...         return l
...     return inner
...
>>> def global_lookup():
...     return l
...
>>> l = []
>>> f = with_closure(l)
>>> f()
[]
>>> global_lookup()
[]
>>> l.append("hi")
>>> f()
['hi']
>>> global_lookup()
['hi']
>>> l = ['bye']
>>> f()
['hi']
>>> global_lookup()
['bye']

So, notice the way that the two different functions behave differently when you re-assign the global variable (instead of just mutating the object the global variable refers to)

juanpa.arrivillaga
  • 88,713
  • 10
  • 131
  • 172
  • but when the second `f(3)` is called it founds `l` in global scope (cuz we see `4` in output), so as there is no reassignment of `l` withing outer function that means that in the first `f(3)` calling it brings `l` from global namespace too, doesn't it? – Dmitriy Neledva Oct 02 '22 at 18:58
  • 3
    @DmitriyNeledva **no**. Those are *two different names referring to the same object*. **Names have a scope**. Objects do not. The `l` in the global scope refers to *the same object* as the local variable `l` inside `outer`, which is what the free-variable `l` inside `inner` is closed under. Look what happens if you do `l = ["hi"]` – juanpa.arrivillaga Oct 02 '22 at 18:59
  • 2
    @DmitriyNeledva, maybe you want to take a look at [this](https://stackoverflow.com/questions/240178/list-of-lists-changes-reflected-across-sublists-unexpectedly). – Ignatius Reilly Oct 02 '22 at 19:01
  • 1
    @DmitriyNeledva see the edits I've added to hopefully clarify this and let me know if it makes sense to you if you get the chance – juanpa.arrivillaga Oct 02 '22 at 19:13
  • this action `l = ['bye']` is actually two actions. the first one is the declaration of variable `l` and the second one is assignment this `l` to object `['bye']`. variable `l` is something like 'direct access to' object `['bye']`. in this line `l.append("hi")` var `l` is still 'access' to object `[]` and it changes this object, but in this line `l = ['bye']` var `l` becomes an `access` to the new object `['bye']` and now it has nothing to do with object `[]`(=`['hi']`) – Dmitriy Neledva Oct 02 '22 at 20:09
  • @DmitriyNeledva I wouldn't say "declaration", python doesn't really have variable declarations (sorta with type hints, but not really) – juanpa.arrivillaga Oct 02 '22 at 20:10
  • yeah, I understan that python doesn't have variable declarations (like in js) – Dmitriy Neledva Oct 02 '22 at 20:12
  • And you have it backwards - you don't *assign a variable to an object*, you *assign an object to a variable*. But it generally sounds like you have the right idea. – juanpa.arrivillaga Oct 02 '22 at 20:13
  • the general idea: we never change variables, variables are just 'way' ('access') to objects and we work with objects not with variables. and the true name of an object is not a variable but `id()`'s number in Random Access Memory – Dmitriy Neledva Oct 02 '22 at 20:23