像属性一样访问字典键?

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个回答

4
让我发表另一种实现方法,它基于Kinvais的答案,但结合了AttributeDict中提出的思想http://databio.org/posts/python_AttributeDict.html
这个版本的优点是它也适用于嵌套字典。
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:
    # https://dev59.com/DW445IYBdhLWcg3wNXg_#14620633
    # 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

3

这篇答案摘自Luciano Ramalho的《流畅的Python》一书,特此致谢。

class AttrDict:
    """A read-only façade for navigating a JSON-like object
    using attribute notation
    """

    def __init__(self, mapping):
        self._data = dict(mapping)

    def __getattr__(self, name):
        if hasattr(self._data, name):
            return getattr(self._data, name)
        else:
            return AttrDict.build(self._data[name])

    @classmethod
    def build(cls, obj):
        if isinstance(obj, Mapping):
            return cls(obj)
        elif isinstance(obj, MutableSequence):
            return [cls.build(item) for item in obj]
        else:
            return obj

在init方法中,我们将dict转化为dictionary。当使用getattr时,如果该dict已经具有该属性,则尝试从dict中获取该属性。否则,我们将参数传递给一个名为build的类方法。现在,build方法做了有趣的事情。如果对象是dict或类似映射的对象,则将该对象本身作为attr dict。如果它是像列表这样的序列,则传递给当前正在处理的build函数。如果它是其他任何类型,例如str或int,则返回对象本身。

3

我根据这个帖子的意见创建了这个。但是我需要使用odict,所以我必须重写get和set属性。我认为这对大多数特殊用途都有效。

使用方法如下:

# 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

这个类:

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)

这是一个在线程中已经提到的非常棒的模式,但是如果你只想将字典转换为一个可以在IDE中使用自动完成功能的对象:

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

3
不需要自己编写,因为setattr()和getattr()已经存在。
类对象的优势可能体现在类定义和继承中。

2
“这种方式访问字典键的注意事项和陷阱有哪些呢?
正如@Henry所建议的那样,使用点号访问可能不会在字典中使用,因为它将字典键名称限制为Python有效变量,从而限制了所有可能的名称。
以下是一个示例,说明为什么一般情况下点号访问在给定字典d的情况下并不有用:
有效性
以下属性在Python中无效:”
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. 保留 关键字(或内置函数)名称:

d.in
d.False, d.True
d.max, d.min
d.sum
d.id

如果函数参数的名称与保留关键字冲突,通常最好在其后添加一个单下划线 …
B. 方法变量名的命名规则:
变量名遵循与函数名相同的约定。
d.Firstname
d.Country

使用函数命名规则:使用小写字母,必要时使用下划线分隔单词以提高可读性。
有时这些问题会在像pandas这样的中提出,它允许通过名称对DataFrame列进行点访问。解决命名限制的默认机制也是数组表示法——括号内的字符串。
如果这些限制不适用于您的用例,则点访问数据结构有几个选项。

我刚刚遇到了Pandas对象.属性点符号的问题。当进行Pandas过滤时,使用对象.属性符号会使语法变得丑陋。 - Rich Lysakowski PhD

2

抱歉再加一个,但这个解决了子字典和正确的AttributeError,虽然非常简单:

class DotDict(dict):
    def __init__(self, d: dict = {}):
        super().__init__()
        for key, value in d.items():
            self[key] = DotDict(value) if type(value) is dict else value
    
    def __getattr__(self, key):
        if key in self:
            return self[key]
        raise AttributeError(key) #Set proper exception, not KeyError

    __setattr__ = dict.__setitem__
    __delattr__ = dict.__delitem__

1
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

1
在你的.gitignore中放置.idea和任何用户特定或IDE生成的文件是一个好习惯。 - DeusXMachina

1

虽然这不是一个“好”的答案,但我认为这很有用(当前形式无法处理嵌套字典)。只需将您的字典包装在函数中:

def make_funcdict(d=None, **kwargs)
    def funcdict(d=None, **kwargs):
        if d is not None:
            funcdict.__dict__.update(d)
        funcdict.__dict__.update(kwargs)
        return funcdict.__dict__
    funcdict(d, **kwargs)
    return funcdict

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

示例

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

就是这样。如果有人提出这种方法的优缺点,我会很高兴。


1
解决方案是:
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__()

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