子类化Python字典以重写__setitem__
我正在创建一个类,这个类是从 dict
继承而来的,并且我重写了 __setitem__
方法。我想确保在所有可能设置字典项的情况下,我的方法都会被调用。
我发现有三种情况,在这些情况下,Python(这里是 2.6.4 版本)不会调用我重写的 __setitem__
方法,而是直接调用 PyDict_SetItem
。
- 在构造函数中
- 在
setdefault
方法中 - 在
update
方法中
作为一个非常简单的测试:
class MyDict(dict):
def __setitem__(self, key, value):
print "Here"
super(MyDict, self).__setitem__(key, str(value).upper())
>>> a = MyDict(abc=123)
>>> a['def'] = 234
Here
>>> a.update({'ghi': 345})
>>> a.setdefault('jkl', 456)
456
>>> print a
{'jkl': 456, 'abc': 123, 'ghi': 345, 'def': '234'}
你可以看到,只有在明确设置项的时候,重写的方法才会被调用。为了让 Python 始终调用我的 __setitem__
方法,我不得不重新实现这三种方法,像这样:
class MyUpdateDict(dict):
def __init__(self, *args, **kwargs):
self.update(*args, **kwargs)
def __setitem__(self, key, value):
print "Here"
super(MyUpdateDict, self).__setitem__(key, value)
def update(self, *args, **kwargs):
if args:
if len(args) > 1:
raise TypeError("update expected at most 1 arguments, got %d" % len(args))
other = dict(args[0])
for key in other:
self[key] = other[key]
for key in kwargs:
self[key] = kwargs[key]
def setdefault(self, key, value=None):
if key not in self:
self[key] = value
return self[key]
还有其他方法需要我重写吗?这样我才能确保 Python 总是 调用我的 __setitem__
方法?
更新
根据 gs 的建议,我尝试从 UserDict 继承(实际上是从 IterableUserDict 继承,因为我想遍历键),像这样:
from UserDict import *;
class MyUserDict(IterableUserDict):
def __init__(self, *args, **kwargs):
UserDict.__init__(self,*args,**kwargs)
def __setitem__(self, key, value):
print "Here"
UserDict.__setitem__(self,key, value)
这个类似乎在 setdefault
方法上正确调用了我的 __setitem__
,但在 update
方法上,或者在构造函数提供初始数据时,并没有调用它。
更新 2
Peter Hansen 的建议让我更仔细地查看了 dictobject.c,我意识到 update 方法可以简化一些,因为内置字典构造函数无论如何都会调用内置的 update 方法。现在它看起来像这样:
def update(self, *args, **kwargs):
if len(args) > 1:
raise TypeError("update expected at most 1 arguments, got %d" % len(args))
other = dict(*args, **kwargs)
for key in other:
self[key] = other[key]
4 个回答
我觉得Ian的回答和评论非常有帮助,也很清晰。我想指出的是,可能在不必要的情况下,第一次调用父类的__init__
方法会更安全。我最近需要实现一个自定义的OrderedDict(我在用Python 2.7):在根据提议的MyUpdateDict
实现修改我的代码后,我发现只需将
class MyUpdateDict(dict):
替换为:
from collections import OrderedDict
class MyUpdateDict(OrderedDict):
然后上面发布的测试代码就失败了:
Traceback (most recent call last):
File "Desktop/test_updates.py", line 52, in <module>
my_dict = MyUpdateDict([('b',2),('c',3)],a=1)
File "Desktop/test_updates.py", line 5, in __init__
self.update(*args, **kwargs)
File "Desktop/test_updates.py", line 18, in update
self[key] = other[key]
File "Desktop/test_updates.py", line 9, in __setitem__
super(MyUpdateDict, self).__setitem__(key, value)
File "/usr/lib/python2.7/collections.py", line 59, in __setitem__
root = self.__root
AttributeError: 'MyUpdateDict' object has no attribute '_OrderedDict__root'
查看collections.py代码后发现,OrderedDict确实需要调用它的__init__
方法,以便初始化和设置必要的自定义属性。
因此,只需在开始时调用父类的__init__
方法,
from collections import OrderedDict
class MyUpdateDict(Orderedict):
def __init__(self, *args, **kwargs):
super(MyUpdateDict, self).__init__() #<-- HERE call to super __init__
self.update(*args, **kwargs)
我们就有了一个更通用的解决方案,这显然对dict和OrderedDict都有效。
我不能说这个解决方案是否普遍有效,因为我只用OrderedDict测试过。不过,当尝试扩展其他字典子类时,调用父类的__init__
方法可能是无害的或必要的,而不是有害的。
你为什么想要继承字典(dict)呢?
其实你并不需要这样做来创建一个像字典的对象。对于你来说,写一个普通的类可能会更简单,然后再添加你需要的字典功能就可以了。
如果你想实现这个功能,最好的办法可能是使用可变映射(MutableMapping)这个抽象基类。你可以查看PEP 3119 -- 引入抽象基类来了解更多。
这也能帮助你回答“还有其他方法需要重写吗?”这个问题。你需要重写所有的抽象方法。对于可变映射来说,抽象方法包括setitem(设置项)和delitem(删除项)。具体方法包括pop(弹出)、popitem(弹出项)、clear(清空)和update(更新)。
我在回答我自己的问题,因为我最终决定我确实想要继承字典(Dict),而不是创建一个新的映射类。而且,UserDict在某些情况下还是会依赖底层的字典对象,而不是使用提供的 __setitem__
方法。
在反复阅读Python 2.6.4的源代码(主要是 Objects/dictobject.c
,但我也在其他地方查找了各种方法的使用情况)后,我明白了以下代码 足以 确保每次对象被修改时都会调用我的 __setitem__
,并且在其他方面的行为和Python的字典完全一样:
Peter Hansen的建议让我更仔细地查看了 dictobject.c
,我意识到我原来的答案中的更新方法可以简化一些,因为内置的字典构造函数反正会调用内置的更新方法。所以我答案中的第二个更新已经被添加到下面的代码中(由某个热心的人提供的;-)。
class MyUpdateDict(dict):
def __init__(self, *args, **kwargs):
self.update(*args, **kwargs)
def __setitem__(self, key, value):
# optional processing here
super(MyUpdateDict, self).__setitem__(key, value)
def update(self, *args, **kwargs):
if args:
if len(args) > 1:
raise TypeError("update expected at most 1 arguments, "
"got %d" % len(args))
other = dict(args[0])
for key in other:
self[key] = other[key]
for key in kwargs:
self[key] = kwargs[key]
def setdefault(self, key, value=None):
if key not in self:
self[key] = value
return self[key]
我用这段代码进行了测试:
def test_updates(dictish):
dictish['abc'] = 123
dictish.update({'def': 234})
dictish.update(red=1, blue=2)
dictish.update([('orange', 3), ('green',4)])
dictish.update({'hello': 'kitty'}, black='white')
dictish.update({'yellow': 5}, yellow=6)
dictish.setdefault('brown',7)
dictish.setdefault('pink')
try:
dictish.update({'gold': 8}, [('purple', 9)], silver=10)
except TypeError:
pass
else:
raise RunTimeException("Error did not occur as planned")
python_dict = dict([('b',2),('c',3)],a=1)
test_updates(python_dict)
my_dict = MyUpdateDict([('b',2),('c',3)],a=1)
test_updates(my_dict)
结果通过了。我尝试的其他实现都在某个时候失败了。我仍然会接受任何能告诉我我遗漏了什么的答案,但否则,我会在几天内在这个答案旁边打勾,认为这是正确的答案 :)