4 ms·
[Edit: quietbritishjim was faster than me, and makes more or less the same points. You can safely ignore this post.] First let's have a look at this: >>> [
by roelschroeven 3y ago
[Edit: quietbritishjim was faster than me, and makes more or less the same points. You can safely ignore this post.]
First let's have a look at this:
>>> [ x() for x in [lambda: i for i in range(5)] ]
[4, 4, 4, 4, 4]
Variable i in the function body is a nonlocal or global, taken from the nearest enclosing scope where i is bound. When a function (lambdas are functions just like any other) is executed, a nonlocal or global variable gets the value it has in its scope at the time of the function call. Step by step:
>>> import inspect
>>> i = 0
>>> a = lambda: i
>>> inspect.getclosurevars(a)
ClosureVars(nonlocals={}, globals={'i': 0}, builtins={}, unbound=set())
We see that variable i in a refers to the global i, which currently has value 0. Obviously a() returns 0:
>>> a()
0
Next we create a new lambda, with the same definition as before, but we first increment i.
>>> i = 1
>>> b = lambda: i
>>> inspect.getclosurevars(b)
ClosureVars(nonlocals={}, globals={'i': 1}, builtins={}, unbound=set())
So i in b refers to the global i (as before), which now has value 1. Unsurprisingly b() returns 1:
>>> b()
1
But variable i in a also refers to the global i which is now equal to 1:
>>> inspect.getclosurevars(a)
ClosureVars(nonlocals={}, globals={'i': 1}, builtins={}, unbound=set())
And sure enough, a() now returns 1:
>>> a()
1
We can set i to any value, and both a() and b() will then return that value.
>>> i = 'gotcha'
>>> a()
'gotcha'
>>> b()
'gotcha'
Going back to your example, the i used in the lambdas refers to a nonlocal instead of a global (the i used in the list comprehension), but that doesn't make a difference for the topic at hand. What does matter is that the current value of i is used, which is 4 after the for loop in the list comprehension.
Now to the second example.
>>> i = 0
>>> a = lambda i=i: i
i is now a local, since it's a parameter to the function. It's a bit unclear because i is used in two different meanings here; let's rename one of the uses to j. This is exactly equivalent:
>>> a = lambda j=i: j
>>> inspect.getclosurevars(a)
ClosureVars(nonlocals={}, globals={}, builtins={}, unbound=set())
This time there are no closure variables. j in the function body does not anymore refer to anything outside of the function scope. Instead a is now a function with one parameter, which returns the value of that parameter:
>>> a(10)
10
>>> a('hello')
'hello'
What's of course more important here is that we have a default value for that parameter. Default parameter values in Python are evaluated when the function is defined, not when it is executed. i was 0 when we defined a, so a will always have 0 as default parameter. We can see that default value:
>>> a.__defaults__
(0,)
Add a second function:
>>> i = 1
>>> b = lambda i=i: i
>>> b.__defaults__
(1,)
Now b is also a function taking one parameter, but this time that parameter has default value 1. Meanwhile the default value of function a has not changed:
>>> a.__defaults__
(0,)
So now we can predict that a() will return its default parameter value (which is 0), and b() will return its default parameter (which is 1). Let's check.
>>> a()
0
>>> b()
1
So the big difference is that your first example uses nonlocal/global variables which are evaluated when the function is called, while your second example uses default parameter values which are evaluated when the function is defined.
Note: you can change default parameter values though, if you really really want:
>>> a = lambda i=5: i
>>> a()
5
>>> a.__defaults__ = ("don't actually do this",)
>>> a()
"don't actually do this"
- acqq 3y agoI like your explanation, it has more missing information and also demonstrates the use of "import inspect", it's definitely not to be ignored! Did I understand correctly, the fact that the variable is not belonging to the closure is the non-intuitive part, not knowing "how it was decided to do in Python" I'd expect the variable to belong to the closure, but instead just the reference to the variable outside of the closure belongs to the closure? Which AFAIK doesn't save any memory (the reference still has to be there), it was simply a design decision? So in Python the closure just doesn't "enclose" the variables at all? So any use of any variable in it can destroy something used "outside"? How do those using these construct cope with that in practice? Just ignore? Invent some naming schemes? Something else? Reminds me on an old Fortran which allowed changing the global constants passed to the functions via an assignment to the function parameter inside of the function. :)
- roelschroeven 3y agoI guess it's a design decision, I guess it could have been designed differently, but I'm not really sure. Doing it differently could very well break other things that people do rely on. Referring to the original variables instead of making copies does save memory though: in your example there were 5 functions that each would have their own copy, instead of only 1 instance shared between them.