像属性一样访问字典键?

415

我发现通过 obj.foo 访问字典键比通过 obj['foo'] 更加方便,因此我写了这段代码:

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

然而,我认为Python没有默认提供这个功能一定有原因。以这种方式访问字典键的注意事项和陷阱是什么?


21
如果您在各处访问来自固定大小有限集合的硬编码密钥,最好创建保存它们的对象。 collections.namedtuple 对此非常有用。 - user395760
6
这个问题的类似解决方案可在https://dev59.com/mnA75IYBdhLWcg3w7tt6找到,不过它更进一步。 - keflavich
1
在 https://github.com/bcj/AttrDict 找到了一个模块。我不知道它与这里和相关问题中的解决方案相比如何。 - matt wilkie
我建议将该返回语句放在try块中,并添加一个返回False的异常。这样,像if (not) dict.key:这样的检查就可以起作用了。 - Marki
参见:https://dev59.com/dXE95IYBdhLWcg3wV8e_ - dreftymac
显示剩余2条评论
32个回答

415

更新 - 2020

自从近十年前提出这个问题以来,Python本身已经发生了很多变化。

虽然我的原始答案对于某些情况仍然有效(例如:遗留项目停留在较旧的Python版本上以及需要处理具有非常动态字符串键的字典的情况),但我认为通常情况下,在Python 3.7中引入的dataclasses是绝大多数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!"

1
你认为像这样的简单对象会发生内存泄漏吗?
class MyD(object): ... def init(self, d): ... self.dict = d
- Rafe
1
请将版本控制在2.7.3及以下,因为这是我正在使用的版本。 - pi.
1
在2.7.4版本的发布说明中,他们提到已经修复了这个问题(之前没有修复)。 - Robert Siemer
2
@viveksinghggits,仅仅因为你通过“.”访问事物,并不意味着你可以打破语言规则 :) 我也不希望AttrDict自动将包含空格的字段转换成其他内容。 - Yuri Astrakhan
1
每个AttrDict实例实际上存储了2个字典,一个是继承的,另一个在__dict__中。我不确定我是否理解了这一点。实际上只有一个字典,带有一个额外的引用从__dict__。这算什么缺点呢?从头开始的实现可能可以避免额外的引用,但在我看来,这几乎没有什么影响,所以不值得特别指出。我有什么遗漏吗? - haridsv
显示剩余18条评论

128

如果使用数组表示法,键可以包含所有合法的字符串字符。 例如,obj['!#$%^&*()_']


1
@Izkata 是的。关于SE有趣的事情是通常会有一个“顶部问题”,即标题,和一个“底部问题”,可能是因为SE不喜欢听到“标题说了一切”的话;这里的“警告”是底部的一个。 - n611x007
4
尽管 JavaScript 并不是一种特别好的编程语言,但在 JS 中,对象支持属性访问和数组表示法,这既方便了常见情况,也为那些不合法的属性名称提供了通用的替代方式。 - André Caron
@Izkata 这个回答怎么解决问题呢?这个回答只是说键可以有任何名称。 - Melab
8
问题是“以这种方式访问字典键的注意事项和陷阱是什么?”,答案是这里显示的大多数字符将无法使用。 - Izkata

96

回答提出的问题

为什么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.spam使用的地方分配了d[KEY],很容易导致混淆,因为这不是常用的习惯用法。我知道这可能会让我感到困惑。
此外,如果您按以下方式更改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的美学风格,因为我认为它看起来更干净,并且当我遇到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文件中读取首选项时)。在其他情况下,当我因为美观原因想要实例化一个动态类并将某些属性添加到它上面时,我继续使用字典以保持一致性以提高可读性。

我确信OP已经满意地解决了这个问题,但如果他仍然想要这个功能,则建议他从pypi下载提供此功能的软件包之一:

  • 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}

1
“Bunch”已被弃用,但有一个活跃的分支:https://github.com/Infinidat/munch - Rotareti
@Rotareti - 感谢你提醒!这不是我使用的功能,所以我不知道那个。 - Deacon
如果你想再深入一层呢?(指的是 type(...)) - Ole Aldric
7
Python就像是在暴雨中高举着的倒置伞。最初看起来非常时髦,过了一段时间后开始变得沉重,突然间你在Stack Exchange上读到了一些内置专家的东西,整个事情就像是把整个负担都倒在了你的肩膀上。虽然仍旧被淋湿了,但你感觉轻松了许多,一切都变得清晰而焕然一新。 - Ole Aldric
1
感谢提供替代方案,AttrDict在Python 3.10中已经无法使用。 - Dominik
@Dominik - 我之前不知道有Addict这个东西。感谢Piotr Dabkowski在编辑中添加这个。 - Deacon

90

这个stackoverflow问题中,有一个很棒的实现示例可以简化你现有的代码。那么怎么样:

class AttributeDict(dict):
    __slots__ = () 
    __getattr__ = dict.__getitem__
    __setattr__ = dict.__setitem__

更加简洁,未来在__getattr____setattr__函数中不会留下任何余地供额外的冗余信息进入。


8
据我所知,它完美地运作了,因此我给它点赞。@GringoSuave,@Izkata,@P3trus如果有人声称它失败,请提供一个不起作用的示例。 d = AttributeDict(foo=1);d.bar = 1;print d => {'foo': 1, 'bar': 1} 在我这里可以工作! - Dave Abrahams
很遗憾,已经过去8个月了,我不记得了。但是,快速测试表明它在Py 2.6 / Windows下对我无效,但在2.7 / Linux下有效。 - Gringo Suave
@P3trus 运行时分配被添加到实例的__dict__属性(而不是dict),确切地说,例如,实例是d = AttributeDict(),它们被添加到d.__dict__中。我猜你是这个意思,但遇到了注释格式问题。 - n611x007
4
请阅读完整的问题并查看 Hery、Ryan 和 TheCommunistDuck 的答案。这个问题并不是在问如何做,而是可能会出现的问题。 - Izkata
6
如果指定的属性不存在,你应该提供一个__getattr__方法来引发AttributeError异常,否则像getattr(obj, attr, default_value)这样的操作将无法正常工作(即如果obj上不存在attr,不会返回default_value)。 - jcdude
显示剩余4条评论

32
您可以从标准库中提取一个方便的容器类:

您可以从标准库中提取一个方便的容器类:

from argparse import Namespace

为避免复制代码片段。没有标准字典访问方式,但如果你真的想要它,很容易得到一个。argparse中的代码很简单。

class Namespace(_AttributeHolder):
    """Simple object for storing attributes.

    Implements equality by attribute names and values, and provides a simple
    string representation.
    """

    def __init__(self, **kwargs):
        for name in kwargs:
            setattr(self, name, kwargs[name])

    __hash__ = None

    def __eq__(self, other):
        return vars(self) == vars(other)

    def __ne__(self, other):
        return not (self == other)

    def __contains__(self, key):
        return key in self.__dict__

3
为引用标准库加1,以解决原帖作者的第一个评论。 - Gordon Bean
11
Python中包含一个更快的类(用C语言实现)适用于这种情况:types.SimpleNamespace https://docs.python.org/dev/library/types.html#types.SimpleNamespace - Nuno André
只是为了明确:如果d是你的字典,o = Namespace(**d)将包含所需的对象 :) - alexis_thual

24

注意:由于某些原因,像这样的类似代码似乎会影响多进程包。我在找到下面这个stackoverflow页面之前一直苦苦挣扎着解决这个问题:

在Python多进程中寻找异常

24

我在想Python生态系统中"dict keys as attr"的现状。正如许多评论者所指出的,这可能不是您想自己从头开始编写的东西,因为有几个陷阱和难以发现的问题。此外,我不建议使用Namespace作为基类,我曾经走过那条路,结果并不理想。

幸运的是,有几个开源软件包提供了这种功能,可直接pip安装!不幸的是,有几个软件包可供选择。以下是2019年12月的摘要。

候选人(最近的提交日期| #提交 | #贡献者 | 覆盖率%):

  • addict (2021-01-05 | 229 | 22 | 100%)
  • munch (2021-01-22 | 166 | 17 | ?%)
  • easydict (2021-02-28 | 54 | 7 | ?%)
  • attrdict (2019-02-01 | 108 | 5 | 100%)
  • prodict (2021-03-06 | 100 | 2 | ?%)

不再维护或维护不足:

  • treedict (2014-03-28 | 95 | 2 | ?%)
  • bunch (2012-03-12 | 20 | 2 | ?%)
  • NeoBunch

我目前推荐munchaddict。它们有最多的提交、贡献者和发布版本,表明每个软件包都有一个健康的开源代码库。它们具有最干净的readme.md,覆盖率为100%,并且拥有好看的测试集。

我对此没有太大兴趣(暂时没什么兴趣!),除了自己编写过字典/属性代码并浪费了很多时间,因为我不知道所有这些选项:)。如果您喜欢它们,请贡献!特别是,看起来munch需要一个codecov徽章,而addict需要一个Python版本徽章。

addict优点:

  • 递归初始化(foo.a.b.c ='bar'),类似于dict的参数变为addict.Dict

addict缺点:

  • 如果您使用from addict import Dict,则会遮盖typing.Dict
  • 没有键检查。由于允许递归初始化,如果拼写错误,则只会创建一个新属性,而不是抛出KeyError(感谢AljoSt)

munch优点:

  • 唯一命名
  • 内置的JSON和YAML序列化/反序列化函数

munch缺点:

    class BasePstruct(dict): def __getattr__(self, name): if name in self.__slots__: return self[name] return self.__getattribute__(name) def __setattr__(self, key, value): if key in self.__slots__: self[key] = value return if key in type(self).__dict__: self[key] = value return raise AttributeError( "type object '{}' has no attribute '{}'".format(type(self).__name__, key)) class FooPstruct(BasePstruct): __slots__ = ['foo', 'bar']

    这将为您提供一个仍然像字典一样运作的对象,但也让您以更加严谨的方式像访问属性一样访问键。优点是我(或您代码的不幸使用者)确切地知道哪些字段存在或不存在,而且 IDE 可以自动完成字段。另外,继承基本的dict意味着 JSON 序列化很容易。如果您决定使用属性字典,请务必记录期望的字段,以维护自己(和团队成员)的理智。

    请随意编辑/更新此帖子以使其保持最新!


2
addict 的一个很大的缺点是,当你拼错属性时它不会引发异常,而是返回一个新的 Dict(这对于 foo.a.b.c = 'bar' 起到了必要的作用)。 - AljoSt
你说的“no recursive init / only can init one attr at a time”是什么意思?能否举个例子? - Martin Thoma
@MartinThoma - 你不能像这样做 m = Munch(); m.hi.ho = 'silver',因为你会得到一个 AttributeError: hi 的错误,这是因为 m.hi 还没有被赋值。 - DeusXMachina

18

如果您需要一个方法作为键,比如__eq____getattr__怎么办?

并且您不能使用以字母之外的字符开头的条目作为键,因此使用0343853作为键是不可行的。

如果您不想使用字符串呢?


实际上,或者例如其他对象作为键。然而,我会将错误分类为“预期行为”- 我的问题更多地是针对意外情况。 - Izz ad-Din Ruhulessin
pickle.dump 使用 __getstate__ - Cees Timmerman

13

元组可以用作字典的键。在你的结构中,如何访问元组?

此外,namedtuple是一种方便的结构,可以通过属性访问提供值。


8
namedtuple 的缺点是它们不可变。 - Izz ad-Din Ruhulessin
13
有人可能会说,不可变性不是元组的缺陷,而是其特性。 - ben author

11

那么我写的小型Python类Prodict怎么样呢?

此外,您还可以获得自动代码补全递归对象实例化自动类型转换

您可以完全按照您要求的方式进行操作:

p = Prodict()
p.foo = 1
p.bar = "baz"

示例1:类型提示

class Country(Prodict):
    name: str
    population: int

turkey = Country()
turkey.name = 'Turkey'
turkey.population = 79814871

auto code complete

示例2:自动类型转换

germany = Country(name='Germany', population='82175700', flag_colors=['black', 'red', 'yellow'])

print(germany.population)  # 82175700
print(type(germany.population))  # <class 'int'>

print(germany.flag_colors)  # ['black', 'red', 'yellow']
print(type(germany.flag_colors))  # <class 'list'>

2
可以通过pip在Python2上安装,但在Python2上无法正常工作。 - Ant6n
2
@Ant6n 需要使用 Python 3.6+,因为需要类型注释。 - ramazan polat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接