Are inner functions redefined every time their parent function is called?
Asked Answered
B

1

11

I know functions in Python are 1st class citizens, meaning that they are objects of the function class similar to how 5 is an object of the int class. This means that at some point in their life-time a constructor is called. For most functions I would expect this to happen when they are defined (as most functions are presumably defined only once) so that we only pay the construction price once no matter how many times we use it.

But what about nested functions? Those are redefined every time their parent is called. Does this mean we re-construct the object every time? If yes, isn't that grossly inefficient? Wouldn't a private method (assuming the parent function is a method) or another function be much more efficient? I am ignoring scoping arguments in favour of nesting for this discussion.

I run a simple experiment that seems to support my aforementioned argument as the inner function edition is slower:

import time

def f(x, y):
    def defined_inside(x, y):
        return x + y
    return defined_inside(x, y)

def defined_outside(x, y):
    return x + y

def g(x, y):
    return defined_outside(x, y)

start = time.time()
for i in range(10000000):
    _ = f(3, 4)
end = time.time()

print("Using inner function it took {} seconds".format(end - start))

start = time.time()
for i in range(10000000):
    _ = g(3, 4)
end = time.time()

print("Using outer function it took {} seconds".format(end - start))

Results:

Using inner function it took 2.494696855545044 seconds
Using outer function it took 1.8862690925598145 seconds

Bonus question: In case the above is true, how does the situation relate to compiled languages, such as Scala? I grew into a huge fun of nesting and higher order functions, it would be terrible if the trick is as inefficient as it seems.

Beautifully answered 3/4, 2020 at 14:46 Comment(3)
If you are nesting a function you generally want it to create a new object. For example, you would often do this to capture a closure from the parent. That only works if each inner function is created fresh.Uncertainty
Yes, they are recreated each time.Syzran
@Uncertainty capturing closures does not require recreating an object, but only to store an execution context with different bindings, and that is what is used in other languages.Maximinamaximize
C
1

You can easily test this with decorators:

from collections.abc import Callable

def wrapper(fun: Callable[[], None]) -> Callable[[], None]:
    print(f"wrap {fun}")

    def wrap() -> None:
        print(f"calling {fun}")
        fun()

    return wrap

def foo(a: int) -> None:

    @wrapper
    def inner() -> None:
        print(f"inner {a}")

    inner()

foo(0)
foo(1)

The output will be:

wrap <function foo.<locals>.inner at 0x1051fbe20>
calling <function foo.<locals>.inner at 0x1051fbe20>
inner 0
wrap <function foo.<locals>.inner at 0x1051fbec0>
calling <function foo.<locals>.inner at 0x1051fbec0>
inner 1

Every time foo is called the function inner is defined anew (above, each version has its own address). This makes sense as you might want to capture local variables (e.g., a) etc. in the inner function. However, not capturing any variable does not change Python's behavior here. The function gets created twice even if a is not used inside inner. If you don't want this behavior you can define the inner function at the top level and add all closure variables as additional function arguments. This guarantees that the function is created only once:

from typing import Any
from collections.abc import Callable

def wrapper(fun: Callable[[], None]) -> Callable[[], None]:
    print(f"wrap {fun}")

    def wrap(*args: Any) -> None:
        print(f"calling {fun}")
        fun(*args)

    return wrap

@wrapper
def inner(a: int) -> None:  # <-- we need to manually pass `a` every time
    print(f"inner {a}")

def foo(a: int) -> None:
    inner(a)

foo(0)
foo(1)

Here the output is:

wrap <function inner at 0x101558860>
calling <function inner at 0x101558860>
inner 0
calling <function inner at 0x101558860>
inner 1

Notice how the function pointer is the same every time.

In case of other languages, like Scala, it is actually not that expensive to construct the function object. You statically compile the function with an extra hidden "argument" that provides the variables captured by the closure. When the inner function is constructed at runtime the function object stores the closure variables (i.e., the context) and the static function pointer.

Cartierbresson answered 29/6 at 3:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.