Search code examples
pythonmethodsglobaloperator-keyword

Python += versus .extend() inside a function on a global variable


I've read a few other SO (PythonScope and globals don't need global) but nothing seems to explain as explicitly as I would like and I'm having trouble mentally sifting through whether or not PyDocs tells me the answer to my question:

myList = [1]

def foo():
    myList = myList + [2, 3]
def bar():
    myList.extend([2, 3])
def baz():
    myList += [2, 3]

Now, understandably,

>>> foo()
UnboundLocalError: local variable 'myList' referenced before assignment

and

bar()  # works
myList # shows [1, 2, 3]

but then

>>> baz()
UnboundLocalError: local variable 'myList' referenced before assignment

I thought, however, that things like += implicitly called the method operators, in this case extend(), but the error implies that for some reason it does not actually treat += as extends(). Is this consistent with how Python parsing ought to work?

I would have thought that calling functions that are equivalent to method-operators, they would be equivalent in all cases. Instead it seems that it treats += as an actual assignment operator. Except, this isn't completely true, because if I do something (admittedly contrived):

myList = range(50000000) # wait a second or two on my laptop before returning
myList += [0]            # returns instantly
myList = myList + [1]    # wait a second or two before returning

all of which is expected, if += actually just calls extend().

Is there some finer distinction (or very obvious point...) that I'm missing that makes it clear that myList in baz() needs to be treated as a local variable, and that therefore the += cannot be implicitly converted to an extend() such that it recognizes the global variable?


Solution

  • += doesn't implicitly call extend(). Firstly, it is an augmented assignment operator.

    If you look at the section on assignment it says:

    Assignment of an object to a single target is recursively defined as follows.

    If the target is an identifier (name):

    If the name does not occur in a global statement in the current code block: the name is bound to the object in the current local namespace. Otherwise: the name is bound to the object in the current global namespace.

    Since an augmented assignment is:

    Augmented assignment is the combination, in a single statement, of a binary operation and an assignment statement:

    It plays by the same rules. As you can see:

    >>> def baz():
            myList += [2, 3]
    
    
    >>> dis.dis(baz)
      2           0 LOAD_FAST                0 (myList)
                  3 LOAD_CONST               1 (2)
                  6 LOAD_CONST               2 (3)
                  9 BUILD_LIST               2
                 12 INPLACE_ADD         
                 13 STORE_FAST               0 (myList)
                 16 LOAD_CONST               0 (None)
                 19 RETURN_VALUE  
    

    An augmented assignment evaluates the target (which, unlike normal assignment statements, cannot be an unpacking) and the expression list, performs the binary operation specific to the type of assignment on the two operands, and assigns the result to the original target. The target is only evaluated once..

    The first call trys to evaluate myList, this results in LOAD_FAST since there was no global statement it is assumed to be a local variable:

    LOAD_FAST(var_num)

    Pushes a reference to the local co_varnames[var_num] onto the stack.

    It can't be found so the error is raised. If it was found, then we get to the oppcode INPLACE_ADD which calls the method myList.__iadd__ which does the job of extend, once this operation completes the result will be assigned back to the variable but we never get this far.

    You shouldn't really be manipulating globals anyway, return the new result from your function or pass it as a parameter.