I defined the following factorial function using recursion :
def factorial(n):
if n == 0:
return n
else:
return n*factorial(n-1)
Then, I defined the following function to return the least value for which a RecursionError is raised when calling the factorial
function. (I already imported the sys
module for this)
def test_recursion(n):
sys.setrecursionlimit(n)
num = 0
while True:
try:
x = factorial(num)
num += 1
except:
return num
I'm aware that this function is a bit misleading as calling the test_recursion
function itself takes up one slot in the call stack.
Now, if I create the list l1
as follows :
l1 = []
for i in [10,11,12]:
l1.append(test_recursion(i))
l2
is supposed to be the equivalent list, made by using list comprehension
l2 = [test_recursion(i) for i in [10,11,12]]
Now, print(l1, l2, sep = "\n"
gives the following output :
[7,8,9]
[6,7,8]
Why does this happen? This happens with dictionary and set comprehensions as well.
Is it that comprehensions call some function in the background, which takes up a slot in the stack? How does the internal implementation of comprehension explain this?
Yes. Under Python 3.10.13, with these functions:
def test1():
l1 = []
for i in [10,11,12]:
l1.append(test_recursion(i))
return l1
def test2():
l2 = [test_recursion(i) for i in [10,11,12]]
return l2
Using the disassembler on the first snippet (dis.dis(test1)
):
20 0 BUILD_LIST 0
2 STORE_FAST 0 (l1)
21 4 LOAD_CONST 1 ((10, 11, 12))
6 GET_ITER
>> 8 FOR_ITER 9 (to 28)
10 STORE_FAST 1 (i)
22 12 LOAD_FAST 0 (l1)
14 LOAD_METHOD 0 (append)
16 LOAD_GLOBAL 1 (test_recursion)
18 LOAD_FAST 1 (i)
20 CALL_FUNCTION 1
22 CALL_METHOD 1
24 POP_TOP
26 JUMP_ABSOLUTE 4 (to 8)
23 >> 28 LOAD_FAST 0 (l1)
30 RETURN_VALUE
And on the second snippet:
26 0 LOAD_CONST 1 (<code object <listcomp> at 0x1048a0d40, file "/Users/amadan/tmp/a.py", line 26>)
2 LOAD_CONST 2 ('test2.<locals>.<listcomp>')
4 MAKE_FUNCTION 0
6 LOAD_CONST 3 ((10, 11, 12))
8 GET_ITER
10 CALL_FUNCTION 1
12 STORE_FAST 0 (l2)
27 14 LOAD_FAST 0 (l2)
16 RETURN_VALUE
Disassembly of <code object <listcomp> at 0x1048a0d40, file "/Users/amadan/tmp/a.py", line 26>:
26 0 BUILD_LIST 0
2 LOAD_FAST 0 (.0)
>> 4 FOR_ITER 6 (to 18)
6 STORE_FAST 1 (i)
8 LOAD_GLOBAL 0 (test_recursion)
10 LOAD_FAST 1 (i)
12 CALL_FUNCTION 1
14 LIST_APPEND 2
16 JUMP_ABSOLUTE 2 (to 4)
>> 18 RETURN_VALUE
So you can see that the comprehension is treated as a function. I believe its purpose is to isolate the variables created inside the comprehension in their own scope. For example:
x = "original value"
[x for x in ["new value"]]
print(x) # original value
vs
x = "original value"
for x in ["new value"]:
pass
print(x) # new value