字典子类,回调是否都涵盖了所有编辑方法?



我想要一个处理字典的函数,像字典或类似的东西一样,只需很少的工作,这样当调用时我可以运行其他代码。

我认为对字典进行子分类似乎更容易,所以我有。通过阅读help(dict)我认为我已经涵盖了所有字典编辑函数,以便也调用回调。我有吗?还有没有其他像 pop 一样返回编辑值?

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

更好的标题和类名也会很好。

我会使用组合而不是继承,并从collections.abc实现MutableMapping,以便免费实现一些方法。根据文档,您必须提供__getitem____setitem____delitem____iter____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)

请注意,您不需要在方法末尾放置裸return,并且我添加了一个文档字符串来解释该类的作用。

多亏了抽象基类,现在将为您实现以下附加方法:__contains__keysitemsvaluesget__eq____ne__poppopitemclearupdatesetdefault。因为它们都调用上面定义的五个基本方法,所以可以保证通过MutableMapping接口进行的任何更改(尽管不是直接对_map的更改)都将调用回调,因为它始终涉及调用__setitem____delitem__

使用中:

>>> 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'

这样做的唯一缺点是,如果您有显式类型检查,则可能会遇到问题:

>>> isinstance(bd, dict)
False

但是,您通常也应该使用 ABC 进行类型检查(或者只是鸭子打字):

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

我想当你问"这解释了为什么我在我的示例中只看到一个回调?">时,你想知道为什么我们的不同实现会发生以下情况:

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

这是由于MutableMapping.clear的实现;它循环字典中的键,为每个键调用popitem,而__delitem__又调用回调。相比之下,您的实现只调用回调一次,因为您直接实现clear并从那里调用它。

请注意,ABC 方法不会阻止您执行此操作。从您的问题(您可能还不知道)中不清楚哪个是正确的行为,但您仍然可以进入并覆盖 ABC 提供的默认实现

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

我建议使用 ABC 而不是子类化dict的原因是,这为您提供了合理的默认实现,您可以在需要的地方覆盖这些实现,因此您只需要担心您的行为与默认值的不同之处。实现的方法越少也意味着出现简单拼写错误的风险较小,例如__delitme__(如果未提供必需的@abstractmethod则在尝试实例化类时会出现错误)和super().__init__(*a, *kw)

最新更新