Overhead of (was Reasoning behind) nested scope

Peter Otten __peter__ at web.de
Wed Aug 4 08:44:10 CEST 2004


Nigel Rowe wrote:

> Peter Otten wrote:
> 
>> Andy Baker wrote:
>> 
>>> (On a side note is there any way to call a nested function from outside
>>> the parent? I was kind of expecting nested functions to be addressable
>>> through dot notation like methods are but I can see why that wouldn't be
>>> quite right. This might be a better question for the tutor list...)
>> 
>> When you are nesting functions, you don't get one function sitting inside
>> another function. Instead, the function creation code of the "inner"
>> function is executed every time to the effect that you get a new inner
>> function every time you call the outer one:
>> 
>>>>> def make():
>> ...     def f(): return "shoobidoo"
>> ...     return f
>> ...
>>>>> f1 = make()
>>>>> f2 = make()
>>>>> f1(), f2()
>> ('shoobidoo', 'shoobidoo')
>>>>> f1 is f2
>> False
>> 
>> You wouldn't do that in cases like the above, when you get nothing in
>> return for the extra overhead, but somtimes nesting is useful - because
>> of the change in the scoping rules you now get readonly-closures:
>> 
>>>>> def make(s):
>> ...     def f(): return s
>> ...     return f
>> ...
>>>>> f1 = make("now")
>>>>> f2 = make("what")
>>>>> f1(), f2()
>> ('now', 'what')
>> 
>> Peter
> 
> What work is actually done when the
>         'nested function creation code of the "inner" function'
> is executed?
> 
> Given a quick test:-
> <code>
> def outer():
>     def inner():
>         pass
>     return inner
> 
> b1 = outer()
> b2 = outer()
> 
> attrs=[a for a in dir(b1) if not a.startswith('_')]
> for a, a1, a2 in zip(attrs,
>                      [getattr(b1,a) for a in attrs],
>                      [getattr(b2,a) for a in attrs]):
>     print a, a1 is a2
> 
> </code>
> <result>
> func_closure True
> func_code True
> func_defaults True
> func_dict True
> func_doc True
> func_globals True
> func_name True
> </result>
> 
> it appears that all the components of the inner function are the same,
> which just leaves the binding of the code object to 'inner'.

Not the binding, a new function object is created every time outer is run,
i. e. (faked, but I did it for real above with f1 and f2)

>>> b1 is b2
False

> Am I missing something, or is the overhead no worse than, say,
> foo=self.foo, where self.foo is a method?

If it were pure Python it would rather be something like

foo = Function(func_closure=..., func_code=..., ...)

Another perspective is to look at the byte code:

>>> import dis
>>> def outer():
...     def inner(): pass
...     return inner
...
>>> dis.dis(outer)
  2           0 LOAD_CONST               1 (<code object inner at
0x4028eee0, file "<stdin>", line 2>)
              3 MAKE_FUNCTION            0
              6 STORE_FAST               0 (inner)

  3           9 LOAD_FAST                0 (inner)
             12 RETURN_VALUE
             13 LOAD_CONST               0 (None)
             16 RETURN_VALUE
>>> def inner():
...     pass
...
>>> def outer2():
...     return inner
...
>>> dis.dis(outer2)
  2           0 LOAD_GLOBAL              0 (inner)
              3 RETURN_VALUE
              4 LOAD_CONST               0 (None)
              7 RETURN_VALUE

In the example you get three extra instructions, LOAD_CONST, MAKE_FUNCTION
and STORE_FAST (and earn the slight benefit that inner is now a local
instead of a global). A constant code object is used, meaning compilation
takes place at most once when the module is loaded. There is a dedicated
op-code, so function creation should be faster than "normal" creation of a
class instance.

Now this is all nice and dandy, but how do the two contenders perform?

$ timeit.py -s"def inner(): pass" -s"def outer(): return inner" "outer()"
1000000 loops, best of 3: 0.469 usec per loop
$ timeit.py -s"def outer():" -s"  def inner(): pass" -s"  return inner"
"outer()"
1000000 loops, best of 3: 1.12 usec per loop

i. e. nesting the two functions roughly doubles execution time. 
However, creation of an inner function often will only take a small fraction
of the total time spent in the outer function - in the end it's just a
matter of style. 

I use inner functions only when they depend on the local context because I
think it nicely discriminates closures from helpers. I tend to omit
implicit parameters even for helper funtions, therefore nesting them would
gain me nothing.

Peter




More information about the Python-list mailing list