How can I make as "perfect" a subclass of dict as possible?
The end goal is to have a simple dict in which the keys are lowercase.
If I override __getitem__/__setitem__, then get/set don't work. How
do I make them work? Surely I don't need to implement them
individually?
Am I preventing pickling from working, and do I need to implement
__setstate__ etc?
Do I need repr, update and __init__?
Should I just use mutablemapping (it seems one shouldn't use UserDict
or DictMixin)? If so, how? The docs aren't exactly enlightening.
公认的答案是我的第一种方法,但由于它有一些问题,
因为没有人说过另一种方法,实际上是子类化字典,我在这里做一下。
公认的答案有什么问题?
这对我来说似乎是一个相当简单的要求:
我怎样才能使dict的子类尽可能“完美”?
最终目标是有一个键是小写的简单字典。
接受的答案实际上并没有继承dict,对此的测试失败:
>>> isinstance(MyTransformedDict([('Test', 'test')]), dict)
False
理想情况下,任何类型检查代码都将测试我们所期望的接口或抽象基类,但如果我们的数据对象被传递到测试dict的函数中,而我们不能“修复”这些函数,那么这段代码将失败。
人们可能会提出其他吹毛求疵的观点:
接受的答案也缺少类方法:fromkeys。
接受的答案也有一个多余的__dict__ -因此占用更多的内存空间:
>>> .foo = 'bar'
> > > s.__dict__
{'foo': 'bar', 'store': {'test': 'test'}}
实际上是继承dict
我们可以通过继承重用dict方法。我们所需要做的就是创建一个接口层,以确保如果键是字符串,则以小写形式传递到字典中。
如果我重写__getitem__/__setitem__,那么get/set将不起作用。我怎么让它们工作?当然,我不需要单独实现它们?
嗯,单独实现它们是这种方法的缺点,而使用MutableMapping的优点(请参阅已接受的答案),但它确实没有那么多工作。
首先,让我们提出Python 2和Python 3之间的区别,创建一个单例(_RaiseKeyError),以确保我们知道是否实际上得到了dict的参数。弹出,并创建一个函数来确保我们的字符串键是小写的:
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子类pickles很好:
>>> 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实现了6个接口函数(缺少fromkeys),使用dict子类实现了11个接口函数。我不需要实现__iter__或__len__,但我必须实现get、setdefault、pop、update、copy、__contains__和fromkeys——但这些都相当简单,因为我可以对大多数这些实现使用继承。
MutableMapping在Python中实现了一些dict在C中实现的东西——所以我希望dict子类在某些情况下性能更好。
我们在两种方法中都得到了一个免费的__eq__——只有当另一个dict全是小写时,这两种方法才假定相等——但同样,我认为dict子类比较起来更快。
简介:
子类化MutableMapping更简单,bug的机会更少,但速度更慢,占用更多内存(参见冗余dict),并且isinstance(x, dict)失败
继承dict更快,使用更少的内存,并传递isinstance(x, dict),但实现起来更复杂。
哪个更完美?这取决于你对完美的定义。