user2255757
user2255757

Reputation: 766

Python: Nested functions and variable scope

This code doesn't work:

def lol():
    i = 1
    def _lol():
        i += 1
    _lol()
lol()

Error:

local variable 'i' referenced before assignment

But, the following code works fine:

def lol():
    i = [1]
    def _lol():
        i[0] += 1
    _lol()
lol()

Why is that?

Upvotes: 1

Views: 129

Answers (1)

mgilson
mgilson

Reputation: 309929

Python scopes fit into 3 categories -- local, nonlocal and global. By default, a function can only change a reference in the local scope (references are created with the assignment operator).

You're free to mutate an object that you have a reference to which is why the second example works (i is a reference to the list [1], then you change/mutate it's first item). In short, you're mutating the object that i references, you're not trying to change the reference. Note that you can give a function access to change the reference in the global scope via the global keyword:

i = 1
def func():
  global i  # If you comment this out, i doesn't get changed in the global scope
  i = 2

func()
print(i)  # 2 -- 1 if the global statement is commented out.

Note that python3.x adds the nonlocal keyword. It does the same thing as global but to the non-local scope. e.g.

def foo():
    i = 1  # nonlocal to bar
    def bar():
        nonlocal i 
        print(i)
        i += 1
    return bar

bar1 = foo()
bar1()  # 1
bar1()  # 2
bar1()  # 3

bar2 = foo()
bar2()  # 1
bar2()  # 2

bar1()  # 4  bar2 doesn't influence bar1 at all.

augmented operators

This is a bit more advanced, but provided to hopefully help answer questions regarding operators like +=. Consider the case:

x = []
def func():
    x += [1]

You might expect this to work -- After all, x += [1] for a list x is really just x.extend([1]), right?. Unfortunately, it's not quite. We can disassemble func using dis.dis to see a little more what's going on.

>>> dis.dis(func)
  2           0 LOAD_FAST                0 (x)
              3 LOAD_CONST               1 (1)
              6 BUILD_LIST               1
              9 INPLACE_ADD         
             10 STORE_FAST               0 (x)       ### IMPORTANT!
             13 LOAD_CONST               0 (None)
             16 RETURN_VALUE        

Notice the byte-code instruction STORE_FAST? That basically says, store the result of INPLACE_ADD in the name x in the local dictionary. In other words, you write:

x += [1]

but python executes1:

x = x.__iadd__([1])

Why? __iadd__ should operate in place so why does it need to rebind the name to __iadd__'s return value? The rebinding part is the problem -- i.e., this code would work:

x = []
def func():
    x.__iadd__([1])

The answer is because python has immutable objects and __iadd__ needs to work with them too. Because of this, __iadd__ can return an object other than "self". This ends up being incredibly useful. Consider i = 1; i += 1. This invocation only works because int.__iadd__ is allowed to return a new integer.

1Discussing this in even more depth is actually my all-time most upvoted answer on StackOverflow and can be found here

Upvotes: 4

Related Questions