# Python: leaking in `for` loop and lazy evaluation written by [@marc_lelarge](https://twitter.com/marc_lelarge) used to check python prerequisites of the deep learning course [dataflowr](https://dataflowr.github.io/website/). Before reading this post, you should start with the [quiz](https://dataflowr.github.io/quiz/python.html) ![](https://i.imgur.com/ac9rBUk.jpg) If you are not familiar with binding (i.e. assignment) and mutation of variables in Python you can have a look at [Binding vs Mutation](http://web.stanford.edu/class/archive/cs/cs106a/cs106a.1212/handouts/mutation.html). ## 1.0 Basics on scope Before looking at the leaking problem, we illustrate basic properties of scope in python: ```python= def print_11(): x = 11 # new local variable inside the scope of print_11 print(x) # will print the local variable x = 11 def print_x_outside(): print(x) # will print the global variable x (not defined yet) x = 10 # global variable x = 10 def main(): def print_x_inside(): print(x) # will print the variable x in the scope of main (not defined yet) x = 11 # variable x = 11 in the scope of main print_11() # print 11 print_x_inside() # print 11 x = 12 # new binding for the variable x print_x_outside() # print 10 print_11() # print 11 print_x_inside() # print 12 main() ``` ## 1.1 Scope and binding (i.e. assignment) Assume, we want to print the old value of `x` and then modify it. The following code will produce an error: ```python= def main(): def print_modify(new_value): print(x) x = new_value x = 10 print_modify(11) main() ``` The last statement in `print_modify` assigns a new value to `x`, the compiler recognizes it as a local variable. Consequently when the earlier `print(x)` attempts to print the uninitialized local variable and an error results, see [Why am I getting an UnboundLocalError when the variable has a value?](https://docs.python.org/3/faq/programming.html#why-am-i-getting-an-unboundlocalerror-when-the-variable-has-a-value) Probably the easiest way to do this: ```python= def main(): def easy_print_modify(x,new_value): print(x) return new_value x = 10 x = easy_print_modify(x,11) print(x) main() ``` or you can use [`nonlocal`](https://docs.python.org/3/reference/simple_stmts.html#the-nonlocal-statement) : ```python= def main(): def other_print_modify(new_value): nonlocal x print(x) x = new_value x = 10 other_print_modify(11) print(x) main() ``` ## 1.2 Scope and mutation ```python= def print_12(): x = [1, 2] print(x) def print_x_outside(): x[0] = 3 print(x) x = ['a', 'b'] def main(): def print_x_inside(): x[1] = 'b' print(x) x = [1, 2] print_x_inside() # print [1, 'b'] print(x) # print [1, 'b'] print_12() # print [1, 2] x = [1,2,3] print_x_outside() # print [3, 'b'] print_x_inside() # print [1, 'b', 3] print(x) # print [1, 'b', 3] print_12() # print [1, 2] main() print(x) # print [3, 'b'] ``` ## 2.1 Leaking in `for` loop basic example: even the variable `_` becomes a variable accessible outside the `for` loop! ```python= for _ in range(10): foo = 2 print(_, foo) # print 9 2 ``` confusing variables: ```python= i = 20 list_a = [] for i in range(10): list_a.append(2*i) print(i) # print 9 - leaking - ``` ## 2.2 List comprehension to avoid leaking ```python i = 20 litst_b = [2*i for i in range(10)] print(i) # print 20 - no leaking - ``` **Takeaways:** List comprehensions have their own scope, loops don't. ## 3.1 Lazy evaluation Variables in functions are evaluated only when called: ```python= list_fun = [] for i in range(10): list_fun.append(lambda: i) for f in list_fun: print(f()) # print 9 ten times print(i) # print 9 i = 20 for f in list_fun: print(f()) # print 20 ten times ``` Here `i` is not local to the lambdas but is defined in the outer scope, and it is accessed when the lambda is called — not when it is defined. At the end of the loop, the value of `i` is `9`, so all the functions now return `9`. See also [Why do lambdas defined in a loop with different values all return the same result?](https://docs.python.org/3/faq/programming.html#why-do-lambdas-defined-in-a-loop-with-different-values-all-return-the-same-result) With list comprehension, there is a local scope (as seen above) so now `i` is local but since the function is not called at creation, the local variable `i` is not evaluated only when it is called but then `i=9`. ```python= list_fun_comp = [lambda: i for i in range(10)] # variable i is in the local scope for f in list_fun_comp: print(f()) # print 9 ten times i = 20 # this variable is in the global scope for f in list_fun_comp: print(f()) # print 9 ten times ``` We now present several ways to solve the problem: ## 3.2 hack Using an argument with a default value thanks to `i=i` below, will creates a new variable `i` local to the lambda and computed when the lambda is defined: ```python= list_fun_hack = [lambda i=i: i for i in range(10)] for f in list_fun_hack: print(f()) # print 0 1 2 3 4 5 6 7 8 9 ``` ## 3.3 currying In mathematics and computer science, [currying](https://en.wikipedia.org/wiki/Currying) is the technique of converting a function that takes multiple arguments into a sequence of functions that each takes a single argument. Here this is very simple since $f(i,y) =i$ we can rewrite it as $f(i,y) = h(i)(y)$ with $h(i) = (y\to i)$, i.e. $h(i)$ is the constant function equal to $i$. Note that $h: \mathbb{R} \mapsto \mathbb{R}^\mathbb{R}$ below is called with variable `i` in the `for` loop, the local variable `i` is evaluated: ```python= list_fun_curry = [(lambda x :(lambda: x))(i) for i in range(10)] for f in list_fun_curry: print(f()) # print 0 1 2 3 4 5 6 7 8 9 ``` ## 3.4 lazy generator Variables used in the generator expression are evaluated lazily see [Generator expressions](https://docs.python.org/3.10/reference/expressions.html#generator-expressions) so you can use a generator: ```python= def fun_gen(n): i = 0 while i < n: yield (lambda: i) i += 1 for f in fun_gen(10): print(f()) # print 0 1 2 3 4 5 6 7 8 9 ``` But this code will not work as intended: ```python= for f in list(fun_gen(10)): print(f()) # print 10 ten times ``` ## More references [Python Oddities Explained](https://treyhunner.com/python-oddities/#/) by Trey Hunner [Common Gotchas](https://docs.python-guide.org/writing/gotchas/) from The Hitchhiker’s Guide to Python! Back to the deep learning course [dataflowr](https://dataflowr.github.io/website/) ###### tags: `public` `python` `dataflowr`