pythongetattrsetattr

Python: inconsistence in the way you define the function __setattr__?


Consider this code:

class Foo1(dict):
    def __getattr__(self, key): return self[key]
    def __setattr__(self, key, value): self[key] = value

class Foo2(dict):
    __getattr__ = dict.__getitem__
    __setattr__ = dict.__setitem__

o1 = Foo1()
o1.x = 42
print(o1, o1.x)

o2 = Foo2()
o2.x = 42
print(o2, o2.x)

I would expect the same output. However, with CPython 2.5, 2.6 (similarly in 3.2) I get:

({'x': 42}, 42)
({}, 42)

With PyPy 1.5.0, I get the expected output:

({'x': 42}, 42)
({'x': 42}, 42)

Which is the "right" output? (Or what should be the output according to the Python documentation?)


Here is the bug report for CPython.


Solution

  • I suspect it has to do with a lookup optimization. From the source code:

     /* speed hack: we could use lookup_maybe, but that would resolve the
           method fully for each attribute lookup for classes with
           __getattr__, even when the attribute is present. So we use
           _PyType_Lookup and create the method only when needed, with
           call_attribute. */
        getattr = _PyType_Lookup(tp, getattr_str);
        if (getattr == NULL) {
            /* No __getattr__ hook: use a simpler dispatcher */
            tp->tp_getattro = slot_tp_getattro;
            return slot_tp_getattro(self, name);
        }
    

    The fast path does does not look it up on the class dictionary.

    Therefore, the best way to get the desired functionality is to place an override method in the class.

    class AttrDict(dict):
        """A dictionary with attribute-style access. It maps attribute access to
        the real dictionary.  """
        def __init__(self, *args, **kwargs):
            dict.__init__(self, *args, **kwargs)
    
        def __repr__(self):
            return "%s(%s)" % (self.__class__.__name__, dict.__repr__(self))
    
        def __setitem__(self, key, value):
            return super(AttrDict, self).__setitem__(key, value)
    
        def __getitem__(self, name):
            return super(AttrDict, self).__getitem__(name)
    
        def __delitem__(self, name):
            return super(AttrDict, self).__delitem__(name)
    
        __getattr__ = __getitem__
        __setattr__ = __setitem__
    
         def copy(self):
            return AttrDict(self)
    

    Which I found works as expected.