The behaviour really has very little to do with how parameters are passed (which is always the same way; there is no distinction in Python where things are sometimes passed by reference and sometimes passed by value). Rather the problem is to do with how names themselves are found.
lambda: i
creates a function that is of course equivalent to:
def anonymous():
return i
That i
is a name, within the scope of anonymous
. But it's never bound within that scope (not even as a parameter). So for that to mean anything i
must be a name from some outer scope. To find a suitable name i
, Python will look at the scope in which anonymous
was defined in the source code (and then similarly out from there), until it finds a definition for i
.1
So this loop:
foo = []
for i in range(3):
foo.append((lambda: i))
for l in foo:
print(l())
Is almost exactly as if you had written this:
foo = []
for i in range(3):
def anonymous():
return i
foo.append(anonymous)
for l in foo:
print(l())
So that i
in return i
(or lambda: i
) ends up being the same i
from the outer scope, which is the loop variable. Not that they are all references to the same object, but that they are all the same name. So it's simply not possible for the functions stored in foo
to return different values; they're all returning the object referred to by a single name.
To prove it, watch what happens when I remove the variable i
after the loop:
>>> foo = []
>>> for i in range(3):
foo.append((lambda: i))
>>> del i
>>> for l in foo:
print(l())
Traceback (most recent call last):
File "<pyshell#7>", line 2, in <module>
print(l())
File "<pyshell#3>", line 2, in <lambda>
foo.append((lambda: i))
NameError: global name 'i' is not defined
You can see that the problem isn't that each function has a local i
bound to the wrong thing, but rather than each function is returning the value of the same global variable, which I've now removed.
OTOH, when your loop looks like this:
foo = []
for i in range(3):
foo.append((lambda i=i: i))
for l in foo:
print(l())
That is quite like this:
foo = []
for i in range(3):
def anonymous(i=i):
return i
foo.append(anonymous)
for l in foo:
print(l())
Now the i
in return i
is not the same i
as in the outer scope; it's a local variable of the function anonymous
. A new function is created in each iteration of the loop (stored temporarily in the outer scope variable anonymous
, and then permanently in a slot of foo
), so each one has it's own local variables.
As each function is created, the default value of its parameter is set to the value of i
(in the scope defining the functions). Like any other "read" of a variable, that pulls out whatever object is referenced by the variable at that time, and thereafter has no connection to the variable.2
So each function gets the default value of i
as it is in the outer scope at the time it is created, and then when the function is called without an argument that default value becomes the value of the i
in that function's local scope. Each function has no non-local references, so is completely unaffected by what happens outside it.
1 This is done at "compile time" (when the Python file is converted to bytecode), with no regard for what the system is like at runtime; it is almost literally looking for an outer def
block with i = ...
in the source code. So local variables are actually statically resolved! If that lookup chain falls all the way out to the module global scope, then Python assumes that i
will be defined in the global scope at the point that the code will be run, and just treats i
as a global variable whether or not there is a statically visible binding for i
at module scope, hence why you can dynamically create global variables but not local ones.
2 Confusingly, this means that in lambda i=i: i
, the three i
s refer to three completely different "variables" in two different scopes on the one line.
The leftmost i
is the "name" holding the value that will be used for the default value of i
, which exists independently of any particular call of the function; it's almost exactly "member data" stored in the function object.
The second i
is an expression evaluated as the function is created, to get the default value. So the i=i
bit acts very like an independent statement the_function.default_i = i
, evaluated in the same scope containing the lambda
expression.
And finally the third i
is actually the local variable inside the function, which only exists within a call to the anonymous function.