Serializar una llamada de Python a json

85

¿Cuál es la forma recomendada de serializar un namedtuplea json con los nombres de campo retenidos?

Serializar un namedtuplea json da como resultado que solo se serialicen los valores y que los nombres de los campos se pierdan en la traducción. Me gustaría que los campos también se conserven cuando json-ized y, por lo tanto, hice lo siguiente:

class foobar(namedtuple('f', 'foo, bar')):
    __slots__ = ()
    def __iter__(self):
        yield self._asdict()

Lo anterior se serializa en json como espero y se comporta como namedtupleen otros lugares que uso (acceso a atributos, etc.) excepto con resultados que no son de tupla mientras lo itera (lo cual está bien para mi caso de uso).

¿Cuál es la "forma correcta" de convertir a json con los nombres de campo retenidos?

calvinkrishy
fuente
para python 2.7: stackoverflow.com/questions/16938456/…
lowtech

Respuestas:

56

Esto es bastante complicado, ya que namedtuple()es una fábrica que devuelve un nuevo tipo derivado de tuple. Un enfoque sería hacer que su clase también herede UserDict.DictMixin, pero tuple.__getitem__ya está definida y espera un número entero que denote la posición del elemento, no el nombre de su atributo:

>>> f = foobar('a', 1)
>>> f[0]
'a'

En el fondo, namedtuple es un ajuste extraño para JSON, ya que en realidad es un tipo personalizado cuyos nombres de clave se fijan como parte de la definición de tipo , a diferencia de un diccionario donde los nombres de clave se almacenan dentro de la instancia. Esto evita que usted "haga un viaje redondo" a una tupla con nombre, por ejemplo, no puede decodificar un diccionario en una tupla con nombre sin alguna otra información, como un marcador de tipo específico de aplicación en el dict {'a': 1, '#_type': 'foobar'}, que es un poco hacky.

Esto no es ideal, pero si solo necesita codificar tuplas con nombre en diccionarios, otro enfoque es extender o modificar su codificador JSON para casos especiales de estos tipos. Aquí hay un ejemplo de subclases de Python json.JSONEncoder. Esto aborda el problema de garantizar que las tuplas con nombre anidadas se conviertan correctamente en diccionarios:

from collections import namedtuple
from json import JSONEncoder

class MyEncoder(JSONEncoder):

    def _iterencode(self, obj, markers=None):
        if isinstance(obj, tuple) and hasattr(obj, '_asdict'):
            gen = self._iterencode_dict(obj._asdict(), markers)
        else:
            gen = JSONEncoder._iterencode(self, obj, markers)
        for chunk in gen:
            yield chunk

class foobar(namedtuple('f', 'foo, bar')):
    pass

enc = MyEncoder()
for obj in (foobar('a', 1), ('a', 1), {'outer': foobar('x', 'y')}):
    print enc.encode(obj)

{"foo": "a", "bar": 1}
["a", 1]
{"outer": {"foo": "x", "bar": "y"}}
samplebias
fuente
12
En esencia, el namedtuple es un ajuste extraño para JSON, ya que en realidad es un tipo personalizado cuyos nombres de clave se fijan como parte de la definición de tipo, a diferencia de un diccionario donde los nombres de clave se almacenan dentro de la instancia. Comentario muy perspicaz. No había pensado en eso. Gracias. Me gustan las tuplas con nombre, ya que proporcionan una estructura agradable e inmutable con la conveniencia de nombrar atributos. Aceptaré tu respuesta. Habiendo dicho eso, el mecanismo de serialización de Java proporciona más control sobre cómo se serializa el objeto y tengo curiosidad por saber por qué tales ganchos no parecen existir en Python.
calvinkrishy
Ese fue mi primer enfoque, pero en realidad no funciona (para mí de todos modos).
zeekay
1
>>> json.dumps(foobar('x', 'y'), cls=MyEncoder) <<< '["x", "y"]'
zeekay
19
Ah, en Python 2.7+ _iterencode ya no es un método de JSONEncoder.
zeekay
2
@calvin Gracias, también encuentro útil el namedtuple, desearía que hubiera una mejor solución para codificarlo de forma recursiva en JSON. @zeekay Sí, parece que en 2.7+ lo ocultan para que ya no se pueda anular. Eso es decepcionante.
samplebias
77

Si es solo uno namedtupleque está buscando serializar, usar su _asdict()método funcionará (con Python> = 2.7)

>>> from collections import namedtuple
>>> import json
>>> FB = namedtuple("FB", ("foo", "bar"))
>>> fb = FB(123, 456)
>>> json.dumps(fb._asdict())
'{"foo": 123, "bar": 456}'
benselme
fuente
4
Recibo AttributeError: el objeto 'FB' no tiene el atributo ' dict ' cuando se ejecuta ese código en Python 2.7 (x64) en Windows. Sin embargo, fb._asdict () funciona bien.
geographika
5
fb._asdict()o vars(fb)sería mejor.
jpmc26
1
@ jpmc26: no se puede usar varsen un objeto sin un __dict__.
Rufflewind
@Rufflewind Tampoco puedes usar __dict__en esos. =)
jpmc26
4
En python 3 __dict__se ha eliminado. _asdictparece funcionar en ambos.
Andy Hayden
21

Parece que solía ser capaz de simplejson.JSONEncodercrear una subclase para que esto funcione, pero con el último código simplejson, ese ya no es el caso: tiene que modificar el código del proyecto. No veo ninguna razón por la que simplejson no deba admitir namedtuples, así que bifurqué el proyecto, agregué el soporte namedtuple y actualmente estoy esperando que mi rama vuelva al proyecto principal . Si necesita las correcciones ahora, simplemente tire de mi tenedor.

EDITAR : Parece que las últimas versiones de simplejsonahora admiten esto de forma nativa con la namedtuple_as_objectopción, que por defecto es True.

chico lobo
fuente
3
Tu edición es la respuesta correcta. simplejson serializa namedtuples de manera diferente (mi opinión: mejor) que json. Esto realmente hace que el patrón: "intente: importar simplejson como json excepto: importar json", sea riesgoso ya que puede obtener un comportamiento diferente en algunas máquinas dependiendo de si simplejson está instalado. Por esa razón, ahora necesito simplejson en muchos de mis archivos de configuración y me abstengo de ese patrón.
marr75
1
@ marr75 - Lo mismo ocurre con ujson, que es aún más extraño e impredecible en tales casos extremos ...
mac
Pude obtener un namedtuple recursivo serializado en json (bastante impreso) usando:simplejson.dumps(my_tuple, indent=4)
KFL
5

Escribí una biblioteca para hacer esto: https://github.com/ltworf/typedload

Puede ir desde y hacia la tupla con nombre y viceversa.

Admite estructuras anidadas bastante complicadas, con listas, conjuntos, enumeraciones, uniones, valores predeterminados. Debería cubrir los casos más comunes.

editar: la biblioteca también admite clases de datos y atributos.

LtWorf
fuente
2

Convierte de forma recursiva los datos namedTuple a json.

print(m1)
## Message(id=2, agent=Agent(id=1, first_name='asd', last_name='asd', mail='[email protected]'), customer=Customer(id=1, first_name='asd', last_name='asd', mail='[email protected]', phone_number=123123), type='image', content='text', media_url='h.com', la=123123, ls=4512313)

def reqursive_to_json(obj):
    _json = {}

    if isinstance(obj, tuple):
        datas = obj._asdict()
        for data in datas:
            if isinstance(datas[data], tuple):
                _json[data] = (reqursive_to_json(datas[data]))
            else:
                 print(datas[data])
                _json[data] = (datas[data])
    return _json

data = reqursive_to_json(m1)
print(data)
{'agent': {'first_name': 'asd',
'last_name': 'asd',
'mail': '[email protected]',
'id': 1},
'content': 'text',
'customer': {'first_name': 'asd',
'last_name': 'asd',
'mail': '[email protected]',
'phone_number': 123123,
'id': 1},
'id': 2,
'la': 123123,
'ls': 4512313,
'media_url': 'h.com',
'type': 'image'}
Tolgahan ÜZÜN
fuente
1
+1 hice casi lo mismo. Pero su regreso es un dict no json. Debe tener "not", y si un valor en su objeto es un booleano, no se convertirá en verdadero. Creo que es más seguro transformarlo en dict, luego use json.dumps para convertirlo en json.
Fred Laurent
2

Hay una solución más conveniente que es usar el decorador (usa el campo protegido _fields).

Python 2.7+:

import json
from collections import namedtuple, OrderedDict

def json_serializable(cls):
    def as_dict(self):
        yield OrderedDict(
            (name, value) for name, value in zip(
                self._fields,
                iter(super(cls, self).__iter__())))
    cls.__iter__ = as_dict
    return cls

#Usage:

C = json_serializable(namedtuple('C', 'a b c'))
print json.dumps(C('abc', True, 3.14))

# or

@json_serializable
class D(namedtuple('D', 'a b c')):
    pass

print json.dumps(D('abc', True, 3.14))

Python 3.6.6+:

import json
from typing import TupleName

def json_serializable(cls):
    def as_dict(self):
        yield {name: value for name, value in zip(
            self._fields,
            iter(super(cls, self).__iter__()))}
    cls.__iter__ = as_dict
    return cls

# Usage:

@json_serializable
class C(NamedTuple):
    a: str
    b: bool
    c: float

print(json.dumps(C('abc', True, 3.14))
Dmitry T.
fuente
No hagas eso, cambian la API interna todo el tiempo. Mi biblioteca typedload tiene varios casos para diferentes versiones de py.
LtWorf
Sí, está claro. Sin embargo, nadie debería migrar a una versión más reciente de Python sin probar. Y, las otras soluciones utilizan _asdict, que también es un miembro de clase "protegido".
Dmitry T.
1
LtWorf, su biblioteca es GPL y no funciona con frozensets
Thomas Grainger
2
@LtWorf Tu biblioteca también usa _fields;-) github.com/ltworf/typedload/blob/master/typedload/datadumper.py Es parte de la API pública de namedtuple, en realidad: docs.python.org/3.7/library/ ... La gente se confunde con el subrayado (¡no es de extrañar!). Es un mal diseño, pero no sé qué otra opción tenían.
quant_dev
1
¿Qué cosas? ¿Cuando? ¿Puedes citar notas de la versión?
quant_dev
2

La biblioteca jsonplus proporciona un serializador para instancias de NamedTuple. Use su modo de compatibilidad para generar objetos simples si es necesario, pero prefiera el predeterminado, ya que es útil para decodificar.

Gonzalo
fuente
Miré las otras soluciones aquí y descubrí que simplemente agregar esta dependencia me ahorró mucho tiempo. Particularmente porque tenía una lista de NamedTuples que necesitaba pasar como json en la sesión. jsonplus le permite básicamente obtener listas de tuplas con nombre dentro y fuera de json con .dumps()y .loads()sin configuración, simplemente funciona.
Rob
1

Es imposible serializar las tuplas con nombre correctamente con la biblioteca json nativa de Python. Siempre verá las tuplas como listas, y es imposible anular el serializador predeterminado para cambiar este comportamiento. Es peor si los objetos están anidados.

Es mejor usar una biblioteca más robusta como orjson :

import orjson
from typing import NamedTuple

class Rectangle(NamedTuple):
    width: int
    height: int

def default(obj):
    if hasattr(obj, '_asdict'):
        return obj._asdict()

rectangle = Rectangle(width=10, height=20)
print(orjson.dumps(rectangle, default=default))

=>

{
    "width":10,
    "height":20
}
mikebridge
fuente
1
Yo orjsontambién soy fanático .
CircleOnCircles hace
0

Ésta es una vieja pregunta. Sin embargo:

Una sugerencia para todos aquellos con la misma pregunta, piensen detenidamente sobre el uso de cualquiera de las características privadas o internas del NamedTupleporque tienen antes y volverán a cambiar con el tiempo.

Por ejemplo, si su NamedTuplees un objeto de valor plano y solo está interesado en serializarlo y no en los casos en que esté anidado en otro objeto, podría evitar los problemas que surgirían al __dict__eliminarlo o _as_dict()cambiarlo y simplemente hacer algo como (y sí, esto es Python 3 porque esta respuesta es para el presente):

from typing import NamedTuple

class ApiListRequest(NamedTuple):
  group: str="default"
  filter: str="*"

  def to_dict(self):
    return {
      'group': self.group,
      'filter': self.filter,
    }

  def to_json(self):
    return json.dumps(self.to_dict())

Intenté usar el defaultkwarg invocable dumpspara hacer la to_dict()llamada si estaba disponible, pero no se llamó porque NamedTuplees convertible a una lista.

dlamblin
fuente
3
_asdictes parte de la API pública namedtuple. Explican el motivo del guión bajo docs.python.org/3.7/library/… "Además de los métodos heredados de las tuplas, las tuplas con nombre admiten tres métodos adicionales y dos atributos. Para evitar conflictos con los nombres de campo, los nombres de método y atributo comience con un guión bajo ".
quant_dev
@quant_dev gracias, no vi esa explicación. No es una garantía de estabilidad de la API, pero ayuda a que esos métodos sean más confiables. Me gusta la legibilidad explícita de to_dict, pero puedo ver que parece reimplementar _as_dict
dlamblin
0

Aquí está mi opinión sobre el problema. Serializa NamedTuple, se encarga de las listas y de las NamedTuple plegadas dentro de ellas

def recursive_to_dict(obj: Any) -> dict:
_dict = {}

if isinstance(obj, tuple):
    node = obj._asdict()
    for item in node:
        if isinstance(node[item], list): # Process as a list
            _dict[item] = [recursive_to_dict(x) for x in (node[item])]
        elif getattr(node[item], "_asdict", False): # Process as a NamedTuple
            _dict[item] = recursive_to_dict(node[item])
        else: # Process as a regular element
            _dict[item] = (node[item])
return _dict
Oscuro
fuente
0

simplejson.dump()en lugar de json.dumphacer el trabajo. Aunque puede ser más lento.

Smit Johnth
fuente