4

Following up on Reason for unintuitive UnboundLocalError behaviour (I will assume you've read it). Consider the following Python script:

def f():
    # a+=1          # 1
    aa=a
    aa+=1

    # b+='b'        # 2
    bb=b
    bb+='b'

    c[0]+='c'       # 3
    c.append('c')
    cc=c
    cc.append('c')

    d['d']=5        # Update 1
    d['dd']=6       # Update 1
    dd=d            # Update 1
    dd['ddd']=7     # Update 1

    e.add('e')      # Update 2
    ee=e            # Update 2
    ee.add('e')     # Update 2

a=1
b='b'
c=['c']
d={'d':4}           # Update 1
e=set(['e'])        # Update 2
f()
print a
print b
print c
print d             # Update 1
print e             # Update 2

The result of the script is:

1
b
['cc', 'c', 'c']
{'dd': 6, 'd': 5, 'ddd': 7}
set(['e'])

The commented out lines (marked 1,2) are lines that would through an UnboundLocalError and the SO question I referenced explains why. However, the line marked 3 works!

By default, lists are copied by reference in Python, therefore it's understandable that c changes when cc changes. But why should Python allow c to change in the first place, if it didn't allow changes to a and b directly from the method's scope?

I don't see how the fact that by default lists are copied by reference in Python should make this design decision inconsistent.

What am I missing folks?

UPDATES:

  • For completeness I also added the dictionary equivalent to the question above, i.e. I added the source code and marked the update with # Update
  • For further completeness I also added the set equivalent. The set's behavior is actually surprisingly for me. I expected it to act similar to list and dictionary...
Community
  • 1
  • 1
Jonathan Livni
  • 101,334
  • 104
  • 266
  • 359
  • 1
    "by default lists are copied by reference" That's false. Everything is a reference. But lists are mutable objects, where integers are immutable. It would help if you update your terminology to reflect the way Python actually works. – S.Lott Dec 26 '10 at 16:25

2 Answers2

3

Unlike strings and integers, lists in Python are mutable objects. This means they are designed to be changed. The line

c[0] += 'c'

is identical to saying

c.__setitem__(0, c.__getitem__(0) + 'c')

which doesn't make any change to what the name c is bound to. Before and after this call, c is the same list – it's just the contents of this list that have changed.

Had you said

c += ['c']
c = [42]

in the function f(), the same UnboundLocalError would have occured, because the second line makes c a local name, and the first line translates to

c = c + ['c']

requiring the name c to be already bound to something, which (in this local scope) it isn't yet.

balpha
  • 50,022
  • 18
  • 110
  • 131
  • Good examples and very clear answer. I still think it's not intuitive (especially that different changes to the list have different affects, as you demonstrated) albeit perhaps useful in many cases. – Jonathan Livni Dec 26 '10 at 14:58
  • 2
    Long story short: `expr[expr]=expr` is not `name = expr`. –  Dec 26 '10 at 14:59
  • \@delnan - do you guys know why set acts differently than list and dict? (I've updated the question above to show the result – Jonathan Livni Dec 26 '10 at 21:24
  • 1
    @Jonathan I don't see a difference in the behavior of set. In fact, my above mentioned behavior is even clearer here, because there's no syntactic element that "pretends" to be an assignment: All you do is *call a method* on the object bound to the name "e" -- you never do any kind of assignment like "e=..." – balpha Dec 26 '10 at 21:35
2

The important thing to think about is this: what object does a (or b or c) refer to? The line a += 1 is changing which integer a refers to. Integers are immutable, so when a changes from 1 to 2, it's really the same as a = a + 1, which is giving a an entirely new integer to refer to.

On the other hand, c[0] += 'c' doesn't change which list c refers to, it merely changes which string its first element refers to. Lists are mutable, so the same list can be modified without changing its identity.

Ned Batchelder
  • 364,293
  • 75
  • 561
  • 662