在Python中像属性一样访问OrderedDict的键

4 投票
1 回答
4003 浏览
提问于 2025-04-19 18:34

我想写一个容器类,具体要求是:

  1. 既可以像字典那样通过索引访问,比如 data['a'],也可以像访问属性那样使用,比如 data.a;这个问题可以在这里找到解决方案。
  2. 保持添加条目的顺序,比如通过继承 collections.OrderedDict 来实现;这个问题可以在这里找到解决方案。

我把第一个问题的解决方案调整为继承 collections.OrderedDict,而不是 dict,但这样做不成功;具体情况见下面。

from collections import OrderedDict

class mydict(OrderedDict):
    def __init__(self, *args, **kwargs):
        super(mydict, self).__init__(*args, **kwargs)
        self.__dict__ = self

D = mydict(a=1, b=2)

#D['c'] = 3 # fails
D.d    = 4

#print D    # fails

出现失败注释的那两行代码导致了以下错误:

    print D
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/collections.py", line 176, in __repr__
    return '%s(%r)' % (self.__class__.__name__, self.items())
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/collections.py", line 113, in items
    return [(key, self[key]) for key in self]
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/collections.py", line 76, in __iter__
    root = self.__root
AttributeError: 'struct' object has no attribute '_OrderedDict__root'

还有

    D['c'] = 3
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/collections.py", line 58, in __setitem__
    root = self.__root
AttributeError: 'mydict' object has no attribute '_OrderedDict__root'

有没有解决这个问题的方法?可以对 __init__ 函数进行修改吗?

1 个回答

8

首先,如果你只是想让这个功能正常工作,而不是想搞清楚你尝试的做法有什么问题,可以在PyPI、ActiveState等地方找到很多“AttrDict”的实现。你可以搜索一个,看看它的代码,或者直接安装并使用它。


那句 self.__dict__ = self 其实并不是你想要的。我知道有人推荐这样做,但这其实有一些根本性的问题。而你遇到的问题正是其中之一。

具体来说,这样做会导致你丢失所有现有的属性,包括 OrderedDict 本身使用的内部属性(比如你遇到错误的那个 __root)和所有对象都有的特殊属性(比如 __class__)。

当你使用 dict 时,你不会注意到这个问题——至少在CPython中是这样,因为 dict 是用C语言实现的,它需要的特殊成员存储在C结构中,而不是在 __dict__ 中,所以你不会丢失它们。但对于任何用Python实现的类,如果需要特殊成员,这个问题就会显现出来。


Python有一些特殊的方法可以用来自定义属性访问。你想做的就是实现这些方法:

class AttrDict(OrderedDict):
    def __getattr__(self, name):
        return self[name]
    def __setattr__(self, name, value):
        self[name] = value
    def __delattr__(self, name):
        del self[name]

不过,在这种情况下,即使这样做也不能解决所有问题,因为当 OrderedDict 中的代码尝试使用 __root 时,你的重写方法仍然会被调用!要解决这个问题,你需要在这些方法中只转发 部分 调用到你的字典,而不是 全部

而这个“部分”是复杂的,很难做到正确。一个更简单的解决方案是直接封装并委托给一个 OrderedDict,而不是继承它:

class AttrDict(object):
    def __init__(self, *args, **kwargs):
        self._od = OrderedDict(*args, **kwargs)
    def __getattr__(self, name):
        return self._od[name]
    def __setattr__(self, name, value):
        if name == '_od':
            self.__dict__['_od'] = value
        else:
            self._od[name] = value
    def __delattr__(self, name):
        del self._od[name]

或者,更简单的做法是直接把一个 OrderedDict 作为你的 __dict__——这其实是元类的典型用法,但如果你想快速解决问题,可以在 __init____new__ 中这样做。


后面的解决方案只是让你变成一个普通的对象,属性是有序的,而根本不是一个映射。但转发映射方法其实也很简单。实现 __getitem____setitem____delitem____iter____len__,让它们转发到 self._od,并从collections.MutableMapping 继承,以填充其余的API。

现在,如果你回去在PyPI上查找“AttrDict”,并点击实现,你会发现它们的做法正是这样:它们将 __getattr____getitem__ 都转发到一个 OrderedDict 的 __getitem__,等等。

撰写回答