¿Una forma pitónica de convertir un diccionario en una tupla nombrada u otro dictado hashable?

7 minutos de lectura

avatar de usuario
Máximo poder

Tengo un diccionario como:

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

que me gustaría convertir en una tupla con nombre. Mi enfoque actual es con el siguiente código

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

que produce

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

Esto funciona bien para mí (creo), pero me falta un incorporado como…

nt = namedtuple.from_dict() ?

ACTUALIZACIÓN: como se discutió en los comentarios, mi razón para querer convertir mi diccionario a una tupla con nombre es para que se vuelva hashable, pero aún se pueda usar en general como un dictado.

ACTUALIZACIÓN 2: 4 años después de publicar esta pregunta, TLK publica una nueva respuesta que recomienda usar el decorador de clase de datos que creo que es realmente genial. Creo que eso es ahora lo que usaría en el futuro.

  • Con las tuplas nombradas, se supone que debe crear el tipo de tupla nombrada una vez y usarlo repetidamente, no generar un nuevo tipo de tupla nombrada cada vez. Generar un nuevo tipo de tupla con nombre cada vez es lento y anula cualquier beneficio de espacio.

    – usuario2357112

    11 mayo 2017 a las 16:45

  • @ user2357112 presumiblemente, el usuario tiene muchos dictados con las mismas claves.

    – Wim

    11 mayo 2017 a las 16:47

  • No va a haber una función integrada que construya el tipo y la tupla al mismo tiempo, porque se supone que debes reutilizar el tipo.

    – usuario2357112

    11 mayo 2017 a las 16:47

  • Para ir al otro lado (mencionado tuple en el diccionario), mira aquí: stackoverflow.com/q/26180528/674039

    – Wim

    1 oct 2020 a las 16:07

avatar de usuario
ingenio

Para crear la subclase, puede simplemente pasar las claves de un dict directamente:

MyTuple = namedtuple('MyTuple', d)

Ahora, para crear instancias de tupla a partir de este dictado, o cualquier otro dictado con claves coincidentes:

my_tuple = MyTuple(**d)

Tener cuidado: tuplas nombradas comparar en solo valores (ordenado). Están diseñados para ser un reemplazo directo de las tuplas regulares, con acceso a atributos con nombre como característica adicional. Los nombres de los campos no se tendrán en cuenta al realizar comparaciones de igualdad.. Puede que no sea lo que querías ni esperabas del namedtuple ¡escribe! Esto difiere de dict comparaciones de igualdad, que sí tienen en cuenta las claves y también comparan orden agnóstico.

Para los lectores que realmente no necesitan un tipo que sea una subclase de tupla, probablemente no tenga mucho sentido usar una tupla con nombre en primer lugar. Si solo desea usar la sintaxis de acceso de atributos en los campos, sería más simple y más fácil de crear espacio de nombres objetos en su lugar:

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

mi razón para querer convertir mi diccionario a una tupla con nombre es para que se vuelva hashable, pero aún se pueda usar en general como un dict

Para obtener una receta similar a “attrdict” hashable, echa un vistazo a un congelado caja:

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

También puede haber un tipo de mapeo congelado que viene en una versión posterior de Python, mire este borrador de PEP para aceptación o rechazo:

PEP 603 — Agregar un tipo de mapa congelado a las colecciones

  • Para el one-liner, necesita: MyNamedTuple = namedtuple(‘MyNamedTuple’, d.keys())(**d)

    – FLab

    11 mayo 2017 a las 16:47

  • Interesante, ¿es un espacio de nombres hashable? Esa fue mi razón original para querer convertir un dictado en una tupla con nombre

    – Máximo poder

    11 mayo 2017 a las 16:51

  • @MaxPower: ¿Sabe que las tuplas con nombre que construye a partir de {'a': 1} y {'b': 1} ¿Serán iguales y tendrán códigos hash iguales? Algo como tuple(sorted(d.items())) o frozenset(d.items()) puede ser más apropiado. También manejarán claves que no son identificadores de Python válidos, como 'for' o 3.

    – usuario2357112

    11 mayo 2017 a las 16:54

  • @Máximo poder: tuple(sorted(d.items())) construiría diferentes tuplas, porque incluye las claves en las tuplas reales. (Tenga en cuenta que requiere que las claves se puedan ordenar, lo cual está bien para cadenas, y en lo que ya está confiando. El frozenset manejaría claves desordenadas). Las tuplas con nombre que está construyendo no incluyen las claves en las tuplas mismas.

    – usuario2357112

    11 mayo 2017 a las 17:06


  • ¿Por qué “debería” usar SimpleNamespace en lugar de namedtuple si solo hay 1 dict?

    – matt wilkie

    25 de septiembre de 2019 a las 3:26

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

Me gustaría recomendar el clase de datos para este tipo de situación. Similar a una tupla con nombre, pero con más flexibilidad.

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

  • Hola, gracias por agregar esto, realmente me gusta esta respuesta. Agregaré una actualización a mi pregunta vinculándola para que la gente vea esto en el futuro.

    – Máximo poder

    27 de agosto de 2021 a las 14:42

  • fantástica respuesta. tenga en cuenta que las clases de datos tienen un asdict Función auxiliar para garantizar que las instancias de clase de datos se puedan serializar según sea necesario.

    – rv.kvetch

    1 oct 2021 a las 17:20


  • para un caso de uso más complejo, por ej. desea reasignar claves en la deserialización, necesita trabajar con clases de datos anidadas o excluir valores predeterminados en la serialización, le sugiero una biblioteca de serialización rápida como la asistente de clases de datos.

    – rv.kvetch

    1 oct 2021 a las 17:23


  • Descubrí que las clases de datos tienen muchas peculiaridades y puede ser difícil obtener exactamente lo que desea. Si bien tampoco es perfecto, es posible que desee considerar el paquete Pydantic: tiene una clase personalizada que funciona de manera muy similar a las clases de datos y una versión que es directamente compatible con las clases de datos, pero algunas de las peculiaridades están resueltas. Sin embargo, requiere un poco de cambio en la dirección de pensar en las clases de alguna manera…

    – LightCC

    23 de junio a las 23:33

Si desea un enfoque más fácil y tiene la flexibilidad de utilizar otro enfoque que no sea namedtuple Me gustaría sugerir el uso SimpleNamespace (documentos).

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

PD: SimpleNamespace es un tipo, no una clase

Puede usar esta función para manejar diccionarios anidados:

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

avatar de usuario
inteligente manoj

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)

Acceder como obj.name.firstName, obj.id

Esto funcionará para el diccionario anidado con cualquier tipo de datos.

avatar de usuario
Visión

Encuentro el siguiente 4-liner el más hermoso. También admite diccionarios anidados.

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())
    )

La salida se verá bien también:

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

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

¿Ha sido útil esta solución?