web-dev-qa-db-fra.com

Comment faire json.dumps dans Python ignorer un champ non sérialisable

J'essaie de sérialiser la sortie de l'analyse de certaines données binaires avec la bibliothèque Construct2.9. Je veux sérialiser le résultat en JSON.

packet est une instance d'une classe Construct Container.

Apparemment, il contient un _io Caché de type BytesIO - voir la sortie de dict(packet) ci-dessous:

{
'packet_length': 76, 'uart_sent_time': 1, 'frame_number': 42958, 
'subframe_number': 0, 'checksum': 33157, '_io': <_io.BytesIO object at 0x7f81c3153728>, 
'platform':661058, 'sync': 506660481457717506, 'frame_margin': 20642,
'num_tlvs': 1, 'track_process_time': 593, 'chirp_margin': 78,
'timestamp': 2586231182, 'version': 16908293
}

Maintenant, appeler json.dumps(packet) conduit évidemment à une TypeError:

...

File "/usr/lib/python3.5/json/__init__.py", line 237, in dumps
    **kw).encode(obj)
File "/usr/lib/python3.5/json/encoder.py", line 198, in encode
    chunks = self.iterencode(o, _one_shot=True)
File "/usr/lib/python3.5/json/encoder.py", line 256, in iterencode
    return _iterencode(o, 0)
File "/usr/lib/python3.5/json/encoder.py", line 179, in default
    raise TypeError(repr(o) + " is not JSON serializable")
TypeError: <_io.BytesIO object at 0x7f81c3153728> is not JSON serializable

Cependant, ce qui me dérange, c'est que l'exécution de json.dumps(packet, skipkeys=True) entraîne exactement la même erreur, alors que je m'attendrais à ce qu'il saute le champ _io. Quel est le problème ici? Pourquoi skipkeys ne me permet-il pas de sauter le champ _io?

J'ai réussi à faire fonctionner le code en remplaçant JSONEncoder et en retournant None pour les champs de type BytesIO, mais cela signifie que ma chaîne sérialisée contient des charges d'éléments "_io": null, que je préférerais ne pas avoir du tout ...

7
mz8i

Les clés avec un trait de soulignement _ Ne sont pas vraiment "cachées", ce sont juste des chaînes de plus pour JSON. La classe Construct Container n'est qu'un dictionnaire avec ordre, la clé _io N'a rien de spécial pour cette classe.

Vous avez deux options:

  • implémentez un hook default qui renvoie simplement une valeur de remplacement.
  • Filtrez les paires clé-valeur dont vous savez qu'elles ne peuvent pas fonctionner avant la sérialisation.

et peut-être un troisième, mais une analyse informelle des pages du projet Construct ne me dit pas si elle est disponible: avoir Construct sortie JSON ou au moins un dictionnaire compatible JSON, peut-être en utilisant des adaptateurs.

Le hook par défaut ne peut pas empêcher l'ajout de la clé _io À la sortie, mais vous permettrait au moins d'éviter l'erreur:

json.dumps(packet, default=lambda o: '<not serializable>')

Le filtrage peut être effectué récursivement; le @functools.singledispatch() décorateur peut aider à garder un tel code propre:

from functools import singledispatch

_cant_serialize = object()

@singledispatch
def json_serializable(object, skip_underscore=False):
    """Filter a Python object to only include serializable object types

    In dictionaries, keys are converted to strings; if skip_underscore is true
    then keys starting with an underscore ("_") are skipped.

    """
    # default handler, called for anything without a specific
    # type registration.
    return _cant_serialize

@json_serializable.register(dict)
def _handle_dict(d, skip_underscore=False):
    converted = ((str(k), json_serializable(v, skip_underscore))
                 for k, v in d.items())
    if skip_underscore:
        converted = ((k, v) for k, v in converted if k[:1] != '_')
    return {k: v for k, v in converted if v is not _cant_serialize}

@json_serializable.register(list)
@json_serializable.register(Tuple)
def _handle_sequence(seq, skip_underscore=False):
    converted = (json_serializable(v, skip_underscore) for v in seq)
    return [v for v in converted if v is not _cant_serialize]

@json_serializable.register(int)
@json_serializable.register(float)
@json_serializable.register(str)
@json_serializable.register(bool)  # redudant, supported as int subclass
@json_serializable.register(type(None))
def _handle_default_scalar_types(value, skip_underscore=False):
    return value

J'ai également l'implémentation ci-dessus un argument skip_underscore Supplémentaire, pour ignorer explicitement les clés qui ont un caractère _ Au début. Cela aiderait à ignorer tous les attributs "cachés" supplémentaires que la bibliothèque Construct utilise.

Puisque Container est une sous-classe dict, le code ci-dessus gérera automatiquement les instances telles que packet.

12
Martijn Pieters

skipkeys ne fait pas ce que vous pensez qu'il fait - il demande au json.JSONEncoder d'ignorer les touches qui ne sont pas d'un type de base , pas les valeurs des clés - c'est-à-dire que si vous aviez un dict{object(): "foobar"} il sauterait le object(), tandis que sans skipkeys réglé sur True, cela augmenterait un TypeError.

Vous pouvez surcharger JSONEncoder.iterencode() (et son ventre) et y effectuer un filtrage prospectif, mais vous finirez par réécrire à peu près le module json, en le ralentissant dans le processus car vous ne pourrez pas bénéficier des parties compilées. Je vous suggère de pré-traiter vos données via un filtrage itératif et d'ignorer les clés/types que vous ne voulez pas dans votre JSON final. Le module json devrait alors pouvoir le traiter sans aucune instruction supplémentaire. Quelque chose comme:

import collections

class SkipFilter(object):

    def __init__(self, types=None, keys=None, allow_empty=False):
        self.types = Tuple(types or [])
        self.keys = set(keys or [])
        self.allow_empty = allow_empty  # if True include empty filtered structures

    def filter(self, data):
        if isinstance(data, collections.Mapping):
            result = {}  # dict-like, use dict as a base
            for k, v in data.items():
                if k in self.keys or isinstance(v, self.types):  # skip key/type
                    continue
                try:
                    result[k] = self.filter(v)
                except ValueError:
                    pass
            if result or self.allow_empty:
                return result
        Elif isinstance(data, collections.Sequence):
            result = []  # a sequence, use list as a base
            for v in data:
                if isinstance(v, self.types):  # skip type
                    continue
                try:
                    result.append(self.filter(v))
                except ValueError:
                    pass
            if result or self.allow_empty:
                return result
        else:  # we don't know how to traverse this structure...
            return data  # return it as-is, hope for the best...
        raise ValueError

Créez ensuite votre filtre:

import io

preprocessor = SkipFilter([io.BytesIO], ["_io"])  # double-whammy skip of io.BytesIO

Dans ce cas, sauter simplement par type devrait suffire, mais si la touche _io Contient d'autres données indésirables, cela garantit qu'elle ne sera pas dans le résultat final. Quoi qu'il en soit, vous pouvez simplement filtrer les données avant de les transmettre à JSONEncoder:

import json

json_data = json.dumps(preprocessor.filter(packet))  # no _io keys or io.BytesIO data...

Bien sûr, si votre structure contient d'autres données exotiques ou des données qui sont représentées différemment dans JSON en fonction de son type, cette approche peut gâcher car elle transforme tous les mappages en dict et toutes les séquences en list. Cependant, pour une utilisation générale, cela devrait être plus que suffisant.

2
zwer

Ignorer un champ non sérialisable nécessite une logique supplémentaire lourde, comme indiqué correctement dans toutes les réponses précédentes.

Si vous n'avez pas vraiment besoin d'exclure le champ, vous pouvez générer une valeur par défaut à la place:

def safe_serialize(obj):
  default = lambda o: f"<<non-serializable: {type(o).__qualname__}>>"
  return json.dumps(obj, default=default)

obj = {"a": 1, "b": bytes()} # bytes is non-serializable by default
print(safe_serialize(obj))

Cela produira ce résultat:

{"a": 1, "b": "<<non-serializable: bytes>>"}

Ce code affichera le nom du type, ce qui pourrait être utile si vous souhaitez implémenter vos sérialiseurs personnalisés ultérieurement.

1