在Python中像属性一样访问OrderedDict的键
我想写一个容器类,具体要求是:
- 既可以像字典那样通过索引访问,比如
data['a']
,也可以像访问属性那样使用,比如data.a
;这个问题可以在这里找到解决方案。 - 保持添加条目的顺序,比如通过继承
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 个回答
首先,如果你只是想让这个功能正常工作,而不是想搞清楚你尝试的做法有什么问题,可以在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__
,等等。