Serializzare un Python namedtuple in json


87

Qual è il modo consigliato per serializzare un namedtuplein json mantenendo i nomi dei campi?

La serializzazione di a namedtuplein json comporta la serializzazione solo dei valori e la perdita dei nomi dei campi nella traduzione. Vorrei che i campi venissero mantenuti anche quando json-ized e quindi ha fatto quanto segue:

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

Quanto sopra serializza in json come mi aspetto e si comporta come namedtuplein altri posti che uso (accesso agli attributi ecc.) Tranne che con risultati non simili a tupla durante l'iterazione (che va bene per il mio caso d'uso).

Qual è il "modo corretto" di convertire in json mantenendo i nomi dei campi?


Risposte:


55

Questo è piuttosto complicato, poiché namedtuple()è una fabbrica che restituisce un nuovo tipo derivato da tuple. Un approccio potrebbe essere quello di far ereditare anche la tua classe da UserDict.DictMixin, ma tuple.__getitem__è già definita e si aspetta un numero intero che denoti la posizione dell'elemento, non il nome del suo attributo:

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

Fondamentalmente namedtuple è una scelta strana per JSON, poiché è in realtà un tipo personalizzato i cui nomi di chiave sono fissati come parte della definizione del tipo , a differenza di un dizionario in cui i nomi di chiave sono memorizzati all'interno dell'istanza. Questo ti impedisce di eseguire il "round trip" di una namedtuple, ad esempio non puoi decodificare un dizionario in una namedtuple senza qualche altra informazione, come un marcatore di tipo specifico dell'app nel dict {'a': 1, '#_type': 'foobar'}, che è un po 'hacky.

Questo non è l'ideale, ma se hai solo bisogno di codificare namedtuples in dizionari, un altro approccio è estendere o modificare il tuo codificatore JSON a questi tipi di casi speciali. Ecco un esempio di sottoclasse di Python json.JSONEncoder. Questo affronta il problema di garantire che le coppie denominate annidate siano convertite correttamente in dizionari:

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

12
Fondamentalmente namedtuple è una scelta strana per JSON, poiché è in realtà un tipo personalizzato i cui nomi di chiave sono fissati come parte della definizione del tipo, a differenza di un dizionario in cui i nomi di chiave sono memorizzati all'interno dell'istanza. Commento molto perspicace. Non ci avevo pensato. Grazie. Mi piacciono le namedtuples poiché forniscono una bella struttura immutabile con comodità di denominazione degli attributi. Accetterò la tua risposta Detto questo, il meccanismo di serializzazione di Java fornisce un maggiore controllo su come l'oggetto viene serializzato e sono curioso di sapere perché tali hook non sembrano esistere in Python.
calvinkrishy

Questo è stato il mio primo approccio, ma in realtà non funziona (per me comunque).
zeekay

1
>>> json.dumps(foobar('x', 'y'), cls=MyEncoder) <<< '["x", "y"]'
zeekay

19
Ah, in python 2.7+ _iterencode non è più un metodo di JSONEncoder.
zeekay

2
@calvin Grazie, trovo utile anche namedtuple, vorrei che ci fosse una soluzione migliore per codificarlo ricorsivamente in JSON. @zeekay Sì, sembra che in 2.7+ lo nascondano in modo che non possa più essere sovrascritto. Questo è deludente.
samplebias

77

Se è solo uno namedtupleche stai cercando di serializzare, il suo _asdict()metodo funzionerà (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}'

4
Ricevo AttributeError: l'oggetto 'FB' non ha l'attributo ' dict ' quando si esegue quel codice in Python 2.7 (x64) su Windows. Tuttavia fb._asdict () funziona bene.
geographika

5
fb._asdict()o vars(fb)sarebbe meglio.
jpmc26

1
@ jpmc26: non puoi usare varssu un oggetto senza un file __dict__.
Rufflewind

@Rufflewind Non puoi usare neanche __dict__su quelli. =)
jpmc26

4
In Python 3 __dict__è stato rimosso. _asdictsembra funzionare su entrambi.
Andy Hayden

21

Sembra che tu fossi in grado di simplejson.JSONEncodercreare sottoclassi per farlo funzionare, ma con l'ultimo codice simplejson, non è più così: devi effettivamente modificare il codice del progetto. Non vedo alcun motivo per cui simplejson non dovrebbe supportare namedtuples, quindi ho biforcato il progetto, aggiunto il supporto namedtuple e sto attualmente aspettando che il mio ramo venga riportato nel progetto principale . Se hai bisogno delle correzioni ora, tira fuori dalla mia forcella.

EDIT : Sembra che le ultime versioni di simplejsonora supportino nativamente questo con l' namedtuple_as_objectopzione, che per impostazione predefinita è True.


3
La tua modifica è la risposta corretta. simplejson serializza namedtuples in modo diverso (la mia opinione: migliore) rispetto a json. Questo rende davvero il pattern: "try: import simplejson as json tranne: import json", rischioso poiché potresti ottenere un comportamento diverso su alcune macchine a seconda che simplejson sia installato. Per questo motivo, ora richiedo simplejson in molti dei miei file di installazione e mi astengo da questo schema.
marr75,

1
@ marr75 - Idem per ujson, che è ancora più bizzarro e imprevedibile in questi casi limite ...
mac

Sono stato in grado di ottenere una namedtuple ricorsiva serializzata su json (abbastanza stampato) usando:simplejson.dumps(my_tuple, indent=4)
KFL

5

Ho scritto una libreria per farlo: https://github.com/ltworf/typedload

Può andare da e a tupla con nome e viceversa.

Supporta strutture annidate piuttosto complicate, con elenchi, insiemi, enumerazioni, unioni, valori predefiniti. Dovrebbe coprire i casi più comuni.

modifica: la libreria supporta anche dataclass e classi attr.


2

Converte in modo ricorsivo i dati namedTuple in json.

print(m1)
## Message(id=2, agent=Agent(id=1, first_name='asd', last_name='asd', mail='2@mai.com'), customer=Customer(id=1, first_name='asd', last_name='asd', mail='2@mai.com', 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': '2@mai.com',
'id': 1},
'content': 'text',
'customer': {'first_name': 'asd',
'last_name': 'asd',
'mail': '2@mai.com',
'phone_number': 123123,
'id': 1},
'id': 2,
'la': 123123,
'ls': 4512313,
'media_url': 'h.com',
'type': 'image'}

1
+1 ho fatto quasi lo stesso. Ma il tuo ritorno è un dict non json. Devi avere "no", e se un valore nel tuo oggetto è booleano, non verrà convertito in true. Penso che sia più sicuro trasformarlo in dict, quindi usa json.dumps per convertirlo in json.
Fred Laurent,

2

C'è una soluzione più conveniente è utilizzare il decoratore (utilizza il campo protetto _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 e versioni successive:

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

Non farlo, cambiano continuamente l'API interna. La mia libreria typedload ha diversi casi per diverse versioni di py.
LtWorf

Sì, è chiaro. Tuttavia, nessuno dovrebbe migrare a una versione più recente di Python senza test. E le altre soluzioni utilizzano _asdict, che è anche un membro di classe "protetto".
Dmitry T.

1
LtWorf, la tua libreria è GPL e non funziona con frozensets
Thomas Grainger

2
@LtWorf La tua libreria usa anche _fields;-) github.com/ltworf/typedload/blob/master/typedload/datadumper.py Fa parte dell'API pubblica di namedtuple, in realtà: docs.python.org/3.7/library/… La gente viene confusa da il carattere di sottolineatura (non c'è da stupirsi!). È un cattivo design, ma non so quale altra scelta avessero.
quant_dev

1
Quali cose? Quando? Puoi citare le note di rilascio?
quant_dev

2

La libreria jsonplus fornisce un serializzatore per le istanze NamedTuple. Usa la sua modalità di compatibilità per produrre oggetti semplici se necessario, ma preferisci l'impostazione predefinita in quanto è utile per la decodifica.


Ho esaminato le altre soluzioni qui e ho scoperto che la semplice aggiunta di questa dipendenza mi ha fatto risparmiare molto tempo. Soprattutto perché avevo un elenco di NamedTuples che dovevo passare come json nella sessione. jsonplus ti consente fondamentalmente di ottenere elenchi di tuple con nome dentro e fuori da json .dumps()e .loads()nessuna configurazione funziona.
Rob il

1

È impossibile serializzare correttamente le namedtuples con la libreria json nativa di Python. Vedrà sempre le tuple come elenchi ed è impossibile sovrascrivere il serializzatore predefinito per modificare questo comportamento. È peggio se gli oggetti sono nidificati.

Meglio usare una libreria più robusta come 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
}

1
orjsonAnch'io sono un fan di .
CircleOnCircles

0

Questa è una vecchia domanda. Però:

Un suggerimento per tutti coloro che hanno la stessa domanda, pensa attentamente all'utilizzo di qualsiasi caratteristica privata o interna del NamedTupleperché hanno prima e cambieranno di nuovo nel tempo.

Ad esempio, se il tuo NamedTupleè un oggetto di valore flat e sei interessato solo a serializzarlo e non nei casi in cui è annidato in un altro oggetto, potresti evitare i problemi che potrebbero sorgere con la __dict__rimozione o la _as_dict()modifica e fai semplicemente qualcosa di simile (e sì, questo è Python 3 perché questa risposta è per il momento):

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

Ho provato a utilizzare il defaultkwarg richiamabile dumpsper per eseguire la to_dict()chiamata se disponibile, ma non è stato chiamato in quanto NamedTupleè convertibile in un elenco.


3
_asdictfa parte dell'API pubblica namedtuple. Spiegano il motivo del carattere di sottolineatura docs.python.org/3.7/library/… "Oltre ai metodi ereditati dalle tuple, le tuple con nome supportano tre metodi aggiuntivi e due attributi. Per evitare conflitti con i nomi dei campi, i nomi dei metodi e degli attributi inizia con un trattino basso. "
quant_dev

@quant_dev grazie, non ho visto quella spiegazione. Non è una garanzia di stabilità api, ma aiuta a rendere questi metodi più affidabili. Mi piace la leggibilità esplicita di to_dict, ma posso vedere che sembra reimplementare _as_dict
dlamblin

0

Ecco la mia opinione sul problema. Serializza la NamedTuple, si prende cura delle NamedTuples e degli elenchi piegati al loro interno

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

0

simplejson.dump()invece di json.dumpfare il lavoro. Potrebbe essere più lento però.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.