for (var i = 0 ; i < 10 ; i ++ ){
someElement.addEventListener('click',function(){
console.log(i);
})
}
i would always be equal to 9. With let instead of var, i is properly scoped and the executed script display each increment correctly.The solution before let was to introduce a closure in the for statement body
for (var i = 0 ; i < 10 ; i ++ ){
(function(i){
someElement.addEventListener('click',function(){
console.log(i);
}))(i)
}
in order to capture i value. let can also easily isolate the scope of a variable so that it doesn't popule the global scope {
let foo = "bar";
var baz = "qix";
}
// foo is undefined here, while baz is defined.
which removes the need for self invoking functions.If we expand out the loop to be manual we would have a script like:
functions = []
# expanded loop
i = 1
functions.append(lambda: print(f"Hello {i}"))
i = 2
functions.append(lambda: print(f"Hello {i}"))
...
i = 9
functions.append(lambda: print(f"Hello {i}"))
Now at this point if we were to: print(f"Hello {i}")
What would the expected output be? I would posit that anything other than "Hello 9" would be wrong, both logically and intuitively.So by extension a loop of effectively "print(f"Hello {i}")" 9 times should just print "Hello 9" 9 times IMO. Anything else is counter-intuitive and definitely surprising.
def foo(i, j):
i[0] += 1
j += 1
a = [0]
b = 0
foo(a, b)
print(a) # [1]
print(b) # [0]
So people think the body of the loop like a function call.I don't think it's a bad expectation, in fact I think it's quite a natural expectation—in particular if you've programmed functional languages where modifying values is the exception, not the rule—which is why it surprises people. It's just not the one way Python chose.
Which ever makes the most sense depends on the context of a program which is why in languages that have both reference and closure semantics you can choose how the variables are captured. When that isn't the case you need to pick for someone, and it gets weird.
No, this comes down to “should a loop control variable be scoped to the block—or in python’s case function—the loop is in and updated with each iteration or a fresh variable scoped to each loop iteration that happens to share the same name.”
It seems like we're iterating through something to build up a computation we may execute later. I can conceive of a situation where you might want to do that, or at least consider it, but in general I say just do the work now and build a list of results.
It is rare, if only because defining functions (lambdas or otherwise) in a loop is somewhat uncommon.
That's not very helpful when you hit that issue though.
Here's another one, and the way I think about it:
def _loop(i):
functions.append(lambda: print(f"Hello {i}"))
_loop(0)
_loop(1)
_loop(2)
print(i)
# Error!
I highly prefer lexical scoping, where the variables are bound to the block they were declared in, like Javascript's `let` vs the old `var`. This avoids shadowing and general namespace pollution.I know it's not how Python operates, but I think it's how it should. Though I'd argue the syntactical similarities between functions and loops nudge users towards this second model.
>>> functions = []
>>> def _loop(i):
... functions.append(lambda: print(f"Hello {i}"))
...
>>> for i in range(10):
... _loop(i)
...
>>> for f in functions:
... f()
...
Hello 0
Hello 1
Hello 2
Hello 3
Hello 4
Hello 5
Hello 6
Hello 7
Hello 8
Hello 9
> lexical scoping, where the variables are bound to the block they were declared inIn Python that would only work if a "block" included comprehensions. For example:
>>> functions = [lambda: print(f"Hello {i}") for i in range(10)]
>>> for f in functions:
... f()
...
Hello 9
Hello 9
Hello 9
Hello 9
Hello 9
Hello 9
Hello 9
Hello 9
Hello 9
Hello 9
You could fix this by defining a _loop function as above and forming the closure inside it; but changing variable scoping to be lexical in "blocks" wouldn't fix this case unless the list comprehension itself counted as a "block", which is not how Python defines blocks.Since I was confused I did some internet searching and found this:
https://stackoverflow.com/questions/51604346/does-python-sco...
Which I think suggests the thing you are finding confusing isn't lexical scoping in Python, but rather the environment mutability.
Ultimately, I think I better understand what you are highlighting and can't say I entirely disagree. I just personally find how it is today ergonomic, but that could also just be bias as I am fairly comfortable in Python (warts and all).
So, at least in my book, no, Python has "global scope", "function scope" and probably one or two more scopes (I think there's a "class scope" as well).
Here's some code in Python, and some equivalent code in Go.
def foo(a_list):
print(f"list is {len(a_list} elements")
for element in a_list:
print(element)
print(element)
And here's the equivalent Go code: func foo(aList []int) { // Let's use ints...
fmt.Printf("list is %d elements\n", len(aList))
var element int // Notice this declaration! This is ensuring that element is declared outside the lexical scope of the for loop
for _, element = range aList {
fmt.Println(element)
}
fmt.Println(element)
}Python has lexical scoping, but it does _not_ have block level scoping.
https://en.wikipedia.org/wiki/Scope_(computer_science)#Block...
Which, now that I have done enough reading I think crystallizes the confusing thing for others being highlighted here (for me). Depending on preference, the lack of block scoping can be surprising for someone. Which also explains my bias, I started with Python which probably plays a large part in why I find function level scoping without block scoping ergonomic.
For example, equivalent code in Elixir:
i = 0
list = []
list = list ++ [fn -> i end]
i = i + 1
list = list ++ [fn -> i end]
i = i + 1
halfway = list
list = list ++ [fn -> i end]
i = i + 1
list = list ++ [fn -> i end]
IO.inspect Enum.map(halfway, fn f -> f.() end)
IO.inspect Enum.map(list, fn f -> f.() end)
Would produce the functional-intuitive result of
[0,1]
[0,1,2,3]Because there is no mutable state. Those repeated assignments to i and list are exactly equivalent to the scenario where each i was actually i1, i2, i3, etc.
They are both the behavior that’s intuitively obvious. And that’s despite them both being the same behavior.
As we can see, it’s important to not use the simple/obvious implementation because it’s so unintuitive it’ll need to be changed even if breaking (as in C#)
I wouldn't call this a design mistake but more of a misunderstanding of how scope works.
for i in range(10)
The statement literally declares or sets a variable named i in that scope. When the loop exits, i still exists in the scope with the value 9. If you call a function that was given a reference to i, the value will be 9 as expected because the function was called after the loop exited.
Don't see any quirk or mistake here.
For instance the behaviour of a Python loop varies drastically depending on the size of the iteration:
def loop(n):
for i in range(n):
pass
print(i)
loop(10) # 9
loop(0)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in loop
UnboundLocalError: local variable 'i' referenced before assignment
That Python works this way is specific to Python. And a language which doesn't have the issues this implies would be "getting it right", in the sense of avoiding sharp corners and edge cases. for f in (lambda: print(i) for i in range(0, 10)): f()
Unfortunately, Python is simply inconsistent in this regard. For example, list comprehensions leak the variable for back-compat reasons, so if you substitute (lambda: ...) with [lambda: ...] above, you'll get a bunch of 9s.But, backwards compatibility aside, the language could change to make for-loops behave like sequence comprehensions wrt scoping.
No, they don’t. They did in Python 2—list comps were introduced in 2.0, genexps in 2.4, and set/dict comps in 3.0 but also included in the later 2.7 release—but that’s been non-current for more than a decade, and conpletely out of support for two years. Let it go.
> But, backwards compatibility aside, the language could change to make for-loops behave like sequence comprehensions wrt scoping.
Sure in Python 4, but after 2->3, not sure many people are looking forward to that.
Python 3.10.5 (tags/v3.10.5:f377153, Jun 6 2022, 16:14:13) [MSC v.1929 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> [f() for f in [lambda: i for i in range(0, 10)]]
[9, 9, 9, 9, 9, 9, 9, 9, 9, 9]
>>> [f() for f in (lambda: i for i in range(0, 10))]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]If you instead fully evaluate the generator expression before calling any of the functions (for example, by passing it to the list constructor), you get the same behavior as the list comprehension case:
>>> [f() for f in list(lambda: i for i in range(0, 10))]
[9, 9, 9, 9, 9, 9, 9, 9, 9, 9]Side note: I think that commenters above didn't quite understand what I meant by "leaking", because there's more than one scope boundary here. Roughly speaking, any comprehension or loop can be desugared into something that looks like a C-style for-loop:
/* scope 1 */
for (/* scope 2 */) {
/* scope 3 */
}
Scope 1 is outside relative to the loop. Scope 2 is specific to the loop but shared by all its iterations. Scope 3 is specific to one loop iteration. The "leaking" I referred to above is from scope 3 to scope 2. I think other commenters took it to mean leaking from scope 2 to scope 1 - i.e. the ability to use the variable outside of the comprehension; that is, indeed, something that changed between Python 2 and 3.Bug:
var all []*Item
for _, item := range items {
all = append(all, &item)
}
Fix: var all []*Item
for _, item := range items {
item := item
all = append(all, &item)
}[1]: https://stackoverflow.com/questions/2295290/what-do-lambda-f...
[2]: https://docs.python.org/3/faq/programming.html#why-do-lambda...
In Go example the issue happens because item variable is per-loop, in your python example the issue is not related to loops at all, it's just because functions capture the value of global variables at execution time.
And the cherry on top is that the solution is also similar looking(i=i), but working with a different mechanic underneath(default argument assignment).
Anyway, this was my perspective that led me to interpret this as satire. A bit disappointed haha
def fun(initial_empty_list=[]): where initial_empty_list is a reference captured at function definition time, not a new value initialized on each call to the function.
Edit: looks like Rust does it right: https://play.rust-lang.org/?version=stable&mode=debug&editio...
fn main() {
let mut functions = vec![];
let mut i = 0;
functions.push(Box::new(move || println!("Hello {i}")) as Box<dyn Fn()>);
i = 1;
functions.push(Box::new(move || println!("Hello {i}")));
i = 2;
functions.push(Box::new(move || println!("Hello {i}")));
for func in functions {
func();
}
}
But the most equivalent code would be the following which doesn't compile: fn main() {
let mut functions = vec![];
for i in 0..10 {
functions.push(Box::new(|| println!("Hello {i}")))
}
for func in functions {
func();
}
}It would be better if Python could support block-scoping too. (Maybe it is time for inventing a "strict mode" for Python)
by requiring captured variables to be final it removes a lot of ambiguity around what a variable name refers to. If you do want crosstalk between the inner and outer scopes you have to be more explicit and introduce a reference to talk through.
I love python, but I basically avoid this construction and use a single element list if I need it. I can never remember exactly how it works.