26

Let's for the sake of example assume I want to subclass dict and have all keys capitalized:

class capdict(dict):
    def __init__(self,*args,**kwds):
        super().__init__(*args,**kwds)
        mod = [(k.capitalize(),v) for k,v in super().items()]
        super().clear()
        super().update(mod)
    def __getitem__(self,key):
        return super().__getitem__(key.capitalize())
    def __setitem__(self,key,value):
        super().__setitem__(key.capitalize(),value)
    def __delitem__(self,key):
        super().__detitem__(key.capitalize())

This works to an extent,

>>> ex = capdict(map(reversed,enumerate("abc")))
>>> ex
{'A': 0, 'B': 1, 'C': 2}
>>> ex['a']
0

but, of course, only for methods I remembered to implement, for example

>>> 'a' in ex
False

is not the desired behavior.

Now, the lazy way of filling in all the methods that can be derived from the "core" ones would be mixing in collections.abc.MutableMapping. Only, it doesn't work here. I presume because the methods in question (__contains__ in the example) are already provided by dict.

Is there a way of having my cake and eating it? Some magic to let MutableMapping only see the methods I've overridden so that it reimplements the others based on those?

Paul Panzer
  • 47,318
  • 2
  • 37
  • 82

1 Answers1

27

What you could do:

This likely won't work out well (i.e. not the cleanest design), but you could inherit from MutableMapping first and then from dict second.

Then MutableMapping would use whatever methods you've implemented (because they are the first in the lookup chain):

>>> class D(MutableMapping, dict):
        def __getitem__(self, key):
            print(f'Intercepted a lookup for {key!r}')
            return dict.__getitem__(self, key)


>>> d = D(x=10, y=20)
>>> d.get('x', 0)
Intercepted a lookup for 'x'
10
>>> d.get('z', 0)
Intercepted a lookup for 'z'
0

Better way:

The cleanest approach (easy to understand and test) is to just inherit from MutableMapping and then implement the required methods using a regular dict as the base data store (with composition rather than inheritance):

>>> class CapitalizingDict(MutableMapping):
        def __init__(self, *args, **kwds):
            self.store = {}
            self.update(*args, **kwds)
        def __getitem__(self, key):
            key = key.capitalize()
            return self.store[key]
        def __setitem__(self, key, value):
            key = key.capitalize()
            self.store[key] = value
        def __delitem__(self, key):
            del self.store[key]
        def __len__(self):
            return len(self.store)
        def __iter__(self):
            return iter(self.store)
        def __repr__(self):
            return repr(self.store)


>>> d = CapitalizingDict(x=10, y=20)
>>> d
{'X': 10, 'Y': 20}
>>> d['x']
10
>>> d.get('x', 0)
10
>>> d.get('z', 0)
0
>>> d['w'] = 30
>>> d['W']
30
Raymond Hettinger
  • 182,864
  • 54
  • 321
  • 419
  • Thanks! I could have sworn I tried both orders... Out of interest, when I use the "could do" method swapping out all the `super`s for explicit `dict`s then it appears to work, except `len` returns `0`. Where's that coming from? – Paul Panzer Jan 27 '20 at 00:05
  • 2
    The super() call from *\__len_()__* goes to the next in the mro: ``(D, MutableMapping, dict)``. That is the *MutableMappiing.__len__()* method which always returns 0. It wasn't intended to be called directly -- it is always supposed to be overridden. That's why you have to call ``dict.__len__(self)`` directly. And that's one of the reasons that I said "this likely won't work out well" ;-) – Raymond Hettinger Jan 27 '20 at 00:15