wake-up-neo.com

Objekt JSON mit normalem Encoder serialisierbar machen

Bei der JSON-Serialisierung von benutzerdefinierten nicht serialisierbaren Objekten wird normalerweise die Unterklasse json.JSONEncoder verwendet und anschließend ein benutzerdefinierter Encoder an Dumps übergeben.

Es sieht normalerweise so aus:

class CustomEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, foo):
            return obj.to_json()

        return json.JSONEncoder.default(self, obj)

print json.dumps(obj, cls = CustomEncoder)

Was ich versuche, ist, etwas mit dem Standard-Encoder serialisierbar zu machen. Ich sah mich um, konnte aber nichts finden. Mein Gedanke ist, dass es ein Feld gibt, in dem der Encoder die Json-Codierung ermittelt. Etwas ähnlich wie __str__. Vielleicht ein __json__-Feld . Gibt es so etwas in Python?

Ich möchte eine Klasse eines Moduls machen, das ich mache, um JSON für alle, die das Paket verwenden, serialisierbar zu machen, ohne dass sie sich Sorgen machen müssen, ob sie ihre eigenen [trivialen] benutzerdefinierten Encoder implementieren.

53
leonsas

Wie ich in einem Kommentar zu Ihrer Frage gesagt habe, scheint der Quellcode des json-Moduls nicht geeignet zu sein, was Sie wollen. Das Ziel könnte jedoch durch das sogenannte monkey-patching Erreicht werden (siehe Frage Was ist ein Affen-Patch? ) . Dies könnte sein Dies geschieht im __init__.py-Initialisierungsskript Ihres Pakets und würde sich auf alle nachfolgenden json-Modul-Serialisierungen auswirken, da Module in der Regel nur einmal geladen werden und das Ergebnis in sys.modules zwischengespeichert wird.

Der Patch ändert die default-Methode des Standard-Json-Encoders - die Standardfunktion default().

Hier ein Beispiel, das der Einfachheit halber als eigenständiges Modul implementiert wurde:

Modul: make_json_serializable.py

""" Module that monkey-patches json module when it's imported so
JSONEncoder.default() automatically checks for a special "to_json()"
method and uses it to encode the object if found.
"""
from json import JSONEncoder

def _default(self, obj):
    return getattr(obj.__class__, "to_json", _default.default)(obj)

_default.default = JSONEncoder.default  # Save unmodified default.
JSONEncoder.default = _default # Replace it.

Die Verwendung ist trivial, da der Patch durch einfaches Importieren des Moduls angewendet wird.

Beispiel-Client-Skript:

import json
import make_json_serializable  # apply monkey-patch

class Foo(object):
    def __init__(self, name):
        self.name = name
    def to_json(self):  # New special method.
        """ Convert to JSON format string representation. """
        return '{"name": "%s"}' % self.name

foo = Foo('sazpaz')
print(json.dumps(foo))  # -> "{\"name\": \"sazpaz\"}"

Um die Objekttypinformationen beizubehalten, kann die spezielle Methode sie auch in die zurückgegebene Zeichenfolge aufnehmen:

        return ('{"type": "%s", "name": "%s"}' %
                 (self.__class__.__name__, self.name))

Welches die folgende JSON erzeugt, die jetzt den Klassennamen enthält:

"{\"type\": \"Foo\", \"name\": \"sazpaz\"}"

Magie liegt hier

Besser noch als den Ersatz default() nach einer speziell benannten Methode suchen zu lassen, wäre, die meisten Python-Objekte automatisch, einschließlich benutzerdefinierter Klasseninstanzen, serialisieren zu können, ohne eine spezielle Methode hinzufügen zu müssen. Nachdem ich eine Reihe von Alternativen recherchiert hatte, erschien mir das Folgende, das das Modul pickle verwendet, diesem Ideal am nächsten:

Modul: make_json_serializable2.py

""" Module that imports the json module and monkey-patches it so
JSONEncoder.default() automatically pickles any Python objects
encountered that aren't standard JSON data types.
"""
from json import JSONEncoder
import pickle

def _default(self, obj):
    return {'_python_object': pickle.dumps(obj)}

JSONEncoder.default = _default  # Replace with the above.

Natürlich kann nicht alles gebeizt werden, zB Erweiterungstypen. Es gibt jedoch Möglichkeiten, wie man sie über das Pickle-Protokoll handhaben kann, indem sie spezielle Methoden schreiben - ähnlich wie Sie es bereits vorgeschlagen und beschrieben haben - aber dies wäre wahrscheinlich für eine weitaus geringere Anzahl von Fällen erforderlich.

Unabhängig davon bedeutet die Verwendung des Pickle-Protokolls, dass das ursprüngliche Python-Objekt relativ einfach rekonstruiert werden kann, indem ein benutzerdefiniertes object_hook-Funktionsargument für alle json.loads()-Aufrufe bereitgestellt wird, die nach einem '_python_object'-Schlüssel im übergebenen Wörterbuch gesucht haben.

def as_python_object(dct):
    if '_python_object' in dct:
        return pickle.loads(str(dct['_python_object']))
    return dct

pyobj = json.loads(json_str, object_hook=as_python_object)

Wenn dies an vielen Stellen erfolgen muss, kann es sinnvoll sein, eine Wrapperfunktion zu definieren, die automatisch das zusätzliche Schlüsselwortargument bereitstellt:

json_pkloads = functools.partial(json.loads, object_hook=as_python_object)

pyobj = json_pkloads(json_str)

Natürlich könnte dies auch ein Affen-Patch im json-Modul sein, wodurch die Funktion zur Standardeinstellung object_hook (anstelle von None) wird.

Ich kam auf die Idee, pickle von einer answer by Raymond Hettinger zu einer anderen JSON-Serialisierungsfrage zu verwenden, die ich für außergewöhnlich glaubwürdig halte, sowie eine offizielle Quelle (wie in Python-Core-Entwickler).

Portablity zu Python 3

Der obige Code funktioniert nicht wie in Python 3 gezeigt, da json.dumps() ein bytes-Objekt zurückgibt, das die JSONEncoder nicht verarbeiten kann. Der Ansatz ist jedoch noch gültig. Um das Problem zu umgehen, können Sie den von pickle.dumps() zurückgegebenen Wert latin1 "decodieren" und anschließend von latin1 "codieren", bevor Sie ihn in der pickle.loads()-Funktion an as_python_object() übergeben. Dies funktioniert, da beliebige binäre Zeichenfolgen latin1 gültig sind, die immer in Unicode dekodiert und dann wieder in die ursprüngliche Zeichenfolge zurückgeschrieben werden können (wie in diese Antwort von Sven Marnach ) hervorgehoben.

(Obwohl das Folgende in Python 2 gut funktioniert, ist die latin1-Dekodierung und -Kodierung überflüssig.)

from decimal import Decimal

class PythonObjectEncoder(json.JSONEncoder):
    def default(self, obj):
        return {'_python_object': pickle.dumps(obj).decode('latin1')}

def as_python_object(dct):
    if '_python_object' in dct:
        return pickle.loads(dct['_python_object'].encode('latin1'))
    return dct

data = [1,2,3, set(['knights', 'who', 'say', 'ni']), {'key':'value'},
        Decimal('3.14')]
j = json.dumps(data, cls=PythonObjectEncoder, indent=4)
data2 = json.loads(j, object_hook=as_python_object)
assert data == data2  # both should be same
66
martineau

Sie können die Dict-Klasse folgendermaßen erweitern:

#!/usr/local/bin/python3
import json

class Serializable(dict):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # hack to fix _json.so make_encoder serialize properly
        self.__setitem__('dummy', 1)

    def _myattrs(self):
        return [
            (x, self._repr(getattr(self, x))) 
            for x in self.__dir__() 
            if x not in Serializable().__dir__()
        ]

    def _repr(self, value):
        if isinstance(value, (str, int, float, list, Tuple, dict)):
            return value
        else:
            return repr(value)

    def __repr__(self):
        return '<%s.%s object at %s>' % (
            self.__class__.__module__,
            self.__class__.__name__,
            hex(id(self))
        )

    def keys(self):
        return iter([x[0] for x in self._myattrs()])

    def values(self):
        return iter([x[1] for x in self._myattrs()])

    def items(self):
        return iter(self._myattrs())

Um Ihre Klassen mit dem regulären Encoder serialisierbar zu machen, erweitern Sie 'Serializable':

class MySerializableClass(Serializable):

    attr_1 = 'first attribute'
    attr_2 = 23

    def my_function(self):
        print('do something here')


obj = MySerializableClass()

print(obj) druckt etwas wie:

<__main__.MySerializableClass object at 0x1073525e8>

print(json.dumps(obj, indent=4)) druckt etwas wie:

{
    "attr_1": "first attribute",
    "attr_2": 23,
    "my_function": "<bound method MySerializableClass.my_function of <__main__.MySerializableClass object at 0x1073525e8>>"
}
11
Aravindan Ve

Ich schlage vor, den Hack in die Klassendefinition aufzunehmen. Sobald die Klasse definiert ist, unterstützt sie JSON. Beispiel:

import json

class MyClass( object ):

    def _jsonSupport( *args ):
        def default( self, xObject ):
            return { 'type': 'MyClass', 'name': xObject.name() }

        def objectHook( obj ):
            if 'type' not in obj:
                return obj
            if obj[ 'type' ] != 'MyClass':
                return obj
            return MyClass( obj[ 'name' ] )
        json.JSONEncoder.default = default
        json._default_decoder = json.JSONDecoder( object_hook = objectHook )

    _jsonSupport()

    def __init__( self, name ):
        self._name = name

    def name( self ):
        return self._name

    def __repr__( self ):
        return '<MyClass(name=%s)>' % self._name

myObject = MyClass( 'Magneto' )
jsonString = json.dumps( [ myObject, 'some', { 'other': 'objects' } ] )
print "json representation:", jsonString

decoded = json.loads( jsonString )
print "after decoding, our object is the first in the list", decoded[ 0 ]
4

Das Problem beim Überschreiben von JSONEncoder().default ist, dass Sie dies nur einmal tun können. Wenn Sie auf einen bestimmten Datentyp stoßen, der mit diesem Muster nicht funktioniert (z. B. wenn Sie eine seltsame Kodierung verwenden). Mit dem folgenden Muster können Sie Ihre JSON-Klasse jederzeit serialisierbar machen, vorausgesetzt, das Klassenfeld, das Sie serialisieren möchten, ist selbst serialisierbar (und kann einer Python-Liste hinzugefügt werden. Andernfalls müssen Sie dasselbe Muster rekursiv auf Ihr json-Feld anwenden (oder die serialisierbaren Daten daraus extrahieren):

# base class that will make all derivatives JSON serializable:
class JSONSerializable(list): # need to derive from a serializable class.

  def __init__(self, value = None):
    self = [ value ]

  def setJSONSerializableValue(self, value):
    self = [ value ]

  def getJSONSerializableValue(self):
    return self[1] if len(self) else None


# derive  your classes from JSONSerializable:
class MyJSONSerializableObject(JSONSerializable):

  def __init__(self): # or any other function
    # .... 
    # suppose your__json__field is the class member to be serialized. 
    # it has to be serializable itself. 
    # Every time you want to set it, call this function:
    self.setJSONSerializableValue(your__json__field)
    # ... 
    # ... and when you need access to it,  get this way:
    do_something_with_your__json__field(self.getJSONSerializableValue())


# now you have a JSON default-serializable class:
a = MyJSONSerializableObject()
print json.dumps(a)
1
ribamar

Bereiten Sie für die Produktionsumgebung ein eigenes Modul von json mit Ihrem eigenen benutzerdefinierten Encoder vor, um deutlich zu machen, dass Sie etwas überschreiben. Monkey-Patch wird nicht empfohlen, Sie können jedoch einen Affen-Patch in Ihrem Testv ausführen.

Zum Beispiel,

class JSONDatetimeAndPhonesEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, (datetime.date, datetime.datetime)):
            return obj.date().isoformat()
        Elif isinstance(obj, basestring):
            try:
                number = phonenumbers.parse(obj)
            except phonenumbers.NumberParseException:
                return json.JSONEncoder.default(self, obj)
            else:
                return phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.NATIONAL)
        else:
            return json.JSONEncoder.default(self, obj)

sie wollen:

payload = json.dumps (your_data, cls = JSONDatetimeAndPhonesEncoder)

oder:

nutzlast = Ihre_Dumps (Ihre_Daten)

oder:

payload = Ihre_json.dumps (Ihre_Daten)

gehen Sie jedoch in der Testumgebung einen Kopf:

@pytest.fixture(scope='session', autouse=True)
def testenv_monkey_patching():
    json._default_encoder = JSONDatetimeAndPhonesEncoder()

dadurch wird Ihr Encoder auf alle json.dumps-Vorkommen angewendet.

0

Ich verstehe nicht, warum Sie keine serialize-Funktion für Ihre eigene Klasse schreiben können? Sie implementieren den benutzerdefinierten Encoder in der Klasse selbst und ermöglichen "Personen", die serialize-Funktion aufzurufen, die im Wesentlichen self.__dict__ zurückgibt, wobei die Funktionen entfernt werden.

bearbeiten:

Diese Frage stimmt mir zu, dass der einfachste Weg ist, Ihre eigene Methode zu schreiben und die von Json serialisierten Daten zurückzugeben, die Sie möchten. Sie empfehlen auch, Jsonpickle auszuprobieren, aber Sie fügen jetzt eine zusätzliche Abhängigkeit für die Schönheit hinzu, wenn die richtige Lösung eingebaut ist.

0
blakev