像属性一样访问字典键?

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

8

这并没有回答原问题,但对于像我一样在寻找提供此功能的库时来到这里的人们应该很有用。

Addict 是一个很棒的库,可以解决之前回答中提到的许多问题:https://github.com/mewwts/addict

以下是文档中的一个示例:

body = {
    'query': {
        'filtered': {
            'query': {
                'match': {'description': 'addictive'}
            },
            'filter': {
                'term': {'created_by': 'Mats'}
            }
        }
    }
}

使用 addict:

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

8

通常情况下,它并不起作用。并非所有有效的字典键都可以成为可寻址属性(“键”)。因此,您需要小心处理。

Python对象基本上都是字典。因此,我怀疑没有太多性能或其他惩罚。


7

为了给答案增加一些变化,sci-kit learn已将其实现为一个Bunch:

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检查字典键,然后继续检查实际属性。 setstaet是修复"bunches"的pickle / unpickle问题的方法-如果有兴趣,请查看https://github.com/scikit-learn/scikit-learn/issues/6196

6
这是一个使用内置 collections.namedtuple 的不可变记录的简短示例:
def record(name, d):
    return namedtuple(name, d.keys())(**d)

和一个用法示例:

rec = record('Model', {
    'train_op': train_op,
    'loss': loss,
})

print rec.loss(..)

6

由于以下原因,我对现有的选项不满意,于是开发了MetaDict。它与dict完全相同,但可以使用点符号和IDE自动完成,避免了其他解决方案的潜在命名空间冲突和缺陷。所有功能和用法示例都可以在GitHub上找到(请参阅上面的链接)。

完全透明披露:我是MetaDict的作者。

我尝试其他解决方案时遇到的缺陷/限制:

  • Addict
    • 没有IDE中的键自动完成
    • 无法关闭嵌套键分配
    • 新分配的dict对象未转换为支持属性样式键访问
    • 会覆盖内置类型Dict
  • Prodict
    • 没有定义静态架构(类似于dataclass),IDE中没有键自动完成
    • 在内置可迭代对象list或其他嵌套在其中的dict对象中没有递归转换
  • AttrDict
    • 没有IDE中的键自动完成
    • 在幕后将list对象转换为tuple
  • Munch
    • items()update()等内置方法可以被obj.items=[1,2,3]重写
    • 在内置可迭代对象list或其他嵌套在其中的dict对象中没有递归转换
  • EasyDict
    • 只有字符串是有效键,但dict接受所有可哈希对象作为键
    • items()update()等内置方法可以被obj.items=[1,2,3]重写
    • 内置方法的行为与预期不同:obj.pop('unknown_key', None)会引发AttributeError

很好,但不幸的是,当我传递一个dict时,我在Pycharm中无法获得自动完成。虽然这很可能只是Pycharm不支持一般支持的功能。 - rv.kvetch
1
只有在 RAM 中加载 MetaDict 对象时,自动完成才能正常工作,例如在 PyCharm 的交互式调试器或打开的 Python 会话中。自 README 中自动完成功能的截图来自 PyCharm 的 Python 控制台。此外,只有符合 Python 变量语法的字典键才可以通过点表示法访问,因此可以通过 IDE 的自动完成功能进行建议。 - hokage555
@rv.kvetch,您在交互式Python会话中是否通过自动完成看到内置方法(例如items()keys()等)作为建议?如果没有,我怀疑是PyCharm的问题。也许重新启动可以解决它? - hokage555
你会如何评价 Pydantic? - Andrew Mellinger
1
@AndrewMellinger Pydantic 真是太棒了,因为你可以定义一个静态数据模型。这个库并不试图取代 pydantic 或 data classes,而只是允许你使用内置的字典和属性语法。 - hokage555

4

小缺点:它不能在 iPython 中进行漂亮的打印。 - kdb

4

使用SimpleNamespace

from types import SimpleNamespace

obj = SimpleNamespace(color="blue", year=2050)

print(obj.color) #> "blue"
print(obj.year) #> 2050

编辑 / 更新:从词典开始更接近回答OP的问题:

from types import SimpleNamespace

params = {"color":"blue", "year":2020}

obj = SimpleNamespace(**params)

print(obj.color) #> "blue"
print(obj.year) #> 2050


仅供参考,SimpleNamespace 自 Python 3.3 版本开始提供。 - Rockallite

4
这是我使用的内容。
args = {
        'batch_size': 32,
        'workers': 4,
        'train_dir': 'train',
        'val_dir': 'val',
        'lr': 1e-3,
        'momentum': 0.9,
        'weight_decay': 1e-4
    }
args = namedtuple('Args', ' '.join(list(args.keys())))(**args)

print (args.lr)

这是一个好的快速而简单的答案。我唯一的观察/评论是,我认为namedtuple构造函数将接受一个字符串列表,因此您的解决方案可以简化(我想)为:namedtuple('Args', list(args.keys()))(**args) - dancow

4
你可以使用我刚刚制作的这个类来完成。使用这个类,你可以像使用另一个字典一样(包括json序列化)或使用点符号来操作Map对象。希望能对你有所帮助:
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]

使用示例:

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
请注意,它可能会掩盖dict方法,例如:m=Map(); m["keys"] = 42; m.keys()会产生TypeError: 'int' object is not callable - bfontaine
@bfontaine 的想法是成为一种“字段/属性”,而不是“方法”,但如果您分配一个方法而不是一个数字,您可以使用“m.method()”访问该方法。 - epool

4

最简单的方法是定义一个称为 Namespace 的类,并在字典上使用 dict.update() 方法。然后,该字典将被视为一个对象。

class Namespace(object):
    '''
    helps referencing object in a dictionary as dict.key instead of dict['key']
    '''
    def __init__(self, adict):
        self.__dict__.update(adict)



Person = Namespace({'name': 'ahmed',
                     'age': 30}) #--> added for edge_cls


print(Person.name)

惊人 - 最好、最简洁的答案被埋藏在底部,竟然花了将近10年才出现。谢谢! - Dan Nissenbaum
但是,不像字典那样容易打印:strrepr得到<__main__.Namespace object at 0x7f6f5b1004f0> - yurenchen

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