Python中的嵌套字典,如何隐式创建不存在的中间容器?

3 投票
2 回答
951 浏览
提问于 2025-04-16 05:39

我想创建一个多态结构,这个结构可以随时生成,而且输入的内容要尽量少,同时也要容易阅读。例如:

a.b = 1
a.c.d = 2
a.c.e = 3
a.f.g.a.b.c.d = cucu
a.aaa = bau

我不想创建一个中间容器,比如:

a.c = subobject()
a.c.d = 2
a.c.e = 3

我的问题和这个很相似:

实现嵌套字典的最佳方法是什么?

但我对那里的解决方案不太满意,因为我觉得有个bug:
即使你不想创建某些项目,它们也会被生成。比如说,你想比较两个多态结构:在第二个结构中会创建第一个结构中存在的任何属性,而这些属性只是被检查了一下。例如:

a = {1:2, 3: 4}
b = {5:6}

# now compare them:

if b[1] == a[1]
    # whoops, we just created b[1] = {} !

我还想要尽可能简单的表示法

a.b.c.d = 1
    # neat
a[b][c][d] = 1
    # yuck

我尝试从对象类派生……但我还是没法避免上面提到的bug,属性在尝试读取时就会被创建:简单使用dir()就会尝试创建像methods这样的属性……就像这个明显有问题的例子:

class KeyList(object):
    def __setattr__(self, name, value):
        print "__setattr__ Name:", name, "value:", value
        object.__setattr__(self, name, value)
    def __getattribute__(self, name):
        print "__getattribute__ called for:", name
        return object.__getattribute__(self, name)
    def __getattr__(self, name):
        print "__getattr__ Name:", name
        try:
            ret = object.__getattribute__(self, name)
        except AttributeError:
            print "__getattr__ not found, creating..."
            object.__setattr__(self, name, KeyList())
            ret = object.__getattribute__(self, name)
        return ret

>>> cucu = KeyList()
>>> dir(cucu)
__getattribute__ called for: __dict__
__getattribute__ called for: __members__
__getattr__ Name: __members__
__getattr__ not found, creating...
__getattribute__ called for: __methods__
__getattr__ Name: __methods__
__getattr__ not found, creating...
__getattribute__ called for: __class__

谢谢,真的很感谢!

附言:到目前为止,我找到的最佳解决方案是:

class KeyList(dict):
    def keylset(self, path, value):
        attr = self
        path_elements = path.split('.')
        for i in path_elements[:-1]:
            try:
                attr = attr[i]
            except KeyError:
                attr[i] = KeyList()
                attr = attr[i]
        attr[path_elements[-1]] = value

# test
>>> a = KeyList()
>>> a.keylset("a.b.d.e", "ferfr")
>>> a.keylset("a.b.d", {})
>>> a
{'a': {'b': {'d': {}}}}

# shallow copy
>>> b = copy.copy(a)
>>> b
{'a': {'b': {'d': {}}}}
>>> b.keylset("a.b.d", 3)
>>> b
{'a': {'b': {'d': 3}}}
>>> a
{'a': {'b': {'d': 3}}}

# complete copy
>>> a.keylset("a.b.d", 2)
>>> a
{'a': {'b': {'d': 2}}}
>>> b
{'a': {'b': {'d': 2}}}
>>> b = copy.deepcopy(a)
>>> b.keylset("a.b.d", 4)
>>> b
{'a': {'b': {'d': 4}}}
>>> a
{'a': {'b': {'d': 2}}}

2 个回答

1

如果你想找一个不那么动态的解决方案,而是更接近你目前为止找到的最佳方案,可以看看Ian Bicking的formencode中的variabledecode是否符合你的需求。这个包主要是用来处理网页表单和验证的,但里面的一些方法看起来和你想要的挺相似的。
即使没有其他用途,它也可以作为你自己实现的一个参考。

这里有一个小例子:

>>> from formencode.variabledecode import variable_decode, variable_encode
>>>
>>> d={'a.b.c.d.e': 1}
>>> variable_decode(d)
{'a': {'b': {'c': {'d': {'e': 1}}}}}
>>>
>>> d['a.b.x'] = 3
>>> variable_decode(d)
{'a': {'b': {'c': {'d': {'e': 1}}, 'x': 3}}}
>>>
>>> d2 = variable_decode(d)
>>> variable_encode(d2) == d
True
1

我觉得至少你需要在 __getattr__ 里检查一下,看看请求的属性名是否以 __ 开头和结尾。因为符合这个条件的属性是 Python 的一些标准接口,所以你不应该去实例化这些属性。不过,即使这样,你还是会实现一些接口属性,比如 next。如果你把这个对象传给某个使用鸭子类型的函数来检查它是否是一个迭代器,可能会抛出异常。

其实,最好是创建一个“白名单”,列出有效的属性名,可以直接写成一个集合,或者用简单的公式来定义,比如 name.isalpha() and len(name) == 1 适用于你例子中的单字母属性。对于更实际的实现,你可能想定义一组适合你代码工作领域的名称。

另外一个选择是确保你没有动态创建任何协议中包含的属性名,因为 next 就是迭代协议的一部分。collections 模块 中的 ABCs 方法包含了一部分列表,但我不知道完整的列表在哪里。

你还需要跟踪对象是否创建了任何子节点,这样你才能知道如何与其他类似对象进行比较。

如果你想让比较避免自动生成属性,你需要在类中实现 __cmp__ 方法,或者丰富的比较方法,来检查被比较对象的 __dict__

我隐约觉得还有一些复杂的情况我没有考虑到,这也不奇怪,因为这并不是 Python 应该工作的方式。要小心,想想这种方法带来的额外复杂性是否值得。

撰写回答