如何完美地覆盖一个字典?
如何完美地覆盖一个字典?
如何尽可能地创建一个“完美”的dict子类?最终目标是创建一个简单的字典,其中键是小写的。
似乎应该有一些可以重写的基本方法来实现这个目标,但是根据我的所有研究和尝试,似乎并非如此:
- 如果我重写
__getitem__
/__setitem__
,那么get
/set
就无法正常工作。我应该如何让它们正常工作?我肯定不需要逐个实现它们吧? - 我是否阻止了pickle的工作,需要实现
__setstate__
等方法吗? - 我是否需要
repr
、update
和__init__
? - 我是否应该使用mutablemapping(似乎不应该使用
UserDict
或DictMixin
)?如果是这样,该如何使用?文档并没有提供明确的指导。
以下是我第一次尝试的代码,get()
不起作用,而且肯定还有许多其他小问题:
class arbitrary_dict(dict): """在访问键之前,应用任意的键转换函数的字典。""" def __keytransform__(self, key): return key # 重写的方法。列表来源于 # https://stackoverflow.com/questions/2390827/how-to-properly-subclass-dict def __init__(self, *args, **kwargs): self.update(*args, **kwargs) # 注意:我直接使用dict,因为super(dict, self)不起作用。 # 我不确定为什么,也许dict不是新式类。 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()
如何“完美”覆盖一个字典?
问题的出现原因:
- 如果重写__getitem__ / __setitem__,则无法使用get / set方法。如何使它们工作?是否需要单独实现它们?
- 是否阻止了pickle的工作,是否需要实现__setstate__等?
- 是否需要repr、update和__init__?
- 是否应该使用mutablemapping(似乎不应该使用UserDict或DictMixin)?如果是这样,应该如何使用?文档并没有提供具体的解释。
解决方法:
- 创建一个继承自dict的类,并通过创建一个接口层来确保将键以小写形式传入字典中。
- 实现__getitem__、__setitem__、__delitem__、get、setdefault、pop、update、__contains__和fromkeys等方法,确保键以小写形式传入字典中。
- 实现__repr__方法,以提高代码的可调试性。
以上方法可以创建一个尽可能完美的字典子类,其中键都是小写的。
通过使用继承自dict的类,可以重用dict的方法,并通过创建一个接口层来确保键以小写形式传入字典中。虽然这种方法需要实现一些额外的方法,但通过继承,可以轻松地获得一些方法,如len、clear、items、keys、popitem和values。
这种方法还可以正常进行pickle操作,并且可以自动生成合适的__repr__方法。
与使用mutablemapping相比,继承自dict的类可能更快,占用更少的内存,并且可以通过isinstance(x, dict)进行类型检查。然而,继承自mutablemapping的类更简单,并且有更少的机会产生错误,但在某些情况下可能更慢。
选择哪种方法取决于个人对“完美”的定义。
如何完美地覆盖一个字典?
要完美地覆盖一个字典,可以使用Python的ABC(抽象基类)以及collections.abc模块来编写一个像字典一样行为的对象。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(或其他内置类)。这通常没有意义,因为你实际上想做的是实现一个字典的接口,而这正是ABC的用途。
但是,用用户定义的类型实现这个接口通常会导致比使用内置类型更慢的字典操作,对于使用Python的人来说,这可能无关紧要。
有没有办法让isinstance(_, dict) == True?还是只能使用MutableMapping来构建,然后进行子类化?
那么,除了多出20行代码外,这种方法有什么好处呢?为什么不直接使用MyClass = type('MyClass', (dict,), {})
?
你应该写成if isinstance(t, collections.MutableMapping): print t, "can be used like a dict"
。不要检查对象的类型,而是检查接口。
不幸的是,这甚至包括Python标准库中的JSONEncoder。
有没有可能使用__getattr__和__setattr__作为get/set self.store
的别名来实现TransformedDict?(即obj.key = val
的行为与obj['key'] = val
一样。)我一直遇到递归错误或属性错误。
为什么要使用__keytransform__方法,而不直接使用参数key?
keytransform方法是为了让派生类重载的,就像"MyTransformedDict"的例子一样。
Jochen: 你的建议对于你自己的代码来说是正确的,但在其他情况下可能无法实现。例如,json模块明确检查isinstance(obj, dict)
。
为什么不在初始化器中使用self.store = dict(*args, **kwargs)
?
dict(*args, **kwargs)
中的解包操作符*
是错误的。dict()
的构造函数只接受一个位置参数,但可以接受多个关键字参数。因此,TransformedDict的__init__()
也是错误的。应该是__init__(self, seq=None, **kwargs)
。
如何“完美地”覆盖一个字典?
在尝试了两个推荐的方法之后,我选择了一个看起来有些可疑的中间路线来解决Python 2.7的问题。也许在Python 3中更容易,但对我来说:
class MyDict(MutableMapping): # ... the few __methods__ that mutablemapping requires # and then this monstrosity def __class__(self): return dict
这个方法我非常讨厌,但似乎符合我的需求,我的需求包括:
- 可以覆盖`**my_dict`
- 如果你从`dict`继承,这会绕过你的代码。尝试一下吧。
- 这使得[第二个选择](https://stackoverflow.com/a/39375731/16295)对我来说始终不可接受,因为这在Python代码中相当常见。
- 作为`isinstance(my_dict, dict)`的伪装
- `MutableMapping`单独是不行的,所以[第一个选择](https://stackoverflow.com/a/3387975/16295)不够。
- 如果你不需要这个功能,我强烈推荐[第一个选择](https://stackoverflow.com/a/3387975/16295),它简单而可预测。
- 完全可控的行为
- 所以我不能继承`dict`。
如果你需要将自己与其他人区分开来,我个人使用类似这样的方法(尽管我建议使用更好的命名):
def __am_i_me(self): return True 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](https://repl.it/repls/TraumaticToughCockatoo)
基本上:复制[当前的第二个选择](https://stackoverflow.com/a/39375731/16295),在每个方法中添加`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:显然我之前复制错了或者什么的。`__class__`不起作用,`__class__`起作用。[具体是什么意思的"**your_dict
将是空的"(如果你从`dict`继承)?我没有看到字典解包的任何问题...](https://stackoverflow.com/questions/57982946)。如果你实际上将数据放入父字典中(例如LowerDict),它将起作用 - 你将得到那个存储在字典中的数据。如果你没有(例如你想生成每次读取时填充的数据,比如{访问计数:"访问的堆栈跟踪"}),你会注意到`**your_dict`不会执行你的代码,因此它无法输出任何"特殊"内容。例如,你无法计算"读取"次数,因为它不会执行你的计数代码。`MutableMapping`可以实现这一点(如果可以使用它!),但它无法通过`isinstance(..., dict)`验证,因此我不能使用它。遗留软件,是的,遗留软件。
好吧,现在我明白你的意思了。我想我没有预料到会执行`**your_dict`的代码,但我发现非常有趣的是`MutableMapping`会执行这个操作。
是的。这对于许多事情是必要的(例如,我将RPC调用转换为以前的本地字典读取,并且由于某些原因必须按需执行此操作),而且似乎很少有人意识到这一点,即使`**some_dict`相当常见。至少在装饰器中经常发生这种情况,所以如果你有任何装饰器,如果你不考虑这一点,你就会立即面临看似不可能的错误行为。
也许我漏掉了什么,但是`def __class__()`的技巧似乎在Python 2和3中都不起作用,至少在问题[How to register implementation of abc.MutableMapping as a dict subclass?](https://stackoverflow.com/questions/57982946)中的示例代码中(做了一些修改以在这两个版本中工作)。我希望`isinstance(SpreadSheet(), dict)`能返回`True`。
嗯,我可能找到了让它起作用的办法:如果我还在类的`__init__()`方法中添加一个`self.__class__ = dict`,那么`isinstance(_, dict)`将返回`True`。`def __class__()`仍然是必需的,因为如果没有它,赋值会引发`TypeError: __class__ assignment only supported for heap types or ModuleType subclasses`。
有了`self.__class__`的赋值,你的`__class__`类方法的唯一作用是隐藏正常的`__class__`描述符。任何实际使用类方法的东西可能会失败,因为类方法没有任何意义。`__class__`不应该表现为任何类型的方法。
特别是,这个答案使用`__class__`类方法无法使`isinstance`检查像Groxx认为的那样起作用,所以要么那个巨大的遗留代码库不是真正依赖于这些isinstance检查通过,要么其他问题出现了。
我肯定是之前复制粘贴错了什么东西。是的,`__class__`不起作用,`__class__`起作用。(我有一个测试套件,确保这个方法起作用,不知道为什么我在这里搞错了)。证据:[https://repl.it/repls/UnitedScientificSequence](https://repl.it/repls/UnitedScientificSequence)
无论如何,谢谢你的怀疑!这让我更仔细地观察并编写了一些小测试,然后怀疑我之前做了什么事情,以便它之前起作用(或者在Python 2.7.10和2.7.16之间是否发生了一些变化)。耶!旧代码仍然运行。