5

I would like to know details about why this doesn't work as expected:

def outer():
    mylist = []
    def inner():
        mylist += [1]

    inner()

outer()

Especially because mylist.__iadd__([1]) works fine.

agf
  • 171,228
  • 44
  • 289
  • 238
nh2
  • 24,526
  • 11
  • 79
  • 128

4 Answers4

9

The problem is that when you assign to a variable name inside a function, Python assumes you're trying to create a new local variable that will mask a similarly-named variable in outer scope. Since += has to get the value of mylist before it can modify it, it complains, because the local version of mylist isn't yet defined. MRAB's answer gives a clear explanation of the semantics.

On the other hand, when you do mylist.__iadd__([1]), you aren't assigning a new variable name inside the function. You're just using a built-in method to modify an already-assigned variable name. As long as you don't try to assign a new value to mylist, you won't have a problem. For the same reason, the line mylist[0] = 5 would also work inside inner if the definition of mylist in outer were mylist = [1].

Note however that if you do try to assign a new value to mylist anywhere in the function, mylist.__iadd__([1]) will indeed fail:

>>> outer()
>>> def outer():
...     mylist = []
...     def inner():
...         mylist.__iadd__([1])
...         mylist = []
...     inner()
... 
>>> outer()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in outer
  File "<stdin>", line 4, in inner
UnboundLocalError: local variable 'mylist' referenced before assignment

If you want to assign a new value to a variable from a containing scope, in 3.0+ you can use nonlocal, in just the way you'd use global to assign a new value to a variable in global scope. So instead of this:

>>> mylist = []
>>> def inner():
...     global mylist
...     mylist += [1]
... 
>>> inner()
>>> mylist
[1]

You do this:

def outer():
    mylist = []
    def inner():
        nonlocal mylist
        mylist += [1]
    inner()
    print(mylist)
outer()
Community
  • 1
  • 1
senderle
  • 145,869
  • 36
  • 209
  • 233
  • Thanks, that's very detailed. I did not know that Python tries `x = x.__iadd__(y)` (thanks @MRAB) and assumed it would just do `x.__iadd__(y)`. I also finall renamed the variable ;) – nh2 Jul 23 '11 at 00:00
6

If a name is bound (a variable is assigned) in a function then that name is treated as local unless it's declared global.

Thus in inner, mylist is local.

When you write x += y, at runtime Python will try:

x = x.__iadd__(y)

If that fails, Python then tries:

x = x.__add__(y)
MRAB
  • 20,356
  • 6
  • 40
  • 33
  • +1, the specific semantics are useful here. This also shows that `__iadd__` both modifies the object to which it is bound _and_ returns the result of that modification. You won't see many "public" built-in methods do that. – senderle Jul 23 '11 at 00:07
1

Any variable assigned to is assumed to be of local scope.

To assign to a global variable you do:

global var
var = 5

You can't do quite what you're doing in Python 2, but in Python 3 you can do:

nonlocal mylist
mylist += [1]

The Python 2 alternative for changing an item or attribute of something is

def outer():
    mylist = []
    def inner(mylist = mylist):
        mylist += [1]
    inner()
outer()

If you want to replace the value of the variable, you need to:

def outer():
    def setlist(newlist):
        mylist = newlist
    mylist = []
    def inner():
        setlist(['new_list'])
    inner()
outer()
agf
  • 171,228
  • 44
  • 289
  • 238
1

This is because myList inside inner() does not refer to myList defined in outer(), and thats why the plus equals operator does not work. There are two solutions that come to my mind.

The first, would be passing myList to inner as an argument:

def outer():
    mylist = []
    def inner(someList):
        somelist += [1]

    inner(mylist)

outer()

The second solution is to declare myList outside both functions, and then declaring it as global inside both functions:

mylist = []
def outer():
    global mylist
    mylist = []
    def inner():
        global mylist
        mylist += [1]

    inner()

outer()

Though I would recommend the first solution.

Sam
  • 2,398
  • 4
  • 25
  • 37