5

I have this Python 3 pseudo-code:

def f1():
    a, b, c, d, e, f = some_other_fn()
    if (condition):
        f2(a, b, c, d, e, f)

def f2(a, b, c, d, e, f):
    complex_set_of_operations_with(a, b, c, d, e, f)

for i in range(1000):
    f(1)

Now, I am kind of annoyed at the long signature and repetition in f2() and would like to encapsulate that into f1():

def f1():
    def f2():
        complex_set_of_operations_with(a, b, c, d, e, f)

    a, b, c, d, e, f = some_other_fn()
    if (condition):
        f2()

for i in range(1000):
    f(1)

Now, my question is: if I run f1() a thousand times, does the interpreter have to parse f2() a thousand times or is it smart enough to create a reusable reference?

Al Fahad
  • 2,378
  • 5
  • 28
  • 37
user3758232
  • 758
  • 5
  • 19
  • why not `def f2(*args): complex_set_of_operations_with(*args)` – Chris_Rands Feb 02 '18 at 15:29
  • Do you call `f2` recursively, or in more than one place in `f1`? If not, you could just drop `f2` entirely and inline it's code into `f1`. – tobias_k Feb 02 '18 at 15:35
  • @Chris_Rands: I have to use `a`, `b`, `c`, `d`, `e`, and `f` individually different places within `f2()` and I find it more readable to have them declared explicitly. – user3758232 Feb 02 '18 at 16:51
  • @tobias_k I call it only in `f1` that is also why I want to encapsulate it. I could inline it, but I like to keep the operational code separate from the conditions that trigger it. It's mostly a style and readability question—and admittedly, with some academic curiosity added. – user3758232 Feb 02 '18 at 16:53

2 Answers2

5

Let's have a look (using Python 3.5 that I happen to have at hand). We will use the dis module to disassemble the function and inspect its bytecode:

>>> def f1():
...     def f2():
...         complex_set_of_operations_with(a, b, c, d, e, f)
...     a, b, c, d, e, f = some_other_fn()
...     if (condition):
...         f2()
... 
>>> import dis
>>> dis.dis(f1)
  2           0 LOAD_CLOSURE             0 (a)
              3 LOAD_CLOSURE             1 (b)
              6 LOAD_CLOSURE             2 (c)
              9 LOAD_CLOSURE             3 (d)
             12 LOAD_CLOSURE             4 (e)
             15 LOAD_CLOSURE             5 (f)
             18 BUILD_TUPLE              6
             21 LOAD_CONST               1 (<code object f2 at 0x7f5d58589e40, file "<stdin>", line 2>)
             24 LOAD_CONST               2 ('f1.<locals>.f2')
             27 MAKE_CLOSURE             0
             30 STORE_FAST               0 (f2)
             ...  # the rest is omitted for brevity

In runtime, the Python interpreter interprets these primitive bytecode instructions one by one. These instructions are explained in the documentation.

As the last four instructions from the example above suggest, Python indeed builds the internal function every time (and stores it under the name f2), but it appears to do so efficiently by loading a pre-compiled constant code object of f2 (the 21 LOAD_CONST) line, i.e. it does not compile the f2's body over and over again.

plamut
  • 3,085
  • 10
  • 29
  • 40
-2

Python evaluation is lazy. It will only be evaluated when it's actually needed.

https://swizec.com/blog/python-and-lazy-evaluation/swizec/5148

Lazy evaluation python

Nickpick
  • 6,163
  • 16
  • 65
  • 116
  • 3
    Does this answer the question? OP was not asking whether `f2` will be evaluated before `f1` is being run, but whether it's evaluated _again_ each time `f1` runs. Also, both the blog post and the linked answer are about generators, which is something entirely different. – tobias_k Feb 02 '18 at 15:33