Scopes and Nested Functions

The addition of nested function scopes made the variable lookup rules more complex:

  • A reference x looks for the name x first in the current local scope (function), second in the local scopes of the enclosing functions (inner to outer), third in the current global scope (the module file), and fourth in the built-in scope (Note: global declarations make the search begin in the global scope).
  • An assignment, x = 2 for example, creates or changes the name x in the local scope. If x is declared global within the function, the assignment creates or changes the name x in the enclosing module’s scope. If x is declared nonlocal within the function the assignment changes the name x in the closest enclosing function’s local scope (applies for Python3).

Example 1:

>>> x = 2
>>> def function1():
...     x = 3
...     def function2():
...             print(x)
...     function2()
... 
>>> function1()
3

The function2() refers to the x that is in the function1() function’s local scope. Functions can access names in all physically enclosing def statements, therefore the x in function2() is automatically mapped to the x in function1() (LEGB lookup rule).

A better code will be a function that makes and returns another function:

>>> def function1():
...     x = 2
...     def function2():
...             print(x)
...     return function2
... 
>>> todo = function1()
>>> todo()
2

Functions are objects in Python and can be passed back as return values from other functions. Moreover, the function2() remembers the enclosing scope’s x in function1(), although function1() is no longer active.

This is called “factory functions” or “closures” and are used when you need to generate event handlers on the fly in response to conditions at runtime.

>>> def M1(m):
...     def A1(a):
...             return a * m
...     return A1
... 
>>> b = M1(5)
>>> b
.A1 at 0x7feadd074b70>
>>>
>>> b(3)
15
>>> b(4)
20

The first part of the code defines an outer function that simply generates and returns a nested function,
without calling it.
In the second part of the code we call the outer function and we get back a reference to the generated nested function.
In the third part of the code we are calling the nested function that M1 created and passed back.

If we call the outer function again, we get back a new nested function with different state information attached.

>>> c = M1(6)
>>> c
.A1 at 0x7feadd730378>
>>> c(5)
30
>>> b(5)
25

It works this way because each call to a factory function gets its own set of state information. This technique is popular among programmers with backgrounds in functional programming languages.

Of course, you can avoid nesting functions within functions (flat is generally better than nested).

>>> def function1():
...     a = 3
...     function2(a)
... 
>>> def function2(a):
...     print(a)
... 
>>> function1()
3

However, factory functions are common enough in modern Python code, so using them is nothing but beneficial (especially when you start coding lambda expressions).

If a def defined within a function is nested inside a loop, and the nested function references an enclosing scope variable that is changed by that loop, all functions generated within the loop will have the same value.

>>> def Actions():
...     a = []
...     for i in range(3):
...             a.append(lambda x: i * x)
...     return a
... 
>>> a = Actions()
>>> a[0]
. at 0x7feadcfc2d90>
>>> a[1]
. at 0x7feadcfc2e18>
>>> a[2]
. at 0x7feadcfc2ea0>

This doesn’t work! When we pass an argument of 2 in each of the following calls, we get back 2 multiply by 2 for each function in the list, because i is the same in all of them, 2.

>>> a[0](2)
4
>>> a[1](2)
4
>>> a[2](2)
4

To solve the issue you must use defaults to save the variable’s current value.

>>> def Actions():
...     a = []
...     for i in range(3):
...             a.append(lambda x, i = i: i * 2)
...     return a
... 
>>> a = Actions()
>>> a[0](2)
0
>>> a[1](2)
2
>>> a[2](2)
4

Leave a Reply