Reputation: 766
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
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.
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