Does the Python compiler recognize constants defined within a function, such that it only computes their values once regardless of how many times the function is subsequently called in the code?
For example,
def f():
x = [ 1, 2, 3, 4 ]
# stuff
for i in range( 100 ):
f()
Would x
be recalculated the 100 times f()
is called?
It is not always possible to define constants outside the function that uses them, and I'm curious if Python has got your back in these situations.
(Note that this applies to CPython, and may be different in other implementations)
Python code is parsed and compiled into bytecode. You can see the instructions used with the dis
module.
>>> def f(x):
... x = [1, 2, 3, 4]
>>> dis.dis(f)
2 0 LOAD_CONST 1 (1)
2 LOAD_CONST 2 (2)
4 LOAD_CONST 3 (3)
6 LOAD_CONST 4 (4)
8 BUILD_LIST 4
10 STORE_FAST 0 (x)
12 LOAD_CONST 0 (None)
14 RETURN_VALUE
>>> print(dis.Bytecode(f).info())
Name: f
Filename: <stdin>
Argument count: 1
Kw-only arguments: 0
Number of locals: 1
Stack size: 4
Flags: OPTIMIZED, NEWLOCALS, NOFREE
Constants:
0: None
1: 1
2: 2
3: 3
4: 4
Variable names:
0: x
As you can see, integer literals are constants, but lists have to be built everytime.
This is a relatively fast operation (Probably even quicker than looking up a global, but the time is still negligible)
If you had a function g
that used a tuple instead, it is loaded as a constant:
>>> def g(x):
... x = (1, 2, 3, 4)
>>> dis.dis(g)
2 0 LOAD_CONST 5 ((1, 2, 3, 4))
2 STORE_FAST 0 (x)
4 LOAD_CONST 0 (None)
6 RETURN_VALUE
>>> print(dis.Bytecode(g).info())
Name: g
Filename: <stdin>
Argument count: 1
Kw-only arguments: 0
Number of locals: 1
Stack size: 4
Flags: OPTIMIZED, NEWLOCALS, NOFREE
Constants:
0: None
1: 1
2: 2
3: 3
4: 4
5: (1, 2, 3, 4)
Variable names:
0: x
But this seems like a case of premature optimisation.
The constants stored for a function can be found as function.__code__.co_consts
.
>>> g.__code__.co_consts
(None, 1, 2, 3, 4, (1, 2, 3, 4))
The reason a new list has to be built every time is so that if the list is changed, it won't affect a list that is loaded everytime.
And the tuple optimisation goes away if it isn't a list of constants.
>>> def h(x):
... x = (1, 2, 3, x)
>>> dis.dis(h)
2 0 LOAD_CONST 1 (1)
2 LOAD_CONST 2 (2)
4 LOAD_CONST 3 (3)
6 LOAD_FAST 0 (x)
8 BUILD_TUPLE 4
10 STORE_FAST 0 (x)
12 LOAD_CONST 0 (None)
14 RETURN_VALUE
>>> print(dis.Bytecode(h).info())
Name: h
Filename: <stdin>
Argument count: 1
Kw-only arguments: 0
Number of locals: 1
Stack size: 4
Flags: OPTIMIZED, NEWLOCALS, NOFREE
Constants:
0: None
1: 1
2: 2
3: 3
Variable names:
0: x
Short answer: for lists, it does not.
If we check the intermediate code after compilation with dis
, we see:
>>> dis.dis(f)
2 0 LOAD_CONST 1 (1)
3 LOAD_CONST 2 (2)
6 LOAD_CONST 3 (3)
9 LOAD_CONST 4 (4)
12 BUILD_LIST 4
15 STORE_FAST 0 (x)
18 LOAD_CONST 0 (None)
21 RETURN_VALUE
So as you can see the program first loads constants 1
to 4
and pushes these on the stack, and the constructs a list with these constants, so that means it constructs a list each time.
In case the list is not mutated, I propose to define the constant outside the function:
some_constant = [1, 2, 3, 4]
def f():
# use some_constant
# ...
pass
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With