4

Can anyone tell me why this code complains that there's no __setitem__ on container? I thought I only needed __getitem__ on the container to fetch the item and then __iadd__ to set the value, don't know why it's expecting __setitem__

class Item:
    def __init__(self):
        pass

    def __iadd__(self, value):
        print 'added: ' + value
        return self


class Container:

    def __init__(self):
        self.__items = {
            'foo': Item()
        }

    def __getitem__(self, name):
        return self.__items[name]

if __name__ == '__main__':

    # works!
    con = Container()
    item = con['foo']
    item += 'works!'

    # wtf?
    con['foo'] += "what's going on?"

    # output:

    # added: works!
    # added: what's going on?
    # Traceback (most recent call last):
    #   File "foo.py", line 27, in <module>
    #     con['foo'] += "what's going on?"
    # AttributeError: Container instance has no attribute '__setitem__'
JD Frias
  • 4,418
  • 3
  • 21
  • 24

2 Answers2

3

Riddle me this: What happens if you use += on an immutable type, like an int? Can you see why calling += on a value returned from [] would also need to call __setitem__?

The real trick is understanding that any operator in python that has an = in it involves an assignment, since it needs to be able to deal with immutable types in the manner expected.

To make that clear, consider the following:

>>> a = 10*10**9
>>> b = a
>>> a is b
True
>>> a += 1
>>> a is b
False

Clearly, a was assigned to reference a new value. Therefore, it holds that the same happens when you perform += on the value returned by __getitem__.

Unfortunately the python documentation is terribly bad about stating this is what will happen, but as the other answer pointed out with their decompile, it clearly does, and necessarily must to properly handle immutable types stored in mutable containers.

aruisdante
  • 8,875
  • 2
  • 30
  • 37
  • I trimmed my code down to paste it and accidentally removed the return statement, but even then I get the same error. – JD Frias Jul 21 '14 at 21:47
  • Right, because it's still performing an assignment. That's why ``__iadd__`` returns a value in the first place. – aruisdante Jul 21 '14 at 21:47
  • I would avoid using small ints in examples of object comparison, you could end up seeing some unexpected output. – Padraic Cunningham Jul 21 '14 at 21:50
  • @JD. To further drive home the point, look at the documentation for [``operator.iadd``](https://docs.python.org/2/library/operator.html?highlight=__iadd__#operator.__iadd__). Unfortunately the actual python documentation for ``__iadd__`` only alludes to the fact that this happens, rather than saying it explicitly. – aruisdante Jul 21 '14 at 21:53
  • @PadraicCunningham Not really in this case, but sure, I'll use a big number, gets the same result. – aruisdante Jul 21 '14 at 21:56
3

Basically,

con['foo'] += "what's going on?"

compiles to something like:

item = con['foo']
item += "what's going on?"
conf['foo'] = item

You can see, decompiling the code, something like:

  2           0 LOAD_GLOBAL              0 (con)
              3 LOAD_CONST               1 ('foo')
              6 DUP_TOPX                 2
              9 BINARY_SUBSCR       
             10 LOAD_CONST               2 ("what's going on?")
             13 INPLACE_ADD         
             14 ROT_THREE           
             15 STORE_SUBSCR        
Juan Lopes
  • 10,143
  • 2
  • 25
  • 44