像属性一样访问字典键?

422 投票
34 回答
358145 浏览
提问于 2025-04-16 11:44

我觉得用 obj.foo 来访问字典的键比用 obj['foo'] 更方便,所以我写了这个小代码:

class AttributeDict(dict):
    def __getattr__(self, attr):
        return self[attr]
    def __setattr__(self, attr, value):
        self[attr] = value

不过,我想Python之所以不直接提供这种功能,肯定是有原因的。这样访问字典的键有什么需要注意的地方和潜在的问题呢?

34 个回答

97

我来回答被问到的问题

为什么Python不直接提供这个功能?

我猜这和Python之禅有关:“应该有一种——最好只有一种——明显的方法来做这件事。” 这样的话,就会出现两种明显的方式来访问字典中的值:obj['key']obj.key

注意事项和陷阱

这可能会导致代码不够清晰,容易让人困惑。比如,下面的代码可能会让其他人在维护你的代码时感到困惑,甚至你自己在一段时间后再回来看时也会感到迷惑。再次引用Python之禅:“可读性很重要!”

>>> KEY = 'spam'
>>> d[KEY] = 1
>>> # Several lines of miscellaneous code here...
... assert d.spam == 1

如果d被创建或者 KEY被定义或者 d[KEY]在很远的地方被赋值,而你在使用d.spam时,这很容易让人感到困惑,因为这不是一个常用的写法。我知道这可能会让我感到困惑。

另外,如果你像下面这样改变KEY的值(但忘了改变d.spam),你现在得到的结果是:

>>> KEY = 'foo'
>>> d[KEY] = 1
>>> # Several lines of miscellaneous code here...
... assert d.spam == 1
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
AttributeError: 'C' object has no attribute 'spam'

在我看来,这样做不值得。

其他事项

正如其他人提到的,你可以使用任何可哈希的对象(不仅仅是字符串)作为字典的键。例如,

>>> d = {(2, 3): True,}
>>> assert d[(2, 3)] is True
>>> 

这是合法的,但

>>> C = type('C', (object,), {(2, 3): True})
>>> d = C()
>>> assert d.(2, 3) is True
  File "<stdin>", line 1
  d.(2, 3)
    ^
SyntaxError: invalid syntax
>>> getattr(d, (2, 3))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: getattr(): attribute name must be string
>>> 

则不合法。这让你可以使用所有可打印字符或其他可哈希对象作为字典的键,而在访问对象属性时你就没有这个选择。这使得像Python Cookbook (第9章)中的缓存对象元类这样的魔法成为可能。

我来发表一下个人看法

我更喜欢spam.eggs这种写法,而不是spam['eggs'](我觉得看起来更简洁),而当我遇到namedtuple时,我真的开始渴望这种功能。但能够做到以下这一点的便利性更胜一筹。

>>> KEYS = 'spam eggs ham'
>>> VALS = [1, 2, 3]
>>> d = {k: v for k, v in zip(KEYS.split(' '), VALS)}
>>> assert d == {'spam': 1, 'eggs': 2, 'ham': 3}
>>>

这是一个简单的例子,但我经常发现自己在不同的情况下使用字典,而不是使用obj.key这种写法(例如,当我需要从XML文件中读取偏好设置时)。在其他情况下,当我想创建一个动态类并为其添加一些属性以提升美观时,我仍然选择使用字典,以保持一致性,从而提高可读性。

我相信提问者早已对此感到满意,但如果他仍然想要这个功能,我建议他下载一些提供此功能的库:

  • Bunch是我比较熟悉的。它是dict的子类,所以你可以使用所有的功能。
  • AttrDict看起来也不错,但我不太熟悉它,也没有像研究Bunch那样仔细研究过它的源代码。
  • Addict正在积极维护,提供类似属性的访问方式和更多功能。
  • 正如Rotareti在评论中提到的,Bunch已经被弃用,但有一个活跃的分支叫Munch

不过,为了提高代码的可读性,我强烈建议他不要混合使用不同的写法。如果他更喜欢这种写法,那么他应该简单地创建一个动态对象,添加他想要的属性,然后就可以了:

>>> C = type('C', (object,), {})
>>> d = C()
>>> d.spam = 1
>>> d.eggs = 2
>>> d.ham = 3
>>> assert d.__dict__ == {'spam': 1, 'eggs': 2, 'ham': 3}


我来更新一下,回答评论中的后续问题

在评论中,Elmo问道:

如果你想更深入一点呢?(指的是type(...))

虽然我从未使用过这种情况(我通常倾向于使用嵌套的dict以保持一致性),但以下代码是可以工作的:

>>> C = type('C', (object,), {})
>>> d = C()
>>> for x in 'spam eggs ham'.split():
...     setattr(d, x, C())
...     i = 1
...     for y in 'one two three'.split():
...         setattr(getattr(d, x), y, i)
...         i += 1
...
>>> assert d.spam.__dict__ == {'one': 1, 'two': 2, 'three': 3}
128

如果你使用数组的写法,可以把所有合法的字符串字符作为键的一部分。比如说,obj['!#$%^&*()_']

418

更新 - 2020

自从这个问题提出快十年前,Python本身发生了很多变化。

虽然我最初的回答在某些情况下仍然有效(比如一些老旧项目仍在使用旧版Python,或者需要处理动态字符串键的字典),但我认为一般来说,Python 3.7引入的数据类是解决大多数AttrDict使用场景的明显且正确的选择。

最初的回答

最好的做法是:

class AttrDict(dict):
    def __init__(self, *args, **kwargs):
        super(AttrDict, self).__init__(*args, **kwargs)
        self.__dict__ = self

一些优点:

  • 它确实有效!
  • 没有字典类的方法被覆盖(例如,.keys()可以正常工作,当然,前提是你没有给它们赋值,见下文)
  • 属性和项目始终保持同步
  • 尝试访问不存在的键作为属性时,会正确抛出AttributeError而不是KeyError
  • 支持[Tab]自动补全(例如在jupyter和ipython中)

缺点:

  • 如果方法如.keys()被新数据覆盖,它们将无法正常工作
  • 在Python < 2.7.4 / Python3 < 3.2.3中会导致内存泄漏
  • Pylint会对E1123(unexpected-keyword-arg)E1103(maybe-no-member)感到困惑。
  • 对初学者来说,这看起来就像是纯粹的魔法。

关于这个如何工作的简短解释

  • 所有Python对象内部都将它们的属性存储在一个名为__dict__的字典中。
  • 内部字典__dict__并不一定要是“普通字典”,所以我们可以将任何dict()的子类分配给内部字典。
  • 在我们的例子中,我们简单地将正在实例化的AttrDict()实例分配给它(在__init__中)。
  • 通过调用super()__init__()方法,我们确保它(已经)表现得像一个字典,因为这个函数会调用所有的字典实例化代码。

Python不提供这种功能的一个原因

正如“缺点”列表中提到的,这将存储的键的命名空间(这些键可能来自任意和/或不可信的数据!)与内置字典方法属性的命名空间结合在一起。例如:

d = AttrDict()
d.update({'items':["jacket", "necktie", "trousers"]})
for k, v in d.items():    # TypeError: 'list' object is not callable
    print "Never reached!"

撰写回答