将字典转换为namedtuple或另一个可哈希的类字典的Pythonic方式是什么?

109

我有一个类似于字典的数据结构:

d = {'a': 1, 'b': 2, 'c': 3, 'd': 4}

我希望将其转换为namedtuple。我的当前方法是使用以下代码:

namedTupleConstructor = namedtuple('myNamedTuple', ' '.join(sorted(d.keys())))
nt= namedTupleConstructor(**d)

这段代码产生了:

myNamedTuple(a=1, b=2, c=3, d=4)

我认为这可以正常工作,但是我是否错过了类似于...的内置功能?

nt = namedtuple.from_dict() ?

更新:如评论中所讨论的,我想将我的字典转换为namedtuple的原因是使其可哈希,但仍然像字典一样通用。

更新2:在我发布这个问题4年后,TLK发布了一个新答案,推荐使用dataclass装饰器,我认为这真的很棒。我认为这是我未来要使用的。


使用namedtuples时,应该只创建一次命名元组类型并重复使用它,而不是每次生成一个新的命名元组类型。每次生成新的命名元组类型会很慢,并且破坏了任何节省空间的好处。 - user2357112
1
@user2357112,假设该用户拥有许多具有相同键的字典。 - wim
2
不会有一个内置的构造函数可以同时构建类型和元组,因为你应该重用类型。 - user2357112
要将namedtuple转换为字典,请参见此处:https://dev59.com/NF8e5IYBdhLWcg3wAWrs - wim
10个回答

173
创建子类时,您可以直接传递字典的键:
from collections import namedtuple

MyTuple = namedtuple('MyTuple', d)

现在要从这个字典或任何其他具有匹配键的字典中创建元组实例:

my_tuple = MyTuple(**d)

注意:namedtuple只会按值(有序)进行比较。它们被设计成普通元组的替代品,具有命名属性访问作为一个附加功能。在进行等式比较时不考虑字段名称,这可能并不是您从namedtuple类型期望或需要的!这与dict相等比较不同,后者会考虑键并且无序比较。

对于那些不真正需要元组子类类型的读者,使用namedtuple可能没有多大意义。 如果您只想在字段上使用属性访问语法,创建namespace对象将更简单易用:

>>> from types import SimpleNamespace
>>> SimpleNamespace(**d)
namespace(a=1, b=2, c=3, d=4)

我想将我的字典转换为namedtuple的原因是为了使它成为可哈希的,但仍然像字典一样通用。如果需要一个类似于“attrdict”的可哈希对象,请查看冻结的box
>>> from box import Box
>>> b = Box(d, frozen_box=True)
>>> hash(b)
7686694140185755210
>>> b.a
1
>>> b["a"]
1
>>> b["a"] = 2
BoxError: Box is frozen

Python也许会在未来的版本中加入一个冻结映射类型,请留意此草案PEP的批准或驳回:

PEP 603 - 向collections添加frozenmap类型


8
一句话描述:使用下面的代码将字典转换为命名元组: MyNamedTuple = namedtuple('MyNamedTuple', d.keys())(**d) - FLab
3
你是否意识到你从{'a': 1}{'b': 1}构造的namedtuples将会相等并且拥有相同的哈希码?像 tuple(sorted(d.items()))frozenset(d.items()) 这样的方式可能更加适合。它们也可以处理那些不是有效Python标识符的键,比如 'for'3 - user2357112
@user2357112,我之前不知道这个,谢谢你提供的信息。所以{'a':1}和{'b':1}会变成不同的元组(具有不同哈希值),但它们会变成相等的命名元组吗? - Max Power
1
@MaxPower: tuple(sorted(d.items())) 会构建不同的元组,因为它在实际元组中包含键。 (请注意,它需要可排序的键,这对于字符串来说很好,并且您已经依赖它。frozenset 可以处理无法排序的键。)您正在构建的命名元组不会在元组本身中包含键。 - user2357112
1
如果只有一个字典,为什么“应该”使用SimpleNamespace而不是namedtuple? - matt wilkie
显示剩余7条评论

18
from collections import namedtuple
nt = namedtuple('x', d.keys())(*d.values())

8
我建议在这种情况下使用dataclass。它类似于namedtuple,但更加灵活。

https://docs.python.org/3/library/dataclasses.html

from dataclasses import dataclass

@dataclass
class InventoryItem:
    """Class for keeping track of an item in inventory."""
    name: str
    unit_price: float
    quantity_on_hand: int = 0

    def total_cost(self) -> float:
        return self.unit_price * self.quantity_on_hand

1
嘿,谢谢你添加这个答案,我真的很喜欢。我会更新我的问题,并链接到它,让更多人看到。 - Max Power
太棒了的答案。请注意,dataclasses有一个asdict辅助函数,以确保可以根据需要序列化数据类实例。 - rv.kvetch
对于更复杂的用例,例如您想在反序列化时进行键重映射,需要使用嵌套数据类或在序列化时排除默认值,我建议使用快速序列化库,如dataclass-wizard - rv.kvetch
1
我发现数据类有很多怪癖,可能很难得到你想要的结果。虽然不是完美的选择,但可以考虑使用 Pydantic 包 - 它有一个自定义类,工作方式很像数据类,并且还有一个与数据类直接兼容的版本,但一些怪癖已经被解决了。不过,在某些方面,这确实需要对类的思考方向进行一些改变... - LightCC
我点了个踩,因为虽然 dataclasses 很好,但这并没有解决关于如何为任意字典获取命名元组的原始问题。 - bfontaine

8

如果你想要更简单的方法,而且你有使用除了namedtuple以外的其他方法的灵活性,我建议使用SimpleNamespace文档)。

from types import SimpleNamespace as sn

d = {'a': 1, 'b': 2, 'c': 3, 'd': 4}
dd= sn(**d)
# dd.a>>1

# add new property
dd.s = 5
#dd.s>>5

PS:SimpleNamespace是一种类型,而不是一个类。


有没有一种可哈希的方法来做这件事? - Shivangi Singh
1
问题要求提供可哈希类型。不过,其他答案中已经提到了SimpleNamespace作为另一种替代方法。 - wim

4
你可以使用这个函数来处理嵌套的字典:
def create_namedtuple_from_dict(obj):
    if isinstance(obj, dict):
        fields = sorted(obj.keys())
        namedtuple_type = namedtuple(
            typename='GenericObject',
            field_names=fields,
            rename=True,
        )
        field_value_pairs = OrderedDict(
            (str(field), create_namedtuple_from_dict(obj[field]))
            for field in fields
        )
        try:
            return namedtuple_type(**field_value_pairs)
        except TypeError:
            # Cannot create namedtuple instance so fallback to dict (invalid attribute names)
            return dict(**field_value_pairs)
    elif isinstance(obj, (list, set, tuple, frozenset)):
        return [create_namedtuple_from_dict(item) for item in obj]
    else:
        return obj

2
使用字典键作为namedtuple的字段名称。
d = {'a': 1, 'b': 2, 'c': 3, 'd': 4}

def dict_to_namedtuple(d):
    return namedtuple('GenericDict', d.keys())(**d)

 result=dict_to_namedtuple(d)
 print(result)

输出

  GenericDict(a=1, b=2, c=3, d=4)

你可以传递 d,因为迭代 d 会产生键。 - Peter Wood

1
def toNametuple(dict_data):
    return namedtuple(
        "X", dict_data.keys()
    )(*tuple(map(lambda x: x if not isinstance(x, dict) else toNametuple(x), dict_data.values())))

d = {
    'id': 1,
    'name': {'firstName': 'Ritesh', 'lastName':'Dubey'},
    'list_data': [1, 2],
}

obj = toNametuple(d)

访问方式为obj.name.firstNameobj.id

对于任何数据类型的嵌套字典都适用。


1
我认为以下这个4行代码是最美的,它也支持嵌套字典。
def dict_to_namedtuple(typename, data):
    return namedtuple(typename, data.keys())(
        *(dict_to_namedtuple(typename + '_' + k, v) if isinstance(v, dict) else v for k, v in data.items())
    )

输出结果也会很好看:

>>> nt = dict_to_namedtuple('config', {
...     'path': '/app',
...     'debug': {'level': 'error', 'stream': 'stdout'}
... })

>>> print(nt)
config(path='/app', debug=config_debug(level='error', stream='stdout'))

-2

看看这个:

def fill_tuple(NamedTupleType, container):
    if container is None:
        args = [None] * len(NamedTupleType._fields)
        return NamedTupleType(*args)
    if isinstance(container, (list, tuple)):
        return NamedTupleType(*container)
    elif isinstance(container, dict):
        return NamedTupleType(**container)
    else:
        raise TypeError("Cannot create '{}' tuple out of {} ({}).".format(NamedTupleType.__name__, type(container).__name__, container))

对于不正确的名称或无效的参数计数,由namedtuple__init__处理异常。

使用py.test进行测试:

def test_fill_tuple():
    A = namedtuple("A", "aa, bb, cc")

    assert fill_tuple(A, None) == A(aa=None, bb=None, cc=None)
    assert fill_tuple(A, [None, None, None]) == A(aa=None, bb=None, cc=None)
    assert fill_tuple(A, [1, 2, 3]) == A(aa=1, bb=2, cc=3)
    assert fill_tuple(A, dict(aa=1, bb=2, cc=3)) == A(aa=1, bb=2, cc=3)
    with pytest.raises(TypeError) as e:
        fill_tuple(A, 2)
    assert e.value.message == "Cannot create 'A' tuple out of int (2)."

-2

虽然我喜欢@fuggy_yama的答案,但在阅读它之前,我已经有了自己的函数,所以我把它留在这里,只是为了展示一种不同的方法。它还处理了嵌套的namedtuples

def dict2namedtuple(thedict, name):

    thenametuple = namedtuple(name, [])

    for key, val in thedict.items():
        if not isinstance(key, str):
            msg = 'dict keys must be strings not {}'
            raise ValueError(msg.format(key.__class__))

        if not isinstance(val, dict):
            setattr(thenametuple, key, val)
        else:
            newname = dict2namedtuple(val, key)
            setattr(thenametuple, key, newname)

    return thenametuple

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