Skip to content

Commit de7e611

Browse files
committedApr 4, 2025·
Rewrite documentation
1 parent b580550 commit de7e611

File tree

6 files changed

+41
-49
lines changed

6 files changed

+41
-49
lines changed
 

‎python/ql/src/Variables/LoopVariableCapture/LoopVariableCapture.py

-18
This file was deleted.

‎python/ql/src/Variables/LoopVariableCapture/LoopVariableCapture.qhelp

+15-31
Original file line numberDiff line numberDiff line change
@@ -5,56 +5,40 @@
55

66
<overview>
77
<p>
8-
Nested functions are a useful feature of Python as it allows a function to access the variables of its enclosing function.
9-
However, the programmer needs to be aware that when an inner function accesses a variable in an outer scope,
10-
it is the variable that is captured, not the value of that variable.
8+
In Python, a nested function or lambda expression that captures a variable from its surrounding scope is a <em>late-binding</em> closure,
9+
meaning that the value of the variable is determined when the closure is called, not when it is created.
1110
</p>
1211
<p>
13-
Therefore, care must be taken when the captured variable is a loop variable, since it is the loop <em>variable</em> and
14-
<em>not</em> the <em>value</em> of that variable that is captured.
15-
This will mean that by the time that the inner function executes,
16-
the loop variable will have its final value, not the value when the inner function was created.
12+
Care must be taken when the captured variable is a loop variable. If the closure is called after the loop ends, it will use the value of the variable on the last iteration of the loop, rather than the value at the iteration at which it was created.
1713
</p>
1814

1915
</overview>
2016
<recommendation>
2117
<p>
22-
The simplest way to fix this problem is to add a local variable of the same name as the outer variable and initialize that
23-
using the outer variable as a default.
24-
<code>
25-
for var in seq:
26-
...
27-
def inner_func(arg):
28-
...
29-
use(var)
30-
</code>
31-
becomes
32-
<code>
33-
for var in seq:
34-
...
35-
def inner_func(arg, var=var):
36-
...
37-
use(var)
38-
</code>
18+
Ensure that closures that capture loop variables aren't used outside of a single iteration of the loop.
19+
To capture the value of a loop variable at the time the closure is created, use a default parameter, or <code>functools.partial</code>.
3920
</p>
4021

4122
</recommendation>
4223
<example>
4324
<p>
44-
In this example, a list of functions is created which should each increment its argument by its index in the list.
45-
However, since <code>i</code> will be 9 when the functions execute, they will each increment their argument by 9.
25+
In the following (BAD) example, a `tasks` list is created, but each task captures the loop variable <code>i</code>, and reads the same value when run.
4626
</p>
47-
<sample src="LoopVariableCapture.py" />
27+
<sample src="examples/bad.py" />
4828
<p>
49-
This can be fixed by adding the default value as shown below. The default value is computed when the function is created, so the desired effect is achieved.
29+
In the following (GOOD) example, each closure has an `i` default parameter, shadowing the outer <code>i</code> variable, the default value of which is determined as the value of the loop variable <code>i</code> at the time the closure is created.
30+
<sample src="examples/good.py" />
31+
In the following (GOOD) example, <code>functools.partial</code> is used to partially evaluate the lambda expression with the value of <code>i</code>.
32+
<sample src="examples/good2.py" />
5033
</p>
5134

52-
<sample src="LoopVariableCapture2.py" />
5335

5436
</example>
5537
<references>
56-
<li>The Hitchhiker’s Guide to Python: <a href="http://docs.python-guide.org/en/latest/writing/gotchas/#late-binding-closures">Late Binding Closures</a></li>
57-
<li>Python Language Reference: <a href="https://docs.python.org/reference/executionmodel.html#naming-and-binding">Naming and binding</a></li>
38+
<li>The Hitchhiker's Guide to Python: <a href="http://docs.python-guide.org/en/latest/writing/gotchas/#late-binding-closures">Late Binding Closures</a>.</li>
39+
<li>Python Language Reference: <a href="https://docs.python.org/reference/executionmodel.html#naming-and-binding">Naming and binding</a>.</li>
40+
<li>Stack Overflow: <a href="https://stackoverflow.com/questions/3431676/creating-functions-or-lambdas-in-a-loop-or-comprehension">Creating functions (or lambdas) in a loop (or comprehension)</a>.</li>
41+
<li>Python Language Reference: <a href="https://docs.python.org/3/library/functools.html#functools.partial">functools.partial</a>.</li>
5842

5943
</references>
6044
</qhelp>

‎python/ql/src/Variables/LoopVariableCapture/LoopVariableCapture.ql

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* @description Capture of a loop variable is not the same as capturing the value of a loop variable, and may be erroneous.
44
* @kind path-problem
55
* @tags correctness
6+
* quality
67
* @problem.severity error
78
* @sub-severity low
89
* @precision high
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# BAD: The loop variable `i` is captured.
2+
tasks = []
3+
for i in range(5):
4+
tasks.append(lambda: print(i))
5+
6+
# This will print `4,4,4,4,4`, rather than `0,1,2,3,4` as likely intended.
7+
for t in tasks:
8+
t()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# GOOD: A default parameter is used, so the variable `i` is not being captured.
2+
tasks = []
3+
for i in range(5):
4+
tasks.append(lambda i=i: print(i))
5+
6+
# This will print `0,1,2,3,4``.
7+
for t in tasks:
8+
t()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import functools
2+
# GOOD: A default parameter is used, so the variable `i` is not being captured.
3+
tasks = []
4+
for i in range(5):
5+
tasks.append(functools.partial(lambda i: print(i), i))
6+
7+
# This will print `0,1,2,3,4``.
8+
for t in tasks:
9+
t()

0 commit comments

Comments
 (0)
Please sign in to comment.