The dis
module can be used to inspect the internal python bytecode directly. Here are the two functions (modified slightly to be more self consistent), and the resulting bytecodes that are sent to the python interpreter after they are parsed:
def comprehension():
squares = [num for num in range(1,11) if num % 2 == 0]
return squares
def loop():
squares = []
for num in range(1,11):
if num % 2 == 0:
squares.append(num)
return squares
Then calling dis.dis(comprehension)
:
>>> dis(comprehension)
10 0 LOAD_CONST 1 (<code object <listcomp> at 0x0000021EB480BBE0, file "C:\Users\aaron\Documents\scripts\python\untitled2.py", line 10>)
2 LOAD_CONST 2 ('comprehension.<locals>.<listcomp>')
4 MAKE_FUNCTION 0
6 LOAD_GLOBAL 0 (range)
8 LOAD_CONST 3 (1)
10 LOAD_CONST 4 (11)
12 CALL_FUNCTION 2
14 GET_ITER
16 CALL_FUNCTION 1
18 STORE_FAST 0 (squares)
11 20 LOAD_FAST 0 (squares)
22 RETURN_VALUE
Disassembly of <code object <listcomp> at 0x0000021EB480BBE0, file "C:\Users\aaron\Documents\scripts\python\untitled2.py", line 10>:
10 0 BUILD_LIST 0
2 LOAD_FAST 0 (.0)
>> 4 FOR_ITER 20 (to 26)
6 STORE_FAST 1 (num)
8 LOAD_FAST 1 (num)
10 LOAD_CONST 0 (2)
12 BINARY_MODULO
14 LOAD_CONST 1 (0)
16 COMPARE_OP 2 (==)
18 POP_JUMP_IF_FALSE 4
20 LOAD_FAST 1 (num)
22 LIST_APPEND 2
24 JUMP_ABSOLUTE 4
>> 26 RETURN_VALUE
Interpreting this output takes a little practice, but the general flow is that the "list comprehension function" is created first (MAKE_FUNCTION
jumps down to 2nd "disassembly" block), which takes care of creating a new list BUILD_LIST
, iterating over an iterator FOR_ITER
, testing the conditional POP_JUMP_IF_FALSE
, and (when the conditional is true) directly appending to the list LIST_APPEND
. After creating this "comprehension function" we return to the main function to create the iterator which is then passed to the function we created. Finally the result gets stored to a variable and that variable is returned.
The loop
function is actually fewer bytecode instructions, but don't necessarily interpret that as being faster or better... some bytecode instructions are more complicated / slower than others. Only actual profiling can actually tell you the difference in speed.
>>> dis(loop)
14 0 BUILD_LIST 0
2 STORE_FAST 0 (squares)
15 4 LOAD_GLOBAL 0 (range)
6 LOAD_CONST 1 (1)
8 LOAD_CONST 2 (11)
10 CALL_FUNCTION 2
12 GET_ITER
>> 14 FOR_ITER 26 (to 42)
16 STORE_FAST 1 (num)
16 18 LOAD_FAST 1 (num)
20 LOAD_CONST 3 (2)
22 BINARY_MODULO
24 LOAD_CONST 4 (0)
26 COMPARE_OP 2 (==)
28 POP_JUMP_IF_FALSE 14
17 30 LOAD_FAST 0 (squares)
32 LOAD_METHOD 1 (append)
34 LOAD_FAST 1 (num)
36 CALL_METHOD 1
38 POP_TOP
40 JUMP_ABSOLUTE 14
18 >> 42 LOAD_FAST 0 (squares)
44 RETURN_VALUE
Following the path of this function is a little bit more straightforward, but the key difference is really that python has to look up the "append" method from "squares" before it can call it, because it doesn't inherently know that "squares" is a list, and therefore can't just call LIST_APPEND