如何在分配值时仅对嵌套字典实现自动修改?

时间:2017-02-08 20:03:28

标签: python dictionary autovivification

TL; DR
如何在为子键分配值时将超级键在Python dict中自动生成,而不检查子键时是否自动生成它们?

背景 通常在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中复制该行为。

2 个答案:

答案 0 :(得分:2)

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

my_dict[1][2] = 3

my_dict[1]会在字典上调用__getitem__。在那一点上,没有办法知道正在进行任务。只有序列中的最后一个[]__setitem__调用,除非mydict[1]存在,否则无法成功,因为否则,您指定的对象是什么?

所以不要使用autovivication。您可以使用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__调用。这样,我们从一开始就知道我们正在进行一项任务,并且可以在不依赖自动生存的情况下继续进行。

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),]

答案 1 :(得分: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)
>>> {}