11< p > Nested functions and lambdas can reference variables defined in enclosing scopes. This can create tricky bugs when the variable and the function
2- are defined in a loop. If the function is called after the loop, it will see the variables last value instead of seeing the values corresponding to
3- the iteration where the function was defined.</ p >
4- < p > This rule raises an issue when a nested function or lambda references a variable defined in an enclosing loop.</ p >
2+ are defined in a loop. If the function is called in another iteration or after the loop finishes, it will see the variables' last value instead of
3+ seeing the values corresponding to the iteration where the function was defined.</ p >
4+ < p > Capturing loop variables might work for some time but:</ p >
5+ < ul >
6+ < li > it makes the code difficult to understand. </ li >
7+ < li > it increases the risk of introducing a bug when the code is refactored or when dependencies are updated. See an example with the builtin "map"
8+ below. </ li >
9+ </ ul >
10+ < p > One solution is to add a parameter to the function/lambda and use the previously captured variable as its default value. Default values are only
11+ executed once, when the function is defined, which means that the parameter's value will remain the same even when the variable is reassigned in
12+ following iterations.</ p >
13+ < p > Another solution is to pass the variable as an argument to the function/lambda when it is called.</ p >
14+ < p > This rule raises an issue when a function or lambda references a variable defined in an enclosing loop.</ p >
515< h2 > Noncompliant Code Example</ h2 >
616< pre >
717def run():
@@ -12,6 +22,20 @@ <h2>Noncompliant Code Example</h2>
1222 def func():
1323 return i # Noncompliant
1424 mylist.append(func)
25+
26+ def example_of_api_change():
27+ """"
28+ Passing loop variable as default values also makes sure that the code is future-proof.
29+ For example the following code will work as intended with python 2 but not python 3.
30+ Why? because "map" behavior changed. It now returns an iterator and only executes
31+ the lambda when required. The same is true for other functions such as "filter".
32+ """
33+ lst = []
34+ for i in range(5):
35+ lst.append(map(lambda x: x + i, range(3))) # Noncompliant
36+ for sublist in lst:
37+ # prints [4, 5, 6] x 4 with python 3, with python 2 it prints [0, 1, 2], [1, 2, 3], ...
38+ print(list(sublist))
1539</ pre >
1640< h2 > Compliant Solution</ h2 >
1741< pre >
@@ -23,21 +47,33 @@ <h2>Compliant Solution</h2>
2347 def func(i=i): # same for nested functions
2448 return i
2549 mylist.append(func)
50+
51+ def example_of_api_change():
52+ """"
53+ This will work for both python 2 and python 3.
54+ """
55+ lst = []
56+ for i in range(5):
57+ lst.append(map(lambda x, value=i: x + value, range(3))) # Passing "i" as a default value
58+ for sublist in lst:
59+ print(list(sublist))
2660</ pre >
2761< h2 > Exceptions</ h2 >
28- < p > No issue will be raised if the function or lambda is only called in the same loop.</ p >
62+ < p > No issue will be raised if the function or lambda is directly called in the same loop. This still makes the design difficult to understand but it
63+ is less error prone.</ p >
2964< pre >
3065def function_called_in_loop():
3166 for i in range(10):
32- print((lambda param: param * i)(42))
67+ print((lambda param: param * i)(42)) # Calling the lambda directly
3368
3469 def func(param):
3570 return param * i
3671
37- print(func(42))
72+ print(func(42)) # Calling "func" directly
3873</ pre >
3974< h2 > See</ h2 >
4075< ul >
4176 < li > < a href ="https://docs.python-guide.org/writing/gotchas/#mutable-default-arguments "> The Hitchhiker's Guide to Python - Common Gotchas</ a > </ li >
77+ < li > Python documentation - < a href ="https://docs.python.org/3/reference/compound_stmts.html#function-definitions "> Function definitions</ a > </ li >
4278</ ul >
4379
0 commit comments