1

I wanted to have a function that handled a dictionary, addressed like a dictionary or similar with little work, so that when called I could run other code.

I thought it seemed easier to subclass dict, so I have. From reading help(dict) I think I have covered all dictionary editing functions so that a callback is called too. Have I? Also are there any other like pop which return a value of the editing ones?

class BindedDict(dict):
    """Custom dictionary with callback when edited."""

    def __init__(self, callback, *a, **kw):
        self.callback = callback
        super().__init__(*a, *kw)
        return

    def __delitem__(self, *a, **kw):
        super().__delitme__(*a, **kw)
        self.callback()
        return

    def __setitem__(self, *a, **kw):
        super().__setitem__(*a, **kw)
        self.callback()
        return

    def clear(self, *a, **kw):
        super().clear(*a, **kw)
        self.callback()
        return

    def pop(self, *a, **kw):
        r = super().pop(*a, **kw)
        self.callback()
        return r

    def popitem(self, *a, **kw):
        super().popitem(*a, **kw)
        self.callback()
        return

    def setdefault(self, *a, **kw):
        super().setdefault(*a, **kw)
        self.callback()
        return

    def update(self, *a, **kw):
        super().update(*a, **kw)
        self.callback()
        return

Also a better title and class name would be good.

Max
  • 145
  • 1
  • 14
  • `__delitme__`? I'd recommend looking into the `MutableMapping` [ABC](https://docs.python.org/3/library/collections.abc.htmlhttps://docs.python.org/3/library/collections.abc.html). If your code works correctly, and you want improvements suggested, see [codereview.se]. – jonrsharpe Jun 10 '17 at 21:55
  • Thanks @jonrsharpe . I had not tested deleting yet and thats a typo. By the way the ABC link is dead. – Max Jun 10 '17 at 21:57
  • @Peilonrayz managed to paste it twice... https://docs.python.org/3/library/collections.abc.html – jonrsharpe Jun 10 '17 at 22:03
  • @jonrsharpe looking at the docs it does not explain much there, I'm going to google it but if you see this: what and how could I use it? Thanks – Max Jun 10 '17 at 22:07

1 Answers1

1

I'd use composition rather than inheritance, and implement the MutableMapping from collections.abc so that I get some of the methods implemented for free. Per the documentation, you have to provide implementations of __getitem__, __setitem__, __delitem__, __iter__, and __len__:

from collections.abc import MutableMapping


class BoundDict(MutableMapping):
    """Dict-like class that calls a callback on changes.

    Note that the callback is invoked *after* the 
    underlying dictionary has been mutated.

    """

    def __init__(self, callback, *args, **kwargs):
        self._callback = callback
        self._map = dict(*args, **kwargs)

    def __getitem__(self, key):
        return self._map[key]

    def __setitem__(self, key, value):
        self._map[key] = value
        self._callback()

    def __delitem__(self, key):
        del self._map[key]
        self._callback()

    def __iter__(self):
        return iter(self._map)

    def __len__(self):
        return len(self._map)

Note that you don't need to put a bare return at the end of a method, and I've added a docstring to explain what the class does.

Thanks to the abstract base class, the following additional methods will now be implemented for you: __contains__, keys, items, values, get, __eq__, and __ne__, pop, popitem, clear, update, and setdefault. Because they all call through to the five fundamental methods defined above, it's guaranteed that any change through the MutableMapping interface (although not changes to _map directly) will invoke the callback, as it will always involve calling either __setitem__ or __delitem__.

In use:

>>> bd = BoundDict(lambda: print('changed'), [('foo', 'bar')], hello='world')
>>> bd
<BoundDict object at 0x7f8a4ea61048>
>>> list(bd.items())
[('foo', 'bar'), ('hello', 'world')]
>>> bd['foo']
'bar'
>>> bd['hello']
'world'
>>> bd['foo'] = 'baz'
changed
>>> del bd['hello']
changed
>>> bd['foo']
'baz'
>>> bd['hello']
Traceback (most recent call last):
  File "python", line 1, in <module>
  File "python", line 16, in __getitem__
KeyError: 'hello'

The only downside to this is that if you have explicit type checking, you might have issues:

>>> isinstance(bd, dict)
False

However, you should generally be using the ABCs for your type checks, too (or just duck typing):

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

I think when you ask "and that explains why I only see one callback with my example?" below you want to know why the following happens with our different implementations:

>>> BindedDict(lambda: print('changed'), foo='bar', hello='world').clear()
changed
>>> BoundDict(lambda: print('changed'), foo='bar', hello='world').clear()
changed
changed

This is due to the implementation of MutableMapping.clear; it loops over the keys in the dictionary, calls popitem for each one, which in turn calls __delitem__, which in turns calls the callback. By contrast, your implementation calls the callback only once, because you implement clear directly and call it from there.

Note that the ABC approach doesn't prevent you from doing this. It's not clear from your question (and you may not yet know) which is the correct behaviour, but you can still come in and override the default implementations provided by the ABC:

class BoundDict(MutableMapping):
    """..."""

    ...

    def clear(self):
        self._map.clear()  # or e.g. self._map = {}
        self._callback()

The reason I recommend using the ABC rather than subclassing dict is that this gives you sensible default implementations that you can override where you need to, so you only need to worry about where your behaviour differs from the defaults. Fewer methods implemented also means a smaller risk of simple typos like __delitme__ (if you don't provide a required @abstractmethod you get an error when you try to instantiate the class) and super().__init__(*a, *kw).

jonrsharpe
  • 99,167
  • 19
  • 183
  • 334
  • Thanks, this is a really good explanation. What I don't get is why people are saying on different questions at when you subclass dict you don't get the methods? As everything is working from what I tested and it does not have downsides to ABC MM. Another thing, with your example above you say that you only need call back on `__setitem__` and `__delitem__`. Do the other methods use internal methods then to access them as with my code the callback is only called once as if the main two are not called? I might add a logger to each one to see... – Max Jun 10 '17 at 22:46
  • *"why people are saying on different questions at when you subclass dict you don't get the methods?"* - I don't understand the question, if you have other information maybe provide links? *"Do the other methods use internal methods then to access them"* - as I say in the answer, the extra methods that are implemented for you all call through to the five required implementations. Do `bd = BoundDict(lambda: print('changed'), [('foo', 'bar')], hello='world')` then `bd.clear()` and you'll see `changed` twice, for example. – jonrsharpe Jun 10 '17 at 22:49
  • So when you inherit a class you get all its methods, so what methods would I be missing? In your example: `methods implemented for free`, [Another Questions](https://stackoverflow.com/questions/3387691/how-to-perfectly-override-a-dict) Do you, and they mean I do not have to cover all methods which I want to change as it does not use super() which then called the parents unchanged methods and that explains why I only see one callback with my example? – Max Jun 10 '17 at 23:04
  • @Max sorry, I've no idea what you're trying to ask now. It's not clear why you think you're missing *any* methods - **the ABC provides them for you**. This approach doesn't have inheritance or `super`. It's unclear what your example is. – jonrsharpe Jun 10 '17 at 23:07
  • @Max I think I figured out what you meant by the last part at least and have edited my answer. It's worth having a look at the source code I link to, in the context of [how ABCs work generally in Python](https://docs.python.org/3/library/abc.html#module-abc); the `MutableMapping` and `Mapping` are pretty straightforward. – jonrsharpe Jun 11 '17 at 08:39