关于python:像访问属性一样访问dict键?

Accessing dict keys like an attribute?

我发现用obj.foo而不是obj['foo']来访问dict键更方便,所以我写了以下代码:

1
2
3
4
5
class AttributeDict(dict):
    def __getattr__(self, attr):
        return self[attr]
    def __setattr__(self, attr, value):
        self[attr] = value

但是,我认为,一定有一些原因导致python不能提供这种开箱即用的功能。以这种方式访问dict键的注意事项和陷阱是什么?


最好的方法是:

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

一些优点:

  • 它确实有效!
  • 没有隐藏字典类方法(如.keys()工作正常)
  • 属性和项始终保持同步
  • 试图以属性的形式访问不存在的密钥会正确地引发AttributeError,而不是KeyError

欺骗:

  • .keys()这样的方法,如果被输入的数据覆盖,就不能正常工作。
  • 在python<2.7.4/python3<3.2.3中导致内存泄漏
  • 皮林特和E1123(unexpected-keyword-arg)E1103(maybe-no-member)一起吃香蕉。
  • 对于没有经验的人来说,这似乎是纯粹的魔法。

这是如何工作的简短解释

  • 所有python对象内部都将其属性存储在一个名为__dict__的字典中。
  • 内部字典__dict__不需要"只是一个普通的dict",因此我们可以将dict()的任何子类分配给内部字典。
  • 在我们的例子中,我们只需分配我们正在实例化的AttrDict()实例(就像我们在__init__中一样)。
  • 通过调用super()__init__()方法,我们确保它(已经)的行为与字典完全相同,因为该函数调用所有字典实例化代码。

为什么python不提供这种开箱即用的功能

如"cons"列表中所述,这将组合存储键的命名空间(可能来自任意和/或不受信任的数据!)具有内置dict方法属性的命名空间。例如:

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


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


从另一个问题来看,有一个很好的实现示例可以简化现有代码。怎么样:

1
2
3
class AttributeDict(dict):
    __getattr__ = dict.__getitem__
    __setattr__ = dict.__setitem__

更简洁,而且在将来不会给你的__getattr____setattr__功能留下额外的空间。


在那里我回答了被问到的问题为什么python不提供开箱即用的服务?

我怀疑这与python的禅有关:"应该有一种——最好只有一种——显而易见的方法。"这将创建两种明显的方法来访问字典中的值:obj['key']obj.key

警告和陷阱

这包括代码中可能缺乏清晰性和混乱性。也就是说,如果有人打算在以后维护您的代码,那么下面的内容可能会让其他人感到困惑,或者甚至让您感到困惑,如果您暂时不重新使用它的话。同样,来自禅宗:"可读性很重要!"

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

如果实例化d或定义KEY或指定d[KEY]的位置远离d.spam的使用位置,则很容易导致对正在执行的操作产生混淆,因为这不是常用的习惯用法。我知道这有可能让我困惑。

另外,如果您更改KEY的值如下(但错过更改d.spam),您现在可以:

1
2
3
4
5
6
7
>>> 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'

依我看,不值得这么做。

其他项目

正如其他人所指出的,您可以使用任何可散列对象(不仅仅是字符串)作为dict键。例如,

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

是合法的,但

1
2
3
4
5
6
7
8
9
10
11
12
>>> 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食谱(第9章)中的配方。

其中我社论

我更喜欢spam.eggs的美学而不是spam['eggs'](我认为它看起来更干净),当我遇到namedtuple时,我真的开始渴望这种功能。但是能够做到以下几点的便利性胜过它。

1
2
3
4
5
>>> 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}
>>>

这是一个简单的例子,但是我经常发现自己在不同的情况下使用dict,而不是使用obj.key符号(即,当我需要从XML文件中读取prefs时)。在其他情况下,如果出于美观的原因,我试图实例化一个动态类并在其上添加一些属性,那么为了增强可读性,我会继续使用dict来保持一致性。

我相信OP早就解决了这个问题,让他满意了,但是如果他仍然想要这个功能,那么我建议他从Pypi下载一个提供它的包:

  • 我更熟悉的就是邦奇。dict的子类,因此您拥有所有这些功能。
  • AttrDict看起来也不错,但我对它不太熟悉,也没有像我这样详细地查看过信息源。
  • 正如罗塔雷蒂的评论中所指出的,束已经被弃用,但有一个活跃的分叉称为芒奇。

但是,为了提高代码的可读性,我强烈建议他不要混合他的符号样式。如果他更喜欢这个符号,那么他应该简单地实例化一个动态对象,向它添加他想要的属性,并将其称为一天:

1
2
3
4
5
6
>>> 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要求:

What if you want to go one deeper? ( referring to type(...) )

虽然我从未使用过这个用例(同样,我倾向于使用嵌套的dict,用于一致性),以下代码有效:

1
2
3
4
5
6
7
8
9
10
>>> 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}


注意清空:出于某些原因,类似这样的类似乎会破坏多处理包。我只是和这个bug斗争了一段时间,然后发现了这个问题:在python多处理中查找异常


如果您想要一个方法的密钥,比如__eq____getattr__,该怎么办?

你不可能有一个不是以字母开头的条目,所以使用0343853作为键是不可能的。

如果你不想用绳子怎么办?


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

1
from argparse import Namespace

以避免在代码位周围进行复制。没有标准的字典访问,但如果你真的想要,很容易取回。argparse中的代码很简单,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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__


元组可以使用dict键。如何访问构造中的元组?

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


一般来说,它不起作用。并非所有有效的dict键都具有可寻址属性("键")。所以,你需要小心。

python对象基本上都是字典。所以我怀疑是否有很多表现或其他惩罚。


这并不能解决最初的问题,但对于像我这样的人来说,在查找提供此功能的lib时,应该会很有用。

上瘾:这是一个伟大的自由:https://github.com/mewwts/addict它处理了前面答案中提到的许多问题。

文档示例:

1
2
3
4
5
6
7
8
9
10
11
12
body = {
    'query': {
        'filtered': {
            'query': {
                'match': {'description': 'addictive'}
            },
            'filter': {
                'term': {'created_by': 'Mats'}
            }
        }
    }
}

吸毒成瘾:

1
2
3
4
from addict import Dict
body = Dict()
body.query.filtered.query.match.description = 'addictive'
body.query.filtered.filter.term.created_by = 'Mats'

下面是一个使用内置collections.namedtuple的不可变记录的简短示例:

1
2
def record(name, d):
    return namedtuple(name, d.keys())(**d)

以及用法示例:

1
2
3
4
5
6
rec = record('Model', {
    'train_op': train_op,
    'loss': loss,
})

print rec.loss(..)


不需要把自己写成setAttr()和getAttr()已经存在。

类对象的优势可能在类定义和继承中发挥作用。


我是基于这个线程的输入创建的。我需要使用odict,所以我必须覆盖get和set attr。我认为这应该适用于大多数特殊用途。

用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Create an ordered dict normally...
>>> od = OrderedAttrDict()
>>> od["a"] = 1
>>> od["b"] = 2
>>> od
OrderedAttrDict([('a', 1), ('b', 2)])

# Get and set data using attribute access...
>>> od.a
1
>>> od.b = 20
>>> od
OrderedAttrDict([('a', 1), ('b', 20)])

# Setting a NEW attribute only creates it on the instance, not the dict...
>>> od.c = 8
>>> od
OrderedAttrDict([('a', 1), ('b', 20)])
>>> od.c
8

班级:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class OrderedAttrDict(odict.OrderedDict):
   """
    Constructs an odict.OrderedDict with attribute access to data.

    Setting a NEW attribute only creates it on the instance, not the dict.
    Setting an attribute that is a key in the data will set the dict data but
    will not create a new instance attribute
   """

    def __getattr__(self, attr):
       """
        Try to get the data. If attr is not a key, fall-back and get the attr
       """

        if self.has_key(attr):
            return super(OrderedAttrDict, self).__getitem__(attr)
        else:
            return super(OrderedAttrDict, self).__getattr__(attr)


    def __setattr__(self, attr, value):
       """
        Try to set the data. If attr is not a key, fall-back and set the attr
       """

        if self.has_key(attr):
            super(OrderedAttrDict, self).__setitem__(attr, value)
        else:
            super(OrderedAttrDict, self).__setattr__(attr, value)

这是一个在线程中已经提到的非常酷的模式,但是如果您只想获取一个dict并将其转换为一个在IDE中自动完成的对象,等等:

1
2
3
class ObjectFromDict(object):
    def __init__(self, d):
        self.__dict__ = d

显然,现在有了一个用于这个的库——https://pypi.python.org/pypi/attrdict——它实现了这个精确的功能以及递归合并和JSON加载。可能值得一看。


为了增加答案的多样性,Sci Kit Learn将其作为Bunch来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Bunch(dict):                                                              
   """ Scikit Learn's container object                                        

    Dictionary-like object that exposes its keys as attributes.                
    >>> b = Bunch(a=1, b=2)                                                    
    >>> b['b']                                                                  
    2                                                                          
    >>> b.b                                                                    
    2                                                                          
    >>> b.c = 6                                                                
    >>> b['c']                                                                  
    6                                                                          
   """
                                                                       

    def __init__(self, **kwargs):                                              
        super(Bunch, self).__init__(kwargs)                                    

    def __setattr__(self, key, value):                                          
        self[key] = value                                                      

    def __dir__(self):                                                          
        return self.keys()                                                      

    def __getattr__(self, key):                                                
        try:                                                                    
            return self[key]                                                    
        except KeyError:                                                        
            raise AttributeError(key)                                          

    def __setstate__(self, state):                                              
        pass

您所需要的只是获取setattrgetattr方法-getattr检查dict键,并继续检查实际属性。setstaet是一个用于修复酸洗/解压"束"的修复程序-如果未经验证,请检查https://github.com/scikit-learn/scikit-learn/issues/6196


prodict怎么样,我写的小python类控制了它们:)

另外,您还可以获得自动代码完成、递归对象实例化和自动类型转换!

你可以按照你的要求做:

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

示例1:类型提示

1
2
3
4
5
6
7
class Country(Prodict):
    name: str
    population: int

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

auto code complete

示例2:自动类型转换

1
2
3
4
5
6
7
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'>


让我发布另一个实现,它基于Kinvais的答案,但是集成了http://databio.org/posts/python_attributedict.html中提出的attributedict的思想。

此版本的优点是它也适用于嵌套字典:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class AttrDict(dict):
   """
    A class to convert a nested Dictionary into an object with key-values
    that are accessible using attribute notation (AttrDict.attribute) instead of
    key notation (Dict["key"]). This class recursively sets Dicts to objects,
    allowing you to recurse down nested dicts (like: AttrDict.attr.attr)
   """


    # Inspired by:
    # http://stackoverflow.com/a/14620633/1551810
    # http://databio.org/posts/python_AttributeDict.html

    def __init__(self, iterable, **kwargs):
        super(AttrDict, self).__init__(iterable, **kwargs)
        for key, value in iterable.items():
            if isinstance(value, dict):
                self.__dict__[key] = AttrDict(value)
            else:
                self.__dict__[key] = value

你可以用我刚上的这门课来做。使用这个类,您可以像使用另一个字典(包括JSON序列化)一样使用Map对象,也可以使用点表示法。我希望能帮助你:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Map(dict):
   """
    Example:
    m = Map({'first_name': 'Eduardo'}, last_name='Pool', age=24, sports=['Soccer'])
   """

    def __init__(self, *args, **kwargs):
        super(Map, self).__init__(*args, **kwargs)
        for arg in args:
            if isinstance(arg, dict):
                for k, v in arg.iteritems():
                    self[k] = v

        if kwargs:
            for k, v in kwargs.iteritems():
                self[k] = v

    def __getattr__(self, attr):
        return self.get(attr)

    def __setattr__(self, key, value):
        self.__setitem__(key, value)

    def __setitem__(self, key, value):
        super(Map, self).__setitem__(key, value)
        self.__dict__.update({key: value})

    def __delattr__(self, item):
        self.__delitem__(item)

    def __delitem__(self, key):
        super(Map, self).__delitem__(key)
        del self.__dict__[key]

使用实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
m = Map({'first_name': 'Eduardo'}, last_name='Pool', age=24, sports=['Soccer'])
# Add new key
m.new_key = 'Hello world!'
print m.new_key
print m['new_key']
# Update values
m.new_key = 'Yay!'
# Or
m['new_key'] = 'Yay!'
# Delete key
del m.new_key
# Or
del m['new_key']


1
2
3
4
5
6
7
8
9
10
11
12
class AttrDict(dict):

     def __init__(self):
           self.__dict__ = self

if __name__ == '____main__':

     d = AttrDict()
     d['ray'] = 'hope'
     d.sun = 'shine'  >>> Now we can use this . notation
     print d['ray']
     print d.sun


解决方案是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
DICT_RESERVED_KEYS = vars(dict).keys()


class SmartDict(dict):
   """
    A Dict which is accessible via attribute dot notation
   """

    def __init__(self, *args, **kwargs):
       """
        :param args: multiple dicts ({}, {}, ..)
        :param kwargs: arbitrary keys='value'

        If ``keyerror=False`` is passed then not found attributes will
        always return None.
       """

        super(SmartDict, self).__init__()
        self['__keyerror'] = kwargs.pop('keyerror', True)
        [self.update(arg) for arg in args if isinstance(arg, dict)]
        self.update(kwargs)

    def __getattr__(self, attr):
        if attr not in DICT_RESERVED_KEYS:
            if self['__keyerror']:
                return self[attr]
            else:
                return self.get(attr)
        return getattr(self, attr)

    def __setattr__(self, key, value):
        if key in DICT_RESERVED_KEYS:
            raise AttributeError("You cannot set a reserved name as attribute")
        self.__setitem__(key, value)

    def __copy__(self):
        return self.__class__(self)

    def copy(self):
        return self.__copy__()

正如Doug所指出的,有一个包可以用来实现obj.key功能。实际上有一个新版本叫做

新纪元

它有一个很好的特性,可以通过它的neobunchify函数将您的dict转换为一个新束对象。我经常使用mako模板,并将数据作为neobunch对象传递,使其更具可读性,因此如果您碰巧在python程序中使用了一个普通的dict,但希望在mako模板中使用点符号,则可以这样使用:

1
2
3
4
5
6
7
from mako.template import Template
from neobunch import neobunchify

mako_template = Template(filename='mako.tmpl', strict_undefined=True)
data = {'tmpl_data': [{'key1': 'value1', 'key2': 'value2'}]}
with open('out.txt', 'w') as out_file:
    out_file.write(mako_template.render(**neobunchify(data)))

而Mako模板可能看起来像:

1
2
3
4
% for d in tmpl_data:
Column1     Column2
${d.key1}   ${d.key2}
% endfor


What would be the caveats and pitfalls of accessing dict keys in this manner?

正如@henry所建议的,点式访问不能用于dict的一个原因是它将dict键名限制为python有效变量,从而限制了所有可能的名称。

以下是点式访问一般不会有帮助的示例,给出了一个dict,d

有效性

以下属性在python中无效:

1
2
3
4
5
6
d.1_foo                           # enumerated names
d./bar                            # path names
d.21.7, d.12:30                   # decimals, time
d.""                              # empty strings
d.john doe, d.denny's             # spaces, misc punctuation
d.3 * x                           # expressions

风格

PEP8约定将对属性命名施加软约束:

a.保留关键字(或内置函数)名称:

1
2
3
4
5
d.in
d.False, d.True
d.max, d.min
d.sum
d.id

If a function argument's name clashes with a reserved keyword, it is generally better to append a single trailing underscore ...

b.方法和变量名的大小写规则:

Variable names follow the same convention as function names.

1
2
d.Firstname
d.Country

Use the function naming rules: lowercase with words separated by underscores as necessary to improve readability.

有时,这些问题会在pandas之类的库中出现,该库允许按名称对数据帧列进行点式访问。解决命名限制的默认机制也是数组表示法——括号内的字符串。

如果这些约束不适用于您的用例,那么在点式访问数据结构上有几个选项。


这不是一个"好"的答案,但我认为这很漂亮(它不处理当前形式的嵌套式听写)。简单地用一个函数包装您的dict:

1
2
3
4
5
6
7
def make_funcdict(d={}, **kwargs)
    def funcdict(d={}, **kwargs):
        funcdict.__dict__.update(d)
        funcdict.__dict__.update(kwargs)
        return funcdict.__dict__
    funcdict(d, **kwargs)
    return funcdict

现在您有了稍微不同的语法。将dict项作为f.key的属性进行访问。要以通常的方式访问dict项(和其他dict方法),请执行f()['key'],我们可以通过使用关键字参数和/或字典调用f来方便地更新dict

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
d = {'name':'Henry', 'age':31}
d = make_funcdict(d)
>>> for key in d():
...     print key
...
age
name
>>> print d.name
... Henry
>>> print d.age
... 31
>>> d({'Height':'5-11'}, Job='Carpenter')
... {'age': 31, 'name': 'Henry', 'Job': 'Carpenter', 'Height': '5-11'}

就在那里。如果有人提出这种方法的优点和缺点,我会很高兴的。