7

TL; DR
在为子键分配值时,如何在 Python 字典中自动激活超级键,而不在检查子键时自动激活它们?

背景: 通常在 Python 中,在嵌套字典中设置值需要手动确保更高级别的键存在,然后再分配给它们的子键。那是,

my_dict[1][2] = 3

如果不先做类似的事情,将无法按预期可靠地工作

if 1 not in my_dict:
    my_dict[1] = {}

my_dict现在,可以通过创建一个覆盖的类的实例来设置一种自动生存__missing__,如https://stackoverflow.com/a/19829714/6670909所示。

问题:但是,如果您检查这种嵌套字典中是否存在子键,该解决方案会静默自动激活更高级别的键。这导致了以下不幸:

>>> vd = Vividict()
>>> 1 in vd
False
>>> 2 in vd[1]
False
>>> 1 in vd
True

我怎样才能避免这种误导性的结果?顺便说一下,在 Perl 中,我可以通过做

no autovivification qw/exists/;

如果可能的话,基本上我想在 Python 中复制这种行为。

4

2 回答 2

2

这不是一个容易解决的问题,因为在您的示例中:

my_dict[1][2] = 3

my_dict[1]导致__getitem__对字典的调用。那时没有办法知道正在分配任务。只有[]序列中的最后一个是__setitem__调用,除非存在,否则它不会成功mydict[1],因为否则,你分配给什么对象?

所以不要使用自动复活。您可以改用setdefault()常规的dict.

my_dict.setdefault(1, {})[2] = 3

现在这并不完全漂亮,尤其是当您嵌套更深时,因此您可以编写一个辅助方法:

class MyDict(dict):
    def nest(self, keys, value):
       for key in keys[:-1]:
          self = self.setdefault(key, {})
       self[keys[-1]] = value

 my_dict = MyDict()
 my_dict.nest((1, 2), 3)       # my_dict[1][2] = 3

但更好的方法是将其包装成一个新__setitem__的,一次获取所有索引,而不是需要诱导自动激活的中间__getitem__调用。这样,我们从一开始就知道我们正在执行一项任务,并且可以在不依赖 autovivication 的情况下继续进行。

class MyDict(dict):
    def __setitem__(self, keys, value):
       if not isinstance(keys, tuple):
           return dict.__setitem__(self, keys, value)
       for key in keys[:-1]:
          self = self.setdefault(key, {})
       dict.__setitem__(self, keys[-1], value)

my_dict = MyDict()
my_dict[1, 2] = 3

为了保持一致性,您还可以提供__getitem__接受元组中的键,如下所示:

def __getitem__(self, keys):
   if not isinstance(keys, tuple):
       return dict.__getitem__(self, keys)
   for key in keys:
       self = dict.__getitem__(self, key)
   return self

我能想到的唯一缺点是我们不能轻松地将元组用作字典键:我们必须将其写为,例如my_dict[(1, 2),].

于 2017-02-08T20:07:42.183 回答
0

正确的答案是:不要在 Python 中这样做,因为显式优于隐式。

但是,如果您真的想要不保留空子字典的自动激活,可以模拟 Python 中的行为。

try:
    from collections import MutableMapping
except:
    from collections.abc import MutableMapping


class AutoDict(MutableMapping, object):
    def __init__(self, *args, **kwargs):
        super(AutoDict, self).__init__()
        self.data = dict(*args, **kwargs)

    def __getitem__(self, key):
        if key in self.data:
            return self.data.__getitem__(key)
        else:
            return ChildAutoDict(parent=self, parent_key=key)

    def __setitem__(self, key, value):
        return self.data.__setitem__(key, value)

    def __delitem__(self, key):
        return self.data.__delitem__(key)

    def __iter__(self):
        return self.data.__iter__()

    def __len__(self):
        return self.data.__len__()

    def keys(self):
        return self.data.keys()

    def __contains__(self, key):
       return data.__contains__(key)

    def __str__(self):
        return str(self.data)

    def __unicode__(self):
        return unicode(self.data)

    def __repr__(self):
        return repr(self.data)

class ChildAutoDict(AutoDict):
    def __init__(self, parent, parent_key):
        super(ChildAutoDict, self).__init__()
        self.parent = parent
        self.parent_key = parent_key

    def __setitem__(self, key, value):
        if self.parent is not None and not self.parent_key in self.parent:
            # if parent got a new key in the meantime,
            # don't add ourselves
            self.parent.data[self.parent_key] = self
        else:
           self.parent = None
        return self.data.__setitem__(key, value)

    def __delitem__(self, key):
        ret = self.data.__delitem__(key)
        # only remove ourselves from the parent if we are 
        # still occupying our slot.
        if not self and self.parent and self is self.parent[parent_key]:
            self.parent.data.pop(self.parent_key)
        return ret

您从 中得到的__getitem__()本质上是一个字典外观,它仅在自身不为空时才将自身添加到父字典中,并在变为空时将自身移除。

一旦你在中间的某个地方分配了一个“正常”字典,所有这些——当然——就会停止工作,即d[2] = {}d[2][3] = {}不再工作等等。

我还没有真正彻底地测试过这个,所以要小心更多的陷阱。

d = AutoDict()

print(1 in d)
>>> False
print(d)
>>> {}

print(d[2][3])
>>> {}
print(d[2])
>>> {}
print(d)
>>> {}

d[2][3] = 1
print(d)
>>> {2: {3: 1}}

del d[2][3]
print(d)
>>> {}
于 2017-02-08T21:24:23.530 回答