Search code examples
pythondictionaryabc

How would I implement a dict with Abstract Base Classes in Python?


I attempted to implement a mapping in Python by using the abstract base class, MutableMapping, but I got an error on instantiation. How would I go about making a working version of this dictionary that would emulate the builtin dict class, in as many ways as possible, to be clear, with Abstract Base Classes?

>>> class D(collections.MutableMapping):
...     pass
... 
>>> d = D()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class D with abstract methods __delitem__, __getitem__, __iter__, __len__, __setitem__

A good answer will demonstrate how to make this work, specifically without subclassing dict (a concept that I am quite familiar with).


Solution

  • How would I implement a dict with Abstract Base Classes?

    A good answer will demonstrate how to make this work, specifically without subclassing dict.

    Here's the error message: TypeError: Can't instantiate abstract class D with abstract methods __delitem__, __getitem__, __iter__, __len__, __setitem__

    It turns out that one must implement them to use the Abstract Base Class (ABC), MutableMapping.

    Implementation

    So I implement a mapping that works like a dict in most respects that uses the object's attribute reference dict for the mapping. (Delegation is not the same as inheritance, so we'll just delegate to the instance __dict__, we could use any other ad-hoc mapping, but you don't seem to care about that part of the implementation. It makes sense to do it this way in Python 2, because MutableMapping doesn't have __slots__ in Python 2, so you're creating a __dict__ either way. In Python 3, you could avoid dicts altogether by setting __slots__.)

    from collections.abc import MutableMapping
    
    class D(MutableMapping):
        '''
        Mapping that works like both a dict and a mutable object, i.e.
        d = D(foo='bar')
        and 
        d.foo returns 'bar'
        '''
        # ``__init__`` method required to create instance from class.
        def __init__(self, *args, **kwargs):
            '''Use the object dict'''
            self.__dict__.update(*args, **kwargs)
        # The next five methods are requirements of the ABC.
        def __setitem__(self, key, value):
            self.__dict__[key] = value
        def __getitem__(self, key):
            return self.__dict__[key]
        def __delitem__(self, key):
            del self.__dict__[key]
        def __iter__(self):
            return iter(self.__dict__)
        def __len__(self):
            return len(self.__dict__)
        # The final two methods aren't required, but nice for demo purposes:
        def __str__(self):
            '''returns simple dict representation of the mapping'''
            return str(self.__dict__)
        def __repr__(self):
            '''echoes class, id, & reproducible representation in the REPL'''
            return '{}, D({})'.format(super(D, self).__repr__(), 
                                      self.__dict__)
    

    Demonstration

    And to demonstrate the usage:

    >>> d = D((e, i) for i, e in enumerate('abc'))
    >>> d
    <__main__.D object at 0x7f75eb242e50>, D({'b': 1, 'c': 2, 'a': 0})
    >>> d.a
    0
    >>> d.get('b')
    1
    >>> d.setdefault('d', []).append(3)
    >>> d.foo = 'bar'
    >>> print(d)
    {'b': 1, 'c': 2, 'a': 0, 'foo': 'bar', 'd': [3]}
    

    And for ensuring the dict API, lesson learned is that you can always check for collections.abc.MutableMapping:

    >>> isinstance(d, MutableMapping)
    True
    >>> isinstance(dict(), MutableMapping)
    True
    

    And while a dict is always going to be an instance of a MutableMapping due to registration on collections import, the reverse is not always true:

    >>> isinstance(d, dict)
    False
    >>> isinstance(d, (dict, MutableMapping))
    True
    

    After performing this exercise, it is clear to me that using Abstract Base Classes provides only the guarantee of a standard API for users of the class. In this case, users assuming a MutableMapping object will be guaranteed the standard API for Python.

    Caveats:

    The fromkeys class constructor method is not implemented.

    >>> dict.fromkeys('abc')
    {'b': None, 'c': None, 'a': None}
    >>> D.fromkeys('abc')
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AttributeError: type object 'D' has no attribute 'fromkeys'
    

    One could mask the builtin dict methods like get or setdefault

    >>> d['get'] = 'baz'
    >>> d.get('get')
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: 'str' object is not callable
    

    It's fairly simple to unmask again:

    >>> del d['get']
    >>> d.get('get', 'Not there, but working')
    'Not there, but working'
    

    But I wouldn't use this code in production.


    Demonstration without a dict, Python 3:

    >>> class MM(MutableMapping):
    ...   __delitem__, __getitem__, __iter__, __len__, __setitem__ = (None,) *5
    ...   __slots__ = ()
    ...
    >>> MM().__dict__
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AttributeError: 'MM' object has no attribute '__dict__'