如何“完美”覆盖字典?

Posted

技术标签:

【中文标题】如何“完美”覆盖字典?【英文标题】:How to "perfectly" override a dict? 【发布时间】:2011-03-24 04:40:32 【问题描述】:

如何使 dict 的子类尽可能“完美”?最终目标是有一个简单的 dict,其中的键是小写的。

似乎应该有一些我可以重写的小原语来完成这项工作,但根据我所有的研究和尝试,情况似乎并非如此:

如果我override __getitem__/__setitem__,那么get/set 不起作用。我怎样才能让它们工作?当然我不需要单独实现它们?

我是否阻止酸洗工作,我是否需要实现__setstate__ 等?

我是need repr, update and __init__吗?

我应该只使用use mutablemapping(似乎不应该使用UserDictDictMixin)?如果是这样,怎么做?这些文档并不完全有启发性。

这是我的第一次尝试,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://***.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()

【问题讨论】:

我认为 __keytransform__() 应该是静态的。不错的方法。 (前置@staticmethod) 相关:***.com/q/1392396 这是我最喜欢的关于 SO 的问题之一。 【参考方案1】:

您可以使用collections.abc 模块中的ABCs(抽象基类)轻松编写行为类似于dict 的对象。它甚至会告诉您是否错过了某个方法,因此以下是关闭 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 的用途。

【讨论】:

问题——如果不使用用户定义的类型来实现这个接口,通常会导致使用内置类型的类 dict 操作更慢? 有没有办法做到这一点,以便 isinstance(_, dict) == True ?或者你只是使用 Mutable Mapping 来构造然后子类? @NeilG 那么这种方法除了增加 20 行之外,在MyClass = type('MyClass', (dict,), ) 上还有什么好处? @AndyHayden:你应该写if isinstance(t, collections.MutableMapping): print t, "can be used like a dict"。不要检查对象的类型,检查接口。 @NeilG 这很遗憾在 python 标准库中包含 JSONEncoder - github.com/python-git/python/blob/…【参考方案2】:

如何使 dict 的子类尽可能“完美”?

最终目标是有一个简单的字典,其中的键是小写的。

如果我覆盖 __getitem__/__setitem__,那么 get/set 将不起作用。如何 我让他们工作吗?当然我不需要实现它们 个人?

我是否阻止酸洗工作,我是否需要实施 __setstate__ 等?

我需要repr、update和__init__吗?

我应该只使用mutablemapping吗(似乎不应该使用UserDictDictMixin)?如果是这样,怎么做?这些文档并不完全有启发性。

接受的答案将是我的第一种方法,但由于它存在一些问题, 并且由于没有人解决替代方案,实际上是对dict 的子类化,我将在此处执行此操作。

接受的答案有什么问题?

这对我来说似乎是一个相当简单的要求:

如何使 dict 的子类尽可能“完美”? 最终目标是有一个简单的字典,其中的键是小写的。

接受的答案实际上并不是dict 的子类,对此的测试失败:

>>> isinstance(MyTransformedDict([('Test', 'test')]), dict)
False

理想情况下,任何类型检查代码都将测试我们期望的接口或抽象基类,但如果我们的数据对象被传递到正在测试dict 的函数中 - 我们无法“修复" 那些函数,这段代码会失败。

人们可能会提出的其他问题:

接受的答案也缺少类方法:fromkeys

接受的答案也有一个多余的__dict__ - 因此占用了更多的内存空间:

>>> s.foo = 'bar'
>>> s.__dict__
'foo': 'bar', 'store': 'test': 'test'

实际上是子类化dict

我们可以通过继承重用dict方法。我们需要做的就是创建一个接口层,以确保如果键是字符串,则以小写形式将键传递到 dict 中。

如果我覆盖 __getitem__/__setitem__,那么 get/set 将不起作用。我如何让它们工作?当然我不需要单独实现它们吗?

嗯,单独实现它们是这种方法的缺点和使用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__())

我们对引用键的任何方法或特殊方法使用几乎是样板的方法,但除此之外,通过继承,我们得到方法:lenclearitemskeys、@987654351 @ 和 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__

我需要代表、更新和__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(似乎不应该使用UserDictDictMixin)?如果是这样,怎么做?这些文档并不完全有启发性。

是的,这些是几行代码,但它们旨在全面。我的第一个倾向是使用公认的答案, 如果它有问题,然后我会看看我的答案 - 因为它有点复杂,而且没有 ABC 可以帮助我正确设置界面。

过早的优化在寻求性能方面会变得更加复杂。 MutableMapping 更简单 - 所以它立即获得优势,其他一切都相同。尽管如此,为了列出所有差异,让我们进行比较和对比。

我应该补充一点,有人推动将类似的字典放入collections 模块,但it was rejected。您可能应该这样做:

my_dict[transform(key)]

它应该更容易调试。

比较和对比

MutableMapping(缺少fromkeys)实现了 6 个接口函数,dict 子类实现了 11 个接口函数。我不需要实现__iter____len__,但我必须实现getsetdefaultpopupdatecopy__contains__fromkeys -但这些都相当简单,因为我可以对大多数实现使用继承。

MutableMapping 在 Python 中实现了一些 dict 在 C 中实现的东西 - 所以我希望 dict 子类在某些情况下性能更高。

我们在这两种方法中都得到了一个免费的__eq__——这两种方法都假设只有当另一个dict全是小写时才相等——但我认为dict子类会比较快。

总结:

子类化MutableMapping 更简单,出现错误的机会更少,但速度较慢,占用更多内存(请参阅冗余字典),并且失败isinstance(x, dict) 子类化dict 更快,使用更少的内存,并通过isinstance(x, dict),但实现起来更复杂。

哪个更完美?这取决于你对完美的定义。

【讨论】:

立即想到的两种方法是在 __slots__ 中声明 store 属性,或者可能将 __dict__ 重用为 store,但这混合了语义,这是另一个潜在的批评点。跨度> 编写一个采用方法并在第一个argumentmtn 上使用ensure_lower 的装饰器会不会更容易(这始终是关键)?那么它会是相同数量的覆盖,但它们都将采用__getitem__ = ensure_lower_decorator(super(LowerDict, self).__getitem__) 的形式。 感谢您 - 收到 pop 和 fromkeys 的警告,提示它们与基类方法的签名不匹配。 @Mr_and_Mrs_D 我添加了copy 的实现 - 我认为应该这样做,不是吗?我认为它应该测试接口 - 例如pandas DataFrame 对象不是 Mapping 实例(最后检查),但它确实有项目/iteritems。 刚刚做到了! ***.com/a/47361653/16295 和 repl.it/repls/TraumaticToughCockatoo【参考方案3】:

我的要求有点严格:

我必须保留大小写信息(字符串是显示给用户的文件的路径,但它是一个 Windows 应用程序,因此在内部所有操作都必须不区分大小写) 我需要尽可能小的键(它确实对内存性能产生了影响,从 370 个中减少了 110 mb)。这意味着缓存小写版本的密钥不是一种选择。 我需要尽可能快地创建数据结构(再次在性能上产生差异,这次是速度)。我不得不使用内置的

我最初的想法是将我们笨重的 Path 类替换为不区分大小写的 unicode 子类 - 但是:

证明很难做到这一点 - 请参阅:A case insensitive string class in python 结果表明,显式 dict 键处理使代码冗长和混乱 - 并且容易出错(结构到处传递,不清楚它们是否有 CIStr 实例作为键/元素,容易忘记加上 some_dict[CIstr(path)] 是丑)

所以我最后不得不写下那个不区分大小写的字典。感谢@AaronHall 的code,让这变得简单了10 倍。

class CIstr(unicode):
    """See https://***.com/a/43122305/281545, especially for inlines"""
    __slots__ = () # does make a difference in memory performance

    #--Hash/Compare
    def __hash__(self):
        return hash(self.lower())
    def __eq__(self, other):
        if isinstance(other, CIstr):
            return self.lower() == other.lower()
        return NotImplemented
    def __ne__(self, other):
        if isinstance(other, CIstr):
            return self.lower() != other.lower()
        return NotImplemented
    def __lt__(self, other):
        if isinstance(other, CIstr):
            return self.lower() < other.lower()
        return NotImplemented
    def __ge__(self, other):
        if isinstance(other, CIstr):
            return self.lower() >= other.lower()
        return NotImplemented
    def __gt__(self, other):
        if isinstance(other, CIstr):
            return self.lower() > other.lower()
        return NotImplemented
    def __le__(self, other):
        if isinstance(other, CIstr):
            return self.lower() <= other.lower()
        return NotImplemented
    #--repr
    def __repr__(self):
        return '0(1)'.format(type(self).__name__,
                                 super(CIstr, self).__repr__())

def _ci_str(maybe_str):
    """dict keys can be any hashable object - only call CIstr if str"""
    return CIstr(maybe_str) if isinstance(maybe_str, basestring) else maybe_str

class LowerDict(dict):
    """Dictionary that transforms its keys to CIstr instances.
    Adapted from: https://***.com/a/39375731/281545
    """
    __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, 'iteritems'):
            mapping = getattr(mapping, 'iteritems')()
        return ((_ci_str(k), v) for k, v in
                chain(mapping, getattr(kwargs, 'iteritems')()))
    def __init__(self, mapping=(), **kwargs):
        # dicts take a mapping or iterable as their optional first argument
        super(LowerDict, self).__init__(self._process_args(mapping, **kwargs))
    def __getitem__(self, k):
        return super(LowerDict, self).__getitem__(_ci_str(k))
    def __setitem__(self, k, v):
        return super(LowerDict, self).__setitem__(_ci_str(k), v)
    def __delitem__(self, k):
        return super(LowerDict, self).__delitem__(_ci_str(k))
    def copy(self): # don't delegate w/ super - dict.copy() -> dict :(
        return type(self)(self)
    def get(self, k, default=None):
        return super(LowerDict, self).get(_ci_str(k), default)
    def setdefault(self, k, default=None):
        return super(LowerDict, self).setdefault(_ci_str(k), default)
    __no_default = object()
    def pop(self, k, v=__no_default):
        if v is LowerDict.__no_default:
            # super will raise KeyError if no default and key does not exist
            return super(LowerDict, self).pop(_ci_str(k))
        return super(LowerDict, self).pop(_ci_str(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__(_ci_str(k))
    @classmethod
    def fromkeys(cls, keys, v=None):
        return super(LowerDict, cls).fromkeys((_ci_str(k) for k in keys), v)
    def __repr__(self):
        return '0(1)'.format(type(self).__name__,
                                 super(LowerDict, self).__repr__())

隐式与显式仍然是一个问题,但一旦尘埃落定,重命名属性/变量以 ci 开头(以及解释 ci 代表不区分大小写的大胖文档注释)我认为是一个完美的解决方案 - 作为代码必须充分意识到我们正在处理不区分大小写的底层数据结构。 这有望修复一些难以重现的错误,我怀疑这些错误归结为区分大小写。

欢迎评论/更正:)

【讨论】:

CIstr 的 __repr__ 应该使用父类的 __repr__ 来通过 eval(repr(obj)) == obj 测试(我认为现在不行)而不是依赖于 @ 987654329@. 还可以查看total_ordering class decorator - 这将从您的 unicode 子类中消除 4 个方法。但是 dict 子类看起来非常巧妙地实现。 :P 谢谢@AaronHall - 是你实现了:P Re:总排序 - 我故意按照 Raymond Hettinger 的建议编写了内联的方法:***.com/a/43122305/281545。回复:repr:我记得读过一条评论(由一些核心开发 IIRC 撰写),尝试让 repr 通过该测试并不值得麻烦(这很麻烦) - 更好地关注它尽可能提供信息(但仅此而已) 我将允许您使用多余的比较方法(您应该在回答中记下它),但在 您的 情况下,CIstr.__repr__ 可以通过repr 测试非常简单,它应该使调试变得更好。我还会为您的听写添加__repr__。我会在我的回答中进行演示。 @AaronHall:我在 CIstr 中添加了__slots__ - 确实对性能产生了影响(CIstr 并不意味着子类化或确实在 LowerDict 之外使用,应该是静态嵌套的最终类)。仍然不确定如何优雅地解决 repr 问题(刺痛可能包含 '" 引号的组合)【参考方案4】:

你要做的就是

class BatchCollection(dict):
    def __init__(self, *args, **kwargs):
        dict.__init__(*args, **kwargs)

class BatchCollection(dict):
    def __init__(self, inpt=):
        super(BatchCollection, self).__init__(inpt)

我个人使用的示例用法

### EXAMPLE
class BatchCollection(dict):
    def __init__(self, inpt=):
        dict.__init__(*args, **kwargs)

    def __setitem__(self, key, item):
        if (isinstance(key, tuple) and len(key) == 2
                and isinstance(item, collections.Iterable)):
            # self.__dict__[key] = item
            super(BatchCollection, self).__setitem__(key, item)
        else:
            raise Exception(
                "Valid key should be a tuple (database_name, table_name) "
                "and value should be iterable")

注意:仅在python3中测试过

【讨论】:

这些都不适合我:__init__ 的第一个变体给出了错误“TypeError: 'dict' 对象的描述符 '__init__' 需要一个参数”。如果我尝试其他版本的__init__ 并覆盖__setitem__,就像你所做的那样,我会得到“AttributeError:'super' object has no attribute '_BatchCollection__set__item'”,这并不奇怪:方法名称修改已经开始。我不能了解这怎么能被投票 6 次。【参考方案5】:

在尝试了toptwo 的两个建议后,我决定为 Python 2.7 选择一条看似阴暗的中间路线。也许 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) 单独排除 MutableMapping,所以 #1 是不够的 我衷心推荐#1,如果你不需要这个,它简单且可预测 完全可控的行为 所以我不能从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)。在实践和文化上都比_methods 更私密。

到目前为止,除了看起来很阴暗的 __class__ 覆盖之外,我没有任何抱怨。我会很高兴听到其他人遇到的任何问题,但我不完全理解后果。但到目前为止,我没有遇到任何问题,这让我可以在很多地方迁移大量中等质量的代码,而无需任何更改。


作为证据:https://repl.it/repls/TraumaticToughCockatoo

基本上:复制the current #2 option,在每个方法中添加print 'method_name' 行,然后试试这个并观察输出:

d = LowerDict()  # prints "init", or whatever your print statement said
print '------'
splatted = dict(**d)  # note that there are no prints here

您会在其他情况下看到类似的行为。假设您的 fake-dict 是其他数据类型的包装器,因此没有合理的方法将数据存储在 backing-dict 中; **your_dict 将是空的,不管其他方法做什么。

这适用于MutableMapping,但一旦您从dict 继承,它就会变得无法控制。


编辑:作为一个更新,它已经运行了近两年,没有一个问题,在几十万(嗯,可能是几百万)行复杂的、遗留下来的 python 上。所以我很满意:)

编辑 2:显然我很久以前就抄错了。 @classmethod __class__ 不适用于 isinstance 检查 - @property __class__ 可以:https://repl.it/repls/UnitedScientificSequence

【讨论】:

"**your_dict 将是空的" 到底是什么意思(如果您从dict 继承)?我没有看到 dict 解包有任何问题... 如果您实际上将数据放入父字典(就像 LowerDict 一样),它可以工作 - 您将获得该字典存储的数据。如果您(假设您想动态生成数据,例如每次读取时填充的 access_count: "stack trace of access"),您会注意到@987654351 @ 不执行你的代码,所以它不能输出任何“特殊”的东西。例如。您不能计算“读取”,因为它不执行您的读取计数代码。 MutableMapping 确实 工作(如果可以的话,使用它!),但它失败了isinstance(..., dict) 所以我不能使用它。是的旧软件。 好的,我明白你的意思了。我想我没想到会使用**your_dict 执行代码,但我发现MutableMapping 会这样做非常有趣。 是的。这对于许多事情是必要的(例如,我将 RPC 调用填充到过去的本地字典读取中,并且必须根据 Reasons™ 的需求进行),而且似乎很少有人知道它,即使**some_dict 相当普遍。至少它在装饰器中经常发生,所以如果你有任何,如果你不考虑它,你就会立即面临看似不可能的不当行为的风险。 也许我遗漏了一些东西,但 def __class__() 技巧似乎不适用于 Python 2 或 3,至少对于问题 How to register implementation of abc.MutableMapping as a dict subclass? 中的示例代码(修改为在两个版本中工作)。我希望isinstance(SpreadSheet(), dict) 返回True

以上是关于如何“完美”覆盖字典?的主要内容,如果未能解决你的问题,请参考以下文章

C# 覆盖字典 ContainsKey

导航抽屉如何覆盖操作栏?

For循环正在覆盖列表中的字典值[重复]

为啥浮点字典键可以覆盖具有相同值的整数键?

如何在InDesign文档中找到所有对象样式覆盖

Python列表到字典 - 没有值覆盖