6

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?

Superdooperhero
  • 7,584
  • 19
  • 83
  • 138
dwanderson
  • 2,775
  • 2
  • 25
  • 40
  • 5 years later and this bit me again, but in a different way. If `isinstance(other, numpy.ndarray)` and `isinstance(orig, list)`, then `orig += other` fails with a numpy error (`operands could not broadcast`) but `orig.extend(other)` works – dwanderson Aug 30 '18 at 18:04
  • My duplicate marking here is something of a placeholder. I intend to prepare (either write or find) a new canonical that overall explains under what circumstances `global` is needed. Almost everyone who asks a question about is *too* focused to have a really *useful* question. – Karl Knechtel Sep 11 '22 at 06:58
  • Gotcha, makes sense. This has _some_ nuance, in that the answer is more about the difference between method operators vs augmented assignment; but maybe that only ever comes up in relation to `global`. Either way, I'm a fan of more general canonical answers, so that works for me! – dwanderson Sep 13 '22 at 19:43
  • "This has some nuance, in that the answer is more about the difference between method operators vs augmented assignment; but maybe that only ever comes up in relation to global." Rather, the difference only *has a noticeable effect* in combination with `global` (or `nonlocal`). Otherwise, it's simply an added step to reassign a name to what it was already naming. – Karl Knechtel Sep 13 '22 at 19:46

2 Answers2

4

+= 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.

jamylak
  • 128,818
  • 30
  • 231
  • 230
  • Ah, ok, that makes more sense now, thanks for the explanation. And I'll have to look into messing around with the disassembler a bit more, since familiarity with that would've helped me here. And yes, I wouldn't do this in practice - I made the mistake once, wasn't sure why I got this output so I was curious, but then did the normal thing and just passed it as a parameter. Thanks! – dwanderson Apr 21 '13 at 15:22
0

When you mutate the list, you should say global myList. By mutate I mean change the reference. The first example and the 3rd one is basically same, you just use += for shorthand

myList = [1]

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

foo()
bar()
baz()
marcadian
  • 2,608
  • 13
  • 20
  • The examples with the large-range show that my first example (L = L + M) is NOT equivalent to my third example (L += M), otherwise they both would have taken noticeable time, if they are (actively) changing the reference. – dwanderson Apr 21 '13 at 03:27
  • Apparently you're correct, += does not change reference. I've checked this using 'is' operator. – marcadian Apr 21 '13 at 03:42
  • I mean, I think +=/extend()/append() _could_ implicitly change the reference operator, if it needed to resize the list, but the user/code isn't _requiring_ a reference change. Thanks for posting an answer and following back up on it, I appreciate it! – dwanderson Apr 21 '13 at 03:53