Serializzazione dell'istanza della classe su JSON


186

Sto cercando di creare una rappresentazione di stringa JSON di un'istanza di classe e ho delle difficoltà. Diciamo che la classe è costruita in questo modo:

class testclass:
    value1 = "a"
    value2 = "b"

Viene effettuata una chiamata a json.dumps in questo modo:

t = testclass()
json.dumps(t)

Non riesce e mi dice che la classe di test non è serializzabile JSON.

TypeError: <__main__.testclass object at 0x000000000227A400> is not JSON serializable

Ho anche provato a usare il modulo pickle:

t = testclass()
print(pickle.dumps(t, pickle.HIGHEST_PROTOCOL))

E fornisce informazioni sull'istanza di classe ma non un contenuto serializzato dell'istanza di classe.

b'\x80\x03c__main__\ntestclass\nq\x00)\x81q\x01}q\x02b.'

Che cosa sto facendo di sbagliato?



30
Utilizzare una linea, s = json.dumps(obj, default=lambda x: x.__dict__)per le variabili di istanza dell'oggetto serialize ( self.value1, self.value2, ...). È il modo più semplice e diretto. Serializzerà le strutture di oggetti nidificati. La defaultfunzione viene chiamata quando un determinato oggetto non è direttamente serializzabile. Puoi anche guardare la mia risposta qui sotto. Ho trovato le risposte popolari inutilmente complesse, che probabilmente erano vere molto tempo fa.
codeman48,

1
Il tuo testclassnon ha __init__()metodo, in modo da tutte le istanze condivideranno gli stessi due attributi di classe ( value1e value2) definiti nella dichiarazione della classe. Capisci la differenza tra una classe e un'istanza di una?
martineau,

1
C'è una libreria Python per questo github.com/jsonpickle/jsonpickle (commentare poiché la risposta è troppo sotto nella discussione e non sarà raggiungibile.)
migliori auguri

Risposte:


238

Il problema di base è che il codificatore JSON json.dumps()sa solo come serializzare un set limitato di tipi di oggetti per impostazione predefinita, tutti i tipi predefiniti. Elenco qui: https://docs.python.org/3.3/library/json.html#encoders-and-decoders

Una buona soluzione sarebbe quella di far ereditare la tua classe JSONEncodere quindi implementare la JSONEncoder.default()funzione, e far sì che quella funzione emetta il JSON corretto per la tua classe.

Una soluzione semplice sarebbe quella di chiamare json.dumps()il .__dict__membro di quell'istanza. Questo è un Python standard dicte se la tua classe è semplice sarà serializzabile JSON.

class Foo(object):
    def __init__(self):
        self.x = 1
        self.y = 2

foo = Foo()
s = json.dumps(foo) # raises TypeError with "is not JSON serializable"

s = json.dumps(foo.__dict__) # s set to: {"x":1, "y":2}

L'approccio sopra è discusso in questo post sul blog:

    Serializzazione di oggetti Python arbitrari su JSON usando __dict__


3
Ci ho provato Il risultato finale di una chiamata a json.dumps (t .__ dict__) è solo {}.
ferhan,

6
Questo perché la tua classe non ha una .__init__()funzione metodo, quindi la tua istanza di classe ha un dizionario vuoto. In altre parole, {}è il risultato corretto per il tuo codice di esempio.
steveha,

3
Grazie. Questo fa il trucco. Ho aggiunto un semplice init senza parametri e ora chiamando json.dumps (t .__ dict__) restituisce i dati corretti nel formato di: {"valore2": "345", "valore1": "123"} avevo visto post come questo prima, non ero sicuro se avessi bisogno di un serializzatore personalizzato per i membri, la necessità di init non era menzionata esplicitamente o mi mancava. Grazie.
ferhan,

3
Questo lavoro per una singola classe ma non con oggetti correlati alle classi correlate
Nwawel A Iroume,

2
@NwawelAIroume: True. Se hai un oggetto che, ad esempio, contiene più oggetti in un elenco, l'errore è ancorais not JSON serializable
gies0r

57

C'è un modo che funziona alla grande per me che puoi provare:

json.dumps()può assumere un parametro predefinito predefinito in cui è possibile specificare una funzione serializzatore personalizzata per tipi sconosciuti, che nel mio caso assomiglia

def serialize(obj):
    """JSON serializer for objects not serializable by default json code"""

    if isinstance(obj, date):
        serial = obj.isoformat()
        return serial

    if isinstance(obj, time):
        serial = obj.isoformat()
        return serial

    return obj.__dict__

I primi due if sono per la serializzazione di data e ora, quindi viene obj.__dict__restituito un altro oggetto.

l'ultima chiamata è simile a:

json.dumps(myObj, default=serialize)

È particolarmente utile quando si serializza una raccolta e non si desidera chiamare __dict__esplicitamente per ogni oggetto. Qui è fatto automaticamente per te.

Finora ha funzionato così bene per me, in attesa di tuoi pensieri.


Ho capito NameError: name 'serialize' is not defined. Qualche consiglio?
Kyle Delaney,

Molto bella. Solo per le classi che hanno slot:try: dict = obj.__dict__ except AttributeError: dict = {s: getattr(obj, s) for s in obj.__slots__ if hasattr(obj, s)} return dict
fantastico

È sorprendente che un linguaggio così popolare non abbia una sola fodera per confondere un oggetto. Deve essere perché non è tipizzato staticamente.
TheRennen,

49

È possibile specificare il defaultparametro indicato nella json.dumps()funzione:

json.dumps(obj, default=lambda x: x.__dict__)

Spiegazione:

Forma i documenti ( 2.7 , 3.6 ):

``default(obj)`` is a function that should return a serializable version
of obj or raise TypeError. The default simply raises TypeError.

(Funziona su Python 2.7 e Python 3.x)

Nota: in questo caso sono necessarie instancevariabili e non classvariabili, come prova l'esempio nella domanda. (Sto supponendo che chi chiede fosse destinato class instancea essere un oggetto di una classe)

L'ho imparato prima dalla risposta di @ phihag qui . Ho trovato il modo più semplice e pulito per fare il lavoro.


6
Questo ha funzionato per me, ma a causa dei membri datetime.date l'ho cambiato leggermente:default=lambda x: getattr(x, '__dict__', str(x))
Dakota Hawkins,

@Dakota bella soluzione; datetime.dateè un'implementazione in C quindi non ha alcun __dict__attributo. IMHO per uniformità, datetime.datedovrebbe averlo ...
codeman48

22

Faccio solo:

data=json.dumps(myobject.__dict__)

Questa non è la risposta completa, e se hai una sorta di classe di oggetti complicata non otterrai sicuramente tutto. Comunque lo uso per alcuni dei miei oggetti semplici.

Uno su cui funziona davvero bene è la classe "opzioni" che si ottiene dal modulo OptionParser. Qui è insieme alla stessa richiesta JSON.

  def executeJson(self, url, options):
        data=json.dumps(options.__dict__)
        if options.verbose:
            print data
        headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
        return requests.post(url, data, headers=headers)

Potresti voler rimuovere self, se non lo usi all'interno di una classe.
SpiRail,

3
Funzionerà bene, purché l'oggetto non sia composto da altri oggetti.
Haroldo_OK,

19

Usando jsonpickle

import jsonpickle

object = YourClass()
json_object = jsonpickle.encode(object)

5

JSON non è realmente pensato per serializzare oggetti Python arbitrari. È ottimo per serializzare dictoggetti, ma il picklemodulo è davvero quello che dovresti usare in generale. L'output da picklenon è realmente leggibile dall'uomo, ma dovrebbe essere decompresso bene. Se insisti nell'utilizzare JSON, potresti dare un'occhiata al jsonpicklemodulo, che è un approccio ibrido interessante.

https://github.com/jsonpickle/jsonpickle


9
Il problema principale che vedo con Pickle è che è un formato specifico di Python, mentre JSON è un formato indipendente dalla piattaforma. JSON è particolarmente utile se stai scrivendo un'applicazione Web o un back-end per alcune applicazioni mobili. Detto questo, grazie per aver sottolineato Jsonpickle.
Haroldo_OK,

@Haroldo_OK Jsonpickle non viene ancora esportato in JSON, ma non è leggibile in modo molto umano?
Caelum,

4

Ecco due semplici funzioni per la serializzazione di qualsiasi classe non sofisticata, niente di speciale come spiegato prima.

Lo uso per roba di tipo di configurazione perché posso aggiungere nuovi membri alle classi senza aggiustamenti del codice.

import json

class SimpleClass:
    def __init__(self, a=None, b=None, c=None):
        self.a = a
        self.b = b
        self.c = c

def serialize_json(instance=None, path=None):
    dt = {}
    dt.update(vars(instance))

    with open(path, "w") as file:
        json.dump(dt, file)

def deserialize_json(cls=None, path=None):
    def read_json(_path):
        with open(_path, "r") as file:
            return json.load(file)

    data = read_json(path)

    instance = object.__new__(cls)

    for key, value in data.items():
        setattr(instance, key, value)

    return instance

# Usage: Create class and serialize under Windows file system.
write_settings = SimpleClass(a=1, b=2, c=3)
serialize_json(write_settings, r"c:\temp\test.json")

# Read back and rehydrate.
read_settings = deserialize_json(SimpleClass, r"c:\temp\test.json")

# results are the same.
print(vars(write_settings))
print(vars(read_settings))

# output:
# {'c': 3, 'b': 2, 'a': 1}
# {'c': 3, 'b': 2, 'a': 1}

3

Ci sono alcune buone risposte su come iniziare a farlo. Ma ci sono alcune cose da tenere a mente:

  • Cosa succede se l'istanza è nidificata all'interno di una struttura di dati di grandi dimensioni?
  • E se volessi anche il nome della classe?
  • E se si desidera deserializzare l'istanza?
  • Cosa succede se si utilizza __slots__invece di __dict__?
  • E se non volessi farlo da solo?

json-tricks è una libreria (che ho creato e a cui altri hanno contribuito) che è stata in grado di farlo per un bel po '. Per esempio:

class MyTestCls:
    def __init__(self, **kwargs):
        for k, v in kwargs.items():
            setattr(self, k, v)

cls_instance = MyTestCls(s='ub', dct={'7': 7})

json = dumps(cls_instance, indent=4)
instance = loads(json)

Riavrai la tua istanza. Qui il json si presenta così:

{
    "__instance_type__": [
        "json_tricks.test_class",
        "MyTestCls"
    ],
    "attributes": {
        "s": "ub",
        "dct": {
            "7": 7
        }
    }
}

Se ti piace creare la tua soluzione, potresti guardare alla fonte json-tricksper non dimenticare alcuni casi speciali (come __slots__).

Fa anche altri tipi come array intorpiditi, periodi di dati, numeri complessi; consente anche commenti.


3

Python3.x

Il miglior approccio che sono riuscito a raggiungere con le mie conoscenze è stato questo.
Nota che questo codice tratta anche set ().
Questo approccio è generico e richiede solo l'estensione della classe (nel secondo esempio).
Nota che lo sto solo facendo ai file, ma è facile modificare il comportamento secondo i tuoi gusti.

Tuttavia questo è un CoDec.

Con un po 'più di lavoro puoi costruire la tua classe in altri modi. Presumo un costruttore predefinito per istanziarlo, quindi aggiorno il dict di classe.

import json
import collections


class JsonClassSerializable(json.JSONEncoder):

    REGISTERED_CLASS = {}

    def register(ctype):
        JsonClassSerializable.REGISTERED_CLASS[ctype.__name__] = ctype

    def default(self, obj):
        if isinstance(obj, collections.Set):
            return dict(_set_object=list(obj))
        if isinstance(obj, JsonClassSerializable):
            jclass = {}
            jclass["name"] = type(obj).__name__
            jclass["dict"] = obj.__dict__
            return dict(_class_object=jclass)
        else:
            return json.JSONEncoder.default(self, obj)

    def json_to_class(self, dct):
        if '_set_object' in dct:
            return set(dct['_set_object'])
        elif '_class_object' in dct:
            cclass = dct['_class_object']
            cclass_name = cclass["name"]
            if cclass_name not in self.REGISTERED_CLASS:
                raise RuntimeError(
                    "Class {} not registered in JSON Parser"
                    .format(cclass["name"])
                )
            instance = self.REGISTERED_CLASS[cclass_name]()
            instance.__dict__ = cclass["dict"]
            return instance
        return dct

    def encode_(self, file):
        with open(file, 'w') as outfile:
            json.dump(
                self.__dict__, outfile,
                cls=JsonClassSerializable,
                indent=4,
                sort_keys=True
            )

    def decode_(self, file):
        try:
            with open(file, 'r') as infile:
                self.__dict__ = json.load(
                    infile,
                    object_hook=self.json_to_class
                )
        except FileNotFoundError:
            print("Persistence load failed "
                  "'{}' do not exists".format(file)
                  )


class C(JsonClassSerializable):

    def __init__(self):
        self.mill = "s"


JsonClassSerializable.register(C)


class B(JsonClassSerializable):

    def __init__(self):
        self.a = 1230
        self.c = C()


JsonClassSerializable.register(B)


class A(JsonClassSerializable):

    def __init__(self):
        self.a = 1
        self.b = {1, 2}
        self.c = B()

JsonClassSerializable.register(A)

A().encode_("test")
b = A()
b.decode_("test")
print(b.a)
print(b.b)
print(b.c.a)

modificare

Con un po 'più di ricerca ho trovato un modo per generalizzare senza la necessità della chiamata al metodo del registro SUPERCLASS , usando una metaclasse

import json
import collections

REGISTERED_CLASS = {}

class MetaSerializable(type):

    def __call__(cls, *args, **kwargs):
        if cls.__name__ not in REGISTERED_CLASS:
            REGISTERED_CLASS[cls.__name__] = cls
        return super(MetaSerializable, cls).__call__(*args, **kwargs)


class JsonClassSerializable(json.JSONEncoder, metaclass=MetaSerializable):

    def default(self, obj):
        if isinstance(obj, collections.Set):
            return dict(_set_object=list(obj))
        if isinstance(obj, JsonClassSerializable):
            jclass = {}
            jclass["name"] = type(obj).__name__
            jclass["dict"] = obj.__dict__
            return dict(_class_object=jclass)
        else:
            return json.JSONEncoder.default(self, obj)

    def json_to_class(self, dct):
        if '_set_object' in dct:
            return set(dct['_set_object'])
        elif '_class_object' in dct:
            cclass = dct['_class_object']
            cclass_name = cclass["name"]
            if cclass_name not in REGISTERED_CLASS:
                raise RuntimeError(
                    "Class {} not registered in JSON Parser"
                    .format(cclass["name"])
                )
            instance = REGISTERED_CLASS[cclass_name]()
            instance.__dict__ = cclass["dict"]
            return instance
        return dct

    def encode_(self, file):
        with open(file, 'w') as outfile:
            json.dump(
                self.__dict__, outfile,
                cls=JsonClassSerializable,
                indent=4,
                sort_keys=True
            )

    def decode_(self, file):
        try:
            with open(file, 'r') as infile:
                self.__dict__ = json.load(
                    infile,
                    object_hook=self.json_to_class
                )
        except FileNotFoundError:
            print("Persistence load failed "
                  "'{}' do not exists".format(file)
                  )


class C(JsonClassSerializable):

    def __init__(self):
        self.mill = "s"


class B(JsonClassSerializable):

    def __init__(self):
        self.a = 1230
        self.c = C()


class A(JsonClassSerializable):

    def __init__(self):
        self.a = 1
        self.b = {1, 2}
        self.c = B()


A().encode_("test")
b = A()
b.decode_("test")
print(b.a)
# 1
print(b.b)
# {1, 2}
print(b.c.a)
# 1230
print(b.c.c.mill)
# s

2

Credo che invece dell'eredità come suggerito nella risposta accettata, è meglio usare il polimorfismo. Altrimenti devi avere una grande istruzione if else per personalizzare la codifica di ogni oggetto. Ciò significa che creare un codificatore predefinito generico per JSON come:

def jsonDefEncoder(obj):
   if hasattr(obj, 'jsonEnc'):
      return obj.jsonEnc()
   else: #some default behavior
      return obj.__dict__

e quindi avere una jsonEnc()funzione in ogni classe che si desidera serializzare. per esempio

class A(object):
   def __init__(self,lengthInFeet):
      self.lengthInFeet=lengthInFeet
   def jsonEnc(self):
      return {'lengthInMeters': lengthInFeet * 0.3 } # each foot is 0.3 meter

Quindi chiami json.dumps(classInstance,default=jsonDefEncoder)

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.