如何使dict的子类尽可能“完美”?
最终目标是要有一个简单的字典,其中的键是小写的。
如果我覆盖__getitem__
/ __setitem__
,则获取/设置不起作用。我如何使它们工作?当然,我不需要单独实施它们吗?
我是否在阻止酸洗,我需要实施
__setstate__
等吗?
我需要repr,update和__init__
吗?
我应该只使用mutablemapping
(似乎不应该使用UserDict
或DictMixin
)吗?如果是这样,怎么办?这些文档并不完全具有启发性。
可接受的答案将是我的第一种方法,但是由于它存在一些问题,并且由于没有人解决替代方法,实际上dict
是将a子类化,因此我将在此处进行操作。
接受的答案有什么问题?
对我来说,这似乎是一个非常简单的请求:
如何使dict的子类尽可能“完美”?最终目标是要有一个简单的字典,其中的键是小写的。
接受的答案实际上不是子类dict
,并且对此的测试失败:
>>> isinstance(MyTransformedDict([('Test', 'test')]), dict)
False
理想情况下,任何类型检查代码都将测试我们期望的接口或抽象基类,但是如果将我们的数据对象传递给正在测试的函数,dict
而我们无法“修复”这些函数,则此代码将失败。
其他可能引起的争议:
- 可接受的答案也缺少类方法:
fromkeys
。
可接受的答案也有冗余__dict__
-因此会占用更多的内存空间:
>>> s.foo = 'bar'
>>> s.__dict__
{'foo': 'bar', 'store': {'test': 'test'}}
实际上是子类化 dict
我们可以通过继承重用dict方法。我们需要做的就是创建一个接口层,以确保键(如果是字符串)以小写形式传递到字典中。
如果我覆盖__getitem__
/ __setitem__
,则获取/设置不起作用。我如何使它们工作?当然,我不需要单独实施它们吗?
好吧,分别实现它们是此方法的缺点,也是使用方法的不利之处MutableMapping
(请参阅接受的答案),但实际上并不需要太多工作。
首先,让我们排除Python 2和Python 3之间的差异,创建一个singleton(_RaiseKeyError
)以确保我们知道是否确实获得的参数dict.pop
,并创建一个函数以确保我们的字符串键是小写的:
from itertools import chain
try: # Python 2
str_base = basestring
items = 'iteritems'
except NameError: # Python 3
str_base = str, bytes, bytearray
items = 'items'
_RaiseKeyError = object() # singleton for no-default behavior
def ensure_lower(maybe_str):
"""dict keys can be any hashable object - only call lower if str"""
return maybe_str.lower() if isinstance(maybe_str, str_base) else maybe_str
现在我们实现-我使用super
了完整参数,因此该代码适用于Python 2和3:
class LowerDict(dict): # dicts take a mapping or iterable as their optional first argument
__slots__ = () # no __dict__ - that would be redundant
@staticmethod # because this doesn't make sense as a global function.
def _process_args(mapping=(), **kwargs):
if hasattr(mapping, items):
mapping = getattr(mapping, items)()
return ((ensure_lower(k), v) for k, v in chain(mapping, getattr(kwargs, items)()))
def __init__(self, mapping=(), **kwargs):
super(LowerDict, self).__init__(self._process_args(mapping, **kwargs))
def __getitem__(self, k):
return super(LowerDict, self).__getitem__(ensure_lower(k))
def __setitem__(self, k, v):
return super(LowerDict, self).__setitem__(ensure_lower(k), v)
def __delitem__(self, k):
return super(LowerDict, self).__delitem__(ensure_lower(k))
def get(self, k, default=None):
return super(LowerDict, self).get(ensure_lower(k), default)
def setdefault(self, k, default=None):
return super(LowerDict, self).setdefault(ensure_lower(k), default)
def pop(self, k, v=_RaiseKeyError):
if v is _RaiseKeyError:
return super(LowerDict, self).pop(ensure_lower(k))
return super(LowerDict, self).pop(ensure_lower(k), v)
def update(self, mapping=(), **kwargs):
super(LowerDict, self).update(self._process_args(mapping, **kwargs))
def __contains__(self, k):
return super(LowerDict, self).__contains__(ensure_lower(k))
def copy(self): # don't delegate w/ super - dict.copy() -> dict :(
return type(self)(self)
@classmethod
def fromkeys(cls, keys, v=None):
return super(LowerDict, cls).fromkeys((ensure_lower(k) for k in keys), v)
def __repr__(self):
return '{0}({1})'.format(type(self).__name__, super(LowerDict, self).__repr__())
我们使用的样板化的做法对任何方法或特殊方法引用的关键,但在其他方面,通过继承,我们获得方法:len
,clear
,items
,keys
,popitem
,和values
是免费的。尽管这需要一些仔细的思考才能正确解决,但看到它可行却是微不足道的。
(请注意,haskey
在Python 2 中已弃用,在Python 3中已删除。)
这是一些用法:
>>> ld = LowerDict(dict(foo='bar'))
>>> ld['FOO']
'bar'
>>> ld['foo']
'bar'
>>> ld.pop('FoO')
'bar'
>>> ld.setdefault('Foo')
>>> ld
{'foo': None}
>>> ld.get('Bar')
>>> ld.setdefault('Bar')
>>> ld
{'bar': None, 'foo': None}
>>> ld.popitem()
('bar', None)
我是否在阻止酸洗,我需要实施
__setstate__
等吗?
酸洗
dict子类的泡菜就可以了:
>>> import pickle
>>> pickle.dumps(ld)
b'\x80\x03c__main__\nLowerDict\nq\x00)\x81q\x01X\x03\x00\x00\x00fooq\x02Ns.'
>>> pickle.loads(pickle.dumps(ld))
{'foo': None}
>>> type(pickle.loads(pickle.dumps(ld)))
<class '__main__.LowerDict'>
__repr__
我需要repr,update和__init__
吗?
我们定义了update
和__init__
,但是__repr__
默认情况下您会很漂亮:
>>> ld # without __repr__ defined for the class, we get this
{'foo': None}
但是,最好编写一个,__repr__
以提高代码的可调试性。理想的测试是eval(repr(obj)) == obj
。如果您的代码很简单,我强烈建议您:
>>> ld = LowerDict({})
>>> eval(repr(ld)) == ld
True
>>> ld = LowerDict(dict(a=1, b=2, c=3))
>>> eval(repr(ld)) == ld
True
您会看到,这正是我们重新创建等效对象所需要的-这可能会出现在我们的日志或回溯中:
>>> ld
LowerDict({'a': 1, 'c': 3, 'b': 2})
结论
我应该只使用mutablemapping
(似乎不应该使用UserDict
或DictMixin
)吗?如果是这样,怎么办?这些文档并不完全具有启发性。
是的,这些是更多几行代码,但是它们旨在变得更全面。我的第一个倾向是使用公认的答案,如果有问题,我将看一下我的答案-因为它有点复杂,而且没有ABC可以帮助我正确设置界面。
过早的优化将使搜索性能变得更加复杂。
MutableMapping
更简单-在其他所有条件相同的情况下,它可以立即获得优势。不过,要列出所有差异,让我们进行比较和对比。
我应该补充一点,是有人试图将类似的字典放入collections
模块中,但是被拒绝了。您可能应该这样做:
my_dict[transform(key)]
它应该更容易调试。
比较和对比
有MutableMapping
(缺少fromkeys
)实现的6个接口函数和带有dict
子类的11 个接口函数。我并不需要实现__iter__
或者__len__
,而是我要实现get
,setdefault
,pop
,update
,copy
,__contains__
,和fromkeys
-但这些都是相当琐碎,因为我可以使用继承大多数这些实现的。
该MutableMapping
实现在Python中dict
实现了一些用C 实现的东西-因此,我希望dict
在某些情况下子类的性能更高。
我们__eq__
在两种方法上都获得了自由-只有当另一个dict都为小写时,这两种方法才假定相等-但是,我再次认为,dict
子类的比较会更快。
摘要:
- 子类化
MutableMapping
更简单,发生错误的机会更少,但更慢,占用更多内存(请参阅冗余字典),并且失败isinstance(x, dict)
- 子类化
dict
更快,使用更少的内存并通过isinstance(x, dict)
,但是实现起来却更加复杂。
哪个更完美?那取决于您对完美的定义。