Search code examples
pythonbytecodepython-internals

Puzzled with LOAD_FAST/STORE_FAST of python


When I wrote some code, I found a interesting thing:

def test():
  l = []
  for i in range(10):
    def f():pass
    print(f)
    #l.append(f)

test()

import dis
dis.dis(test)

The output is :

<function test.<locals>.f at 0x7f46c0b0d400>
<function test.<locals>.f at 0x7f46c0b0d488>
<function test.<locals>.f at 0x7f46c0b0d400>
<function test.<locals>.f at 0x7f46c0b0d488>
<function test.<locals>.f at 0x7f46c0b0d400>
<function test.<locals>.f at 0x7f46c0b0d488>
<function test.<locals>.f at 0x7f46c0b0d400>
<function test.<locals>.f at 0x7f46c0b0d488>
<function test.<locals>.f at 0x7f46c0b0d400>
<function test.<locals>.f at 0x7f46c0b0d488>
  6           0 BUILD_LIST               0
              3 STORE_FAST               0 (l)

  7           6 SETUP_LOOP              42 (to 51)
              9 LOAD_GLOBAL              0 (range)
             12 LOAD_CONST               1 (10)
             15 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             18 GET_ITER
        >>   19 FOR_ITER                28 (to 50)
             22 STORE_FAST               1 (i)

  8          25 LOAD_CONST               2 (<code object f at 0x7f46c0bd8420, file "ts.py", line 8>)
             28 LOAD_CONST               3 ('test.<locals>.f')
             31 MAKE_FUNCTION            0
             34 STORE_FAST               2 (f)

  9          37 LOAD_GLOBAL              1 (print)
             40 LOAD_FAST                2 (f)
             43 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             46 POP_TOP
             47 JUMP_ABSOLUTE           19
        >>   50 POP_BLOCK
        >>   51 LOAD_CONST               0 (None)
             54 RETURN_VALUE

when

def test():
  l = []
  for i in range(10):
    def f():pass
    print(f)
    l.append(f)

test()

import dis
dis.dis(test)

And the output is:

<function test.<locals>.f at 0x7ff88ffe0400>
<function test.<locals>.f at 0x7ff88ffe0488>
<function test.<locals>.f at 0x7ff88ffe0510>
<function test.<locals>.f at 0x7ff88ffe0598>
<function test.<locals>.f at 0x7ff88ffe0620>
<function test.<locals>.f at 0x7ff88ffe06a8>
<function test.<locals>.f at 0x7ff88ffe0730>
<function test.<locals>.f at 0x7ff88ffe07b8>
<function test.<locals>.f at 0x7ff88ffe0840>
<function test.<locals>.f at 0x7ff88ffe08c8>
  6           0 BUILD_LIST               0
              3 STORE_FAST               0 (l)

  7           6 SETUP_LOOP              55 (to 64)
              9 LOAD_GLOBAL              0 (range)
             12 LOAD_CONST               1 (10)
             15 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             18 GET_ITER
        >>   19 FOR_ITER                41 (to 63)
             22 STORE_FAST               1 (i)

  8          25 LOAD_CONST               2 (<code object f at 0x7ff8900ab420, file "ts.py", line 8>)
             28 LOAD_CONST               3 ('test.<locals>.f')
             31 MAKE_FUNCTION            0
             34 STORE_FAST               2 (f)

  9          37 LOAD_GLOBAL              1 (print)
             40 LOAD_FAST                2 (f)
             43 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             46 POP_TOP

 10          47 LOAD_FAST                0 (l)
             50 LOAD_ATTR                2 (append)
             53 LOAD_FAST                2 (f)
             56 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             59 POP_TOP
             60 JUMP_ABSOLUTE           19
        >>   63 POP_BLOCK
        >>   64 LOAD_CONST               0 (None)
             67 RETURN_VALUE

If STORE_FAST "cached" the f, why in the first code snippet, the address of f is alternant?

And in the second snippet, it has two LOAD_FAST , and the result is normal.

Is LOAD_FAST/STORE_FAST did some unknow things?


Solution

  • That's happening because in each alternate iteration the older function object after the re-declaration of the current f has no references left, so it is garbage collected and Python can re-use that memory space in next iteration. On the other hand in the second the list is referring to each function so they are never garbage collected.

    This is an implementation dependent thing, CPython's garbage collection is based on reference count. On PyPy the output is different:

    $ ~/pypy-2.4.0-linux64/bin# ./pypy 
    Python 2.7.8 (f5dcc2477b97, Sep 18 2014, 11:33:30)
    [PyPy 2.4.0 with GCC 4.6.3] on linux2
    Type "help", "copyright", "credits" or "license" for more information.
    >>>> def test():
    ....     for i in range(10):
    ....         def f(): pass
    ....         print f
    .... 
    >>>> test()
    <function f at 0x00007f055c77d5b0>
    <function f at 0x00007f055c77d628>
    <function f at 0x00007f055c77d6a0>
    <function f at 0x00007f055c77d718>
    <function f at 0x00007f055c77d790>
    <function f at 0x00007f055c77d808>
    <function f at 0x00007f055c77d880>
    <function f at 0x00007f055c77d8f8>
    <function f at 0x00007f055c77d970>
    <function f at 0x00007f055c77d9e8>