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?

4
  • why not def f2(*args): complex_set_of_operations_with(*args) Commented Feb 2, 2018 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. Commented Feb 2, 2018 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. Commented Feb 2, 2018 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. Commented Feb 2, 2018 at 16:53

2 Answers 2

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.

Sign up to request clarification or add additional context in comments.

1 Comment

Excellent insight. Thanks. I am also using 3.5 by the way, so this fits like a glove.
-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

1 Comment

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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.