127 lines
4.2 KiB
Plaintext
Raw Permalink Normal View History

== Why is this an issue?
2020-06-30 12:47:33 +02:00
Nested functions and lambdas can reference variables defined in enclosing scopes. This can create tricky bugs when the variable and the function 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 seeing the values corresponding to the iteration where the function was defined.
2021-02-02 15:02:10 +01:00
2020-06-30 12:47:33 +02:00
Capturing loop variables might work for some time but:
2020-06-30 12:47:33 +02:00
* it makes the code difficult to understand.
* 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" below.
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 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 following iterations.
2021-02-02 15:02:10 +01:00
2020-06-30 12:47:33 +02:00
Another solution is to pass the variable as an argument to the function/lambda when it is called.
2021-02-02 15:02:10 +01:00
2020-06-30 12:47:33 +02:00
This rule raises an issue when a function or lambda references a variable defined in an enclosing loop.
=== Noncompliant code example
2020-06-30 12:47:33 +02:00
2022-02-04 17:28:24 +01:00
[source,python]
2020-06-30 12:47:33 +02:00
----
def run():
mylist = []
for i in range(5):
mylist.append(lambda: i) # Noncompliant
def func():
return i # Noncompliant
mylist.append(func)
def example_of_api_change():
""""
Passing loop variable as default values also makes sure that the code is future-proof.
For example the following code will work as intended with python 2 but not python 3.
Why? because "map" behavior changed. It now returns an iterator and only executes
the lambda when required. The same is true for other functions such as "filter".
"""
lst = []
for i in range(5):
lst.append(map(lambda x: x + i, range(3))) # Noncompliant
for sublist in lst:
# prints [4, 5, 6] x 4 with python 3, with python 2 it prints [0, 1, 2], [1, 2, 3], ...
print(list(sublist))
----
=== Compliant solution
2020-06-30 12:47:33 +02:00
2022-02-04 17:28:24 +01:00
[source,python]
2020-06-30 12:47:33 +02:00
----
def run():
mylist = []
for i in range(5):
mylist.append(lambda i=i: i) # passing the variable as a parameter with a default value
def func(i=i): # same for nested functions
return i
mylist.append(func)
def example_of_api_change():
""""
This will work for both python 2 and python 3.
"""
lst = []
for i in range(5):
lst.append(map(lambda x, value=i: x + value, range(3))) # Passing "i" as a default value
for sublist in lst:
print(list(sublist))
----
=== Exceptions
2020-06-30 12:47:33 +02:00
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 is less error prone.
2021-02-02 15:02:10 +01:00
[source,python]
2020-06-30 12:47:33 +02:00
----
def function_called_in_loop():
for i in range(10):
print((lambda param: param * i)(42)) # Calling the lambda directly
def func(param):
return param * i
print(func(42)) # Calling "func" directly
----
== Resources
2020-06-30 12:47:33 +02:00
* https://docs.python-guide.org/writing/gotchas/#mutable-default-arguments[The Hitchhiker's Guide to Python - Common Gotchas]
* Python documentation - https://docs.python.org/3/reference/compound_stmts.html#function-definitions[Function definitions]
ifdef::env-github,rspecator-view[]
'''
== Implementation Specification
(visible only on this page)
=== Message
* If the function is NOT a lambda: "Add a parameter to function "FFF" and use variable "XXX" as its default value; The value of "XXX" value might change at the next loop iteration."
* If the function is a lambda: "Add a parameter to the parent lambda function and use variable "XXX" as its default value; The value of "XXX" value might change at the next loop iteration."
=== Highlighting
Primary: the first time the variable is used in the function/lambda
Secondaries:
* every assignment of the variable in the enclosing loop
** message: Assignment in the loop
* the nested function name:
** message: Function capturing this variable
* the "lambda" keyword
** message: Lambda capturing this variable
'''
== Comments And Links
(visible only on this page)
include::../comments-and-links.adoc[]
endif::env-github,rspecator-view[]