Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why doesnt Pythons += (plus equals) operator modify variables from inner functions?

Tags:

python

scope

list

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.

like image 213
nh2 Avatar asked Jul 22 '11 23:07

nh2


3 Answers

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()
like image 44
agf Avatar answered Sep 28 '22 06:09

agf


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()
like image 75
senderle Avatar answered Sep 28 '22 06:09

senderle


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)
like image 24
MRAB Avatar answered Sep 28 '22 06:09

MRAB