Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python compiler and constants defined inside a function

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.

like image 285
Jet Blue Avatar asked Dec 14 '22 20:12

Jet Blue


2 Answers

(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
like image 73
Artyer Avatar answered Jan 03 '23 13:01

Artyer


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
like image 36
Willem Van Onsem Avatar answered Jan 03 '23 13:01

Willem Van Onsem