如何“完美”重写一个字典?
我该如何创建一个“完美”的dict子类呢?我的最终目标是有一个简单的dict,它的键都是小写字母。
看起来应该有一些基本的方法可以重写,让这个功能实现,但根据我所有的研究和尝试,似乎并不是这样:
如果我重写了
__getitem__
/__setitem__
,那么get
/set
就不能用了。我该怎么让它们正常工作呢?难道我需要单独实现它们吗?我是不是阻止了序列化(pickling)的工作?我需要实现
__setstate__
等方法吗?我需要
repr
、update
和__init__
吗?我是不是应该 使用 mutablemapping(似乎不应该使用
UserDict
或DictMixin
)?如果是的话,怎么做呢?文档并没有给出明确的指导。
这是我第一次尝试的代码,get()
不工作,肯定还有很多其他小问题:
class arbitrary_dict(dict):
"""A dictionary that applies an arbitrary key-altering function
before accessing the keys."""
def __keytransform__(self, key):
return key
# Overridden methods. List from
# https://stackoverflow.com/questions/2390827/how-to-properly-subclass-dict
def __init__(self, *args, **kwargs):
self.update(*args, **kwargs)
# Note: I'm using dict directly, since super(dict, self) doesn't work.
# I'm not sure why, perhaps dict is not a new-style class.
def __getitem__(self, key):
return dict.__getitem__(self, self.__keytransform__(key))
def __setitem__(self, key, value):
return dict.__setitem__(self, self.__keytransform__(key), value)
def __delitem__(self, key):
return dict.__delitem__(self, self.__keytransform__(key))
def __contains__(self, key):
return dict.__contains__(self, self.__keytransform__(key))
class lcdict(arbitrary_dict):
def __keytransform__(self, key):
return str(key).lower()
6 个回答
在尝试了两个最热门的建议后,我选择了一条看起来不太靠谱的中间路线,适用于Python 2.7。也许Python 3更靠谱,但对我来说:
class MyDict(MutableMapping):
# ... the few __methods__ that mutablemapping requires
# and then this monstrosity
@property
def __class__(self):
return dict
我其实很讨厌这样做,但似乎符合我的需求,这些需求包括:
- 可以重写
**my_dict
- 如果你从
dict
继承,这样会绕过你的代码。试试看。 - 这让我在任何时候都无法接受 #2,因为在Python代码中这种情况非常常见。
- 如果你从
- 看起来像
isinstance(my_dict, dict)
- 完全可控的行为
- 所以我不能从
dict
继承。
- 所以我不能从
如果你需要让自己和别人区分开来,个人上我用的是这样的东西(不过我建议用更好的名字):
def __am_i_me(self):
return True
@classmethod
def __is_it_me(cls, other):
try:
return other.__am_i_me()
except Exception:
return False
只要你只需要在内部识别自己,这样做就能减少意外调用 __am_i_me
的可能,因为Python会对名字进行处理(外部调用这个类的任何东西都会把它改成 _MyDict__am_i_me
)。在实践和文化上,这比 _method
更私密。
到目前为止我没有任何抱怨,除了那个看起来很可疑的 __class__
重写。听到其他人遇到的问题我会很高兴,虽然我不完全理解后果。但到目前为止我没有遇到任何问题,这让我能够在很多地方迁移大量质量一般的代码,而不需要做任何更改。
作为证据: https://repl.it/repls/TraumaticToughCockatoo
基本上:复制 当前的 #2 选项,在每个方法中添加 print 'method_name'
语句,然后试试这个,看看输出:
d = LowerDict() # prints "init", or whatever your print statement said
print '------'
splatted = dict(**d) # note that there are no prints here
你会在其他场景中看到类似的行为。假设你的假 dict
是某种其他数据类型的包装器,那么就没有合理的方法将数据存储在后端字典中;**your_dict
将会是空的,无论其他方法做什么。
这在 MutableMapping
中工作正常,但一旦你从 dict
继承,就变得无法控制。
编辑:作为更新,这个已经运行了将近两年,没有出现过任何问题,处理了数十万(可能是几百万)行复杂的、遗留的Python代码。所以我对此很满意 :)
编辑2:显然我很久以前复制错了什么。 @classmethod __class__
不适用于 isinstance
检查 - @property __class__
可以: https://repl.it/repls/UnitedScientificSequence
我该如何尽可能完美地创建一个字典的子类?
最终目标是有一个简单的字典,所有的键都是小写字母。
如果我重写了
__getitem__
和__setitem__
,那么获取和设置就不能正常工作。我该怎么做才能让它们正常工作?难道我需要单独实现它们吗?我是否阻止了序列化的工作?我需要实现
__setstate__
等吗?我需要实现
repr
、update
和__init__
吗?我应该直接使用
mutablemapping
吗(似乎不应该使用UserDict
或DictMixin
)?如果是这样,我该怎么做?文档并没有很清楚地说明。
我接受的答案是我最初的想法,但由于它有一些问题,而且没有人提到替代方案,实际上是子类化一个 dict
,所以我在这里会这样做。
接受的答案有什么问题?
对我来说,这似乎是一个相当简单的请求:
我该如何尽可能完美地创建一个字典的子类?最终目标是有一个简单的字典,所有的键都是小写字母。
接受的答案实际上并没有子类化 dict
,而且对此的测试失败了:
>>> isinstance(MyTransformedDict([('Test', 'test')]), dict)
False
理想情况下,任何类型检查的代码都应该测试我们期望的接口或一个抽象基类,但如果我们的数据对象被传递到测试 dict
的函数中,而我们无法“修复”这些函数,这段代码就会失败。
其他可能的争论点:
- 接受的答案也缺少类方法:
fromkeys
。 接受的答案还有一个多余的
__dict__
,因此在内存中占用了更多空间:>>> s.foo = 'bar' >>> s.__dict__ {'foo': 'bar', 'store': {'test': 'test'}}
实际子类化 dict
我们可以通过继承重用字典的方法。我们需要做的就是创建一个接口层,确保如果键是字符串,就以小写形式传入字典。
如果我重写了
__getitem__
和__setitem__
,那么获取和设置就不能正常工作。我该怎么做才能让它们正常工作?难道我需要单独实现它们吗?
实际上,单独实现它们是这种方法的缺点,而使用 MutableMapping
是其优点(见接受的答案),但这并不是额外的工作。
首先,让我们考虑一下 Python 2 和 3 之间的区别,创建一个单例 (_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__
等吗?
序列化
这个字典子类的序列化工作得很好:
>>> 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
子类的比较会更快。
总结:
- 子类化
MutableMapping
更简单,出错的机会更少,但速度较慢,占用更多内存(见多余的字典),并且无法通过isinstance(x, dict)
。 - 子类化
dict
更快,占用更少内存,并且可以通过isinstance(x, dict)
,但实现起来复杂度更高。
哪种更完美?这取决于你对完美的定义。
你可以很简单地写一个像 dict
的对象,使用的是 ABC(抽象基类),这个功能在 collections.abc
模块里。它甚至会告诉你如果你漏掉了某个方法,所以下面是一个最简单的版本,可以让 ABC 不再发出警告。
from collections.abc import MutableMapping
class TransformedDict(MutableMapping):
"""A dictionary that applies an arbitrary key-altering
function before accessing the keys"""
def __init__(self, *args, **kwargs):
self.store = dict()
self.update(dict(*args, **kwargs)) # use the free update to set keys
def __getitem__(self, key):
return self.store[self._keytransform(key)]
def __setitem__(self, key, value):
self.store[self._keytransform(key)] = value
def __delitem__(self, key):
del self.store[self._keytransform(key)]
def __iter__(self):
return iter(self.store)
def __len__(self):
return len(self.store)
def _keytransform(self, key):
return key
你可以从 ABC 中获得一些免费的方法:
class MyTransformedDict(TransformedDict):
def _keytransform(self, key):
return key.lower()
s = MyTransformedDict([('Test', 'test')])
assert s.get('TEST') is s['test'] # free get
assert 'TeSt' in s # free __contains__
# free setdefault, __eq__, and so on
import pickle
# works too since we just use a normal dict
assert pickle.loads(pickle.dumps(s)) == s
我不建议直接继承 dict
(或其他内置类型)。这样做通常没有意义,因为你真正想要的是 实现一个 dict
的接口。而这正是 ABC 的用途。