如何在Python中使用点表示法访问字典?

156

我对Python非常陌生,希望能够使用.符号来访问dict中的值。

比如说我有类似这样的test

>>> test = dict()
>>> test['name'] = 'value'
>>> print(test['name'])
value

但是我希望我能够执行test.name来获取value。实际上,我通过在我的类中重写__getattr__方法来实现:

但我希望我能够执行test.name来获取value。实际上,我通过在我的类中覆盖__getattr__方法来实现这一点:

class JuspayObject:

    def __init__(self,response):
        self.__dict__['_response'] = response

    def __getattr__(self,key): 
        try:
            return self._response[key]
        except KeyError,err:
            sys.stderr.write('Sorry no key matches')

这很有效! 当我执行以下操作时:

test.name // I get value.

但问题是当我只打印test时,会出现以下错误:

'Sorry no key matches'
为什么会发生这种情况?

当您请求字典中不存在的属性时,需要调用超类__getattr__。 - David Heffernan
@DavidHeffernan 像 OP 的示例这样的旧式类甚至有超类吗? - Aya
@Aya 没有主意。如果没有的话,使用一个新的样式类。反正还有谁会使用旧的样式类呢? - David Heffernan
1
@DavidHeffernan 嗯,标准 Python 库中仍然有很多旧式类,例如 cgi.py - Aya
1
参见:https://dev59.com/dXE95IYBdhLWcg3wV8e_ - dreftymac
16个回答

274

这个功能已经存在于标准库中(types.SimpleNamespace),所以我建议你直接使用它们的类。

>>> from types import SimpleNamespace
>>> d = {'key1': 'value1', 'key2': 'value2'}
>>> n = SimpleNamespace(**d)
>>> print(n)
namespace(key1='value1', key2='value2')
>>> n.key2
'value2'

通过常规属性访问可以实现添加、修改和删除值,即您可以使用类似于n.key = valdel n.key的语句。

要再次返回字典:

>>> vars(n)
{'key1': 'value1', 'key2': 'value2'}

你的字典中的键必须是字符串标识符,这样属性访问才能正常工作。

Python 3.3 中添加了简单的命名空间。对于语言的旧版本,argparse.Namespace 具有类似的行为。


2
Fabric也有一个漂亮的最小实现,可以在这里找到:https://github.com/fabric/fabric/blob/1.13.1/fabric/utils.py#L186。我也在这里发布了:https://dev59.com/dXE95IYBdhLWcg3wV8e_#41274937。 - Dave
20
如果你能递归这个操作就太好了,也就是访问myobj.subdict.subdict - Pithikos
4
有第三方库提供此功能,可以查看 python-box - wim
3
注意,当d是字典时,'key' in d可以正常工作,但是当dSimpleNamespace时,这种方法不起作用。 - Ahmed Fasih

60

我假设您已经熟悉了JavaScript,想要借用那种语法……但我可以通过个人经验告诉您,这并不是一个好主意。

看起来确实更简洁和整洁; 但从长远来看,它只会变得难以理解。字典就是字典,试图让它们像带有属性的对象一样行事可能会导致(糟糕的)意外。

如果您需要操作对象的字段,仿佛它们是一个字典,当需要时您总是可以使用内部的__dict__属性,然后清楚地表明您正在做什么。或者使用getattr(obj, 'key'),也要考虑继承结构和类属性。

但是通过阅读您的示例,似乎您正在尝试不同的事情……因为点运算符将在没有任何额外代码的情况下查找__dict__属性。


20
糟糕的意外示例:如果在字典中有一个键,它恰好是 Python 关键字,例如字符串 'for',那么属性访问会失败,没有一种优雅的方式可以正确处理这种情况。从一开始整个想法就是基本错误的。 - wim
8
另一方面,自动补全功能可使代码更高效、减少错误。如果字典结构可以预先定义,那就完美了。 - roundar
6
JavaScript非常优雅。 - user1663023
4
JavaScript可以很优雅,也可以变得一团糟(像大多数语言一样)。但保证会变得一团糟的是试图将Python用作JavaScript(或任何其他两种语言)。如果在给定语言中存在预期的某种做事方式,则偏离这种方式只会带来痛苦。 - fortran

21

除了这个答案,还可以支持嵌套字典:

from types import SimpleNamespace

class NestedNamespace(SimpleNamespace):
    def __init__(self, dictionary, **kwargs):
        super().__init__(**kwargs)
        for key, value in dictionary.items():
            if isinstance(value, dict):
                self.__setattr__(key, NestedNamespace(value))
            else:
                self.__setattr__(key, value)

nested_namespace = NestedNamespace({
    'parent': {
        'child': {
            'grandchild': 'value'
        }
    },
    'normal_key': 'normal value',
})


print(nested_namespace.parent.child.grandchild)  # value
print(nested_namespace.normal_key)  # normal value
请注意,此功能不支持字典内嵌在列表中使用点符号表示的情况。

12

你是否可以使用命名元组(named tuple)

from collections import namedtuple
Test = namedtuple('Test', 'name foo bar')
my_test = Test('value', 'foo_val', 'bar_val')
print(my_test)
print(my_test.name)


3
这是一种通过点符号访问数据结构的有趣方式,但似乎与 JSON 或 dict 不太兼容。有些库使用命名元组作为基础,可以提供 JSON 和 dict 支持。建议尝试 https://github.com/dsc/bunch 或 https://github.com/kennknowles/python-jsonpath-rw。 - MarkHu
对于Python版本大于等于3.6,可以考虑使用from typing import NamedTuple - user9074332

7
class convert_to_dot_notation(dict):
    """
    Access dictionary attributes via dot notation
    """

    __getattr__ = dict.get
    __setattr__ = dict.__setitem__
    __delattr__ = dict.__delitem__


test = {"name": "value"}
data = convert_to_dot_notation(test)
print(data.name)

6

__getattr__是当所有其他属性查找规则均失败时使用的后备方法。当您尝试“打印”对象时,Python会查找__repr__方法,由于您的类中未实现该方法,因此最终会调用__getattr__(是的,在Python中,方法也是属性)。您不应假设getattr将使用哪个键,并且最重要的是,如果key无法解析,__getattr__必须引发AttributeError。

顺便说一下:不要使用self.__dict__进行普通属性访问,只需使用简单的属性符号:

class JuspayObject:

    def __init__(self,response):
        # don't use self.__dict__ here
        self._response = response

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

现在,如果你的类没有其他的职责(并且你的Python版本>=2.6,不需要支持旧版本),你可以使用一个命名元组:

http://docs.python.org/2/library/collections.html#collections.namedtuple

5
你可以使用内置方法 argparse.Namespace()
import argparse

args = argparse.Namespace()
args.name = 'value'

print(args.name)
# 'value'

你也可以通过vars(args)获取原始字典。

3

这里是一个简单、方便的点符号辅助示例,它可以处理嵌套项:

def dict_get(data:dict, path:str, default = None):
    pathList = re.split(r'\.', path, flags=re.IGNORECASE)
    result = data
    for key in pathList:
        try:
            key = int(key) if key.isnumeric() else key 
            result = result[key]
        except:
            result = default
            break
    
    return result

使用示例:

my_dict = {"test1": "str1", "nested_dict": {"test2": "str2"}, "nested_list": ["str3", {"test4": "str4"}]}
print(dict_get(my_dict, "test1"))
# str1
print(dict_get(my_dict, "nested_dict.test2"))
# str2
print(dict_get(my_dict, "nested_list.1.test4"))
# str4

经过长时间的搜索,这是我找到的最佳答案。 在使用示例中,您应该将函数名称重命名为dict_get而不是data_dict_get,谢谢。 - itay

3

在使用__getattr__时要小心,因为它用于很多内置的Python功能。

可以试着这样做...

class JuspayObject:

    def __init__(self,response):
        self.__dict__['_response'] = response

    def __getattr__(self, key):
        # First, try to return from _response
        try:
            return self.__dict__['_response'][key]
        except KeyError:
            pass
        # If that fails, return default behavior so we don't break Python
        try:
            return self.__dict__[key]
        except KeyError:
            raise AttributeError, key

>>> j = JuspayObject({'foo': 'bar'})
>>> j.foo
'bar'
>>> j
<__main__.JuspayObject instance at 0x7fbdd55965f0>

2
__getattr__仅在没有其他查找规则匹配时被用作后备,因此无需查找self.__dict__[key],如果调用了__getattr__,那么这已经完成了。如果在self._response ['key']上的查找失败,则只需引发AttributeError即可。 - bruno desthuilliers
@brunodesthuilliers 看起来你是对的。很奇怪。也许这只在 Python v1.5 时代才是必要的。 - Aya

2

这个回答的基础上,只需进行小幅改动即可支持列表:

class NestedNamespace(SimpleNamespace):
def __init__(self, dictionary, **kwargs):
    super().__init__(**kwargs)
    for key, value in dictionary.items():
        if isinstance(value, dict):
            self.__setattr__(key, NestedNamespace(value))
        elif isinstance(value, list):
            self.__setattr__(key, map(NestedNamespace, value))
        else:
            self.__setattr__(key, value)

1
很不幸,尽管您的编码可能是正确的,但它与问题并没有直接相关。问题是为什么打印对象本身会返回错误。 - Nick Lehmann

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