Serializowanie Pythona namedtuple do json


85

Jaki jest zalecany sposób serializacji a namedtupledo json z zachowanymi nazwami pól?

Serializacja a namedtupledo json powoduje, że tylko wartości są serializowane, a nazwy pól są tracone podczas tłumaczenia. Chciałbym, aby pola również zostały zachowane po poddaniu ich jsonizowaniu, dlatego wykonałem następujące czynności:

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

Powyższe serializuje się do json zgodnie z oczekiwaniami i zachowuje się tak, jak namedtuplew innych miejscach, których używam (dostęp do atrybutów itp.), Z wyjątkiem wyników innych niż krotka podczas iteracji (co jest w porządku w moim przypadku użycia).

Jaki jest „prawidłowy sposób” konwertowania na json przy zachowaniu nazw pól?


Odpowiedzi:


56

Jest to dość trudne, ponieważ namedtuple()jest to fabryka, która zwraca nowy typ pochodzący z tuple. Jedną z metod byłoby ustawienie klasy dziedziczącej także po UserDict.DictMixin, ale tuple.__getitem__jest już zdefiniowana i oczekuje liczby całkowitej oznaczającej pozycję elementu, a nie nazwy jego atrybutu:

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

W istocie namedtuple nie pasuje do JSON, ponieważ jest to w rzeczywistości niestandardowy typ, którego nazwy kluczy są ustalone jako część definicji typu , w przeciwieństwie do słownika, w którym nazwy kluczy są przechowywane wewnątrz instancji. Zapobiega to „przełączaniu w obie strony” nazwanego tulei, np. Nie można dekodować słownika z powrotem do nazwanego tulei bez innej informacji, takiej jak znacznik typu specyficznego dla aplikacji w dyktcie {'a': 1, '#_type': 'foobar'}, co jest nieco zepsute.

Nie jest to idealne rozwiązanie, ale jeśli potrzebujesz tylko zakodować namedtuples w słownikach, innym podejściem jest rozszerzenie lub zmodyfikowanie kodera JSON do specjalnych przypadków tych typów. Oto przykład podklasy Pythona json.JSONEncoder. To rozwiązuje problem zapewnienia, że ​​zagnieżdżone krotki nazw są prawidłowo konwertowane na słowniki:

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
W istocie namedtuple nie pasuje do JSON, ponieważ jest to w rzeczywistości niestandardowy typ, którego nazwy kluczy są ustalone jako część definicji typu, w przeciwieństwie do słownika, w którym nazwy kluczy są przechowywane wewnątrz instancji. Bardzo wnikliwy komentarz. Nie myślałem o tym. Dzięki. Lubię namedtuples, ponieważ zapewniają ładną, niezmienną strukturę z wygodą nazywania atrybutów. Przyjmuję twoją odpowiedź. Mimo to mechanizm serializacji Javy zapewnia większą kontrolę nad sposobem serializacji obiektu i jestem ciekawy, dlaczego takie punkty zaczepienia nie istnieją w Pythonie.
— calvinkrishy

To było moje pierwsze podejście, ale tak naprawdę nie działa (w każdym razie dla mnie).
— zeekay

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

19
Ach, w pythonie 2.7+ _iterencode nie jest już metodą JSONEncodera.
— zeekay

2
@calvin Dzięki, uważam, że namedtuple również jest użyteczne, żałuję, że nie ma lepszego rozwiązania do kodowania go rekurencyjnie do JSON. @zeekay Tak, wygląda na to, że w wersji 2.7+ ukrywają to, więc nie można ich już zastąpić. To jest rozczarowujące.
— samplebias

77

Jeśli to tylko jeden, namedtuplektóry chcesz serializować, użycie jego _asdict()metody będzie działać (z Pythonem> = 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
Otrzymuję AttributeError: obiekt „FB” nie ma atrybutu „ dict ” podczas uruchamiania tego kodu w Pythonie 2.7 (x64) w systemie Windows. Jednak fb._asdict () działa dobrze.
— geographika

5
fb._asdict()lub vars(fb)byłoby lepiej.
— jpmc26

1
@ jpmc26: Nie możesz użyć varsna obiekcie bez __dict__.
— Rufflewind

@Rufflewind Nie możesz też __dict__na nich używać . =)
— jpmc26

4
W Pythonie 3 __dict__został usunięty. _asdictwydaje się działać na obu.
— Andy Hayden

21

Wygląda na to, że kiedyś można było tworzyć podklasy, simplejson.JSONEncoderaby to zadziałało, ale z najnowszym kodem simplejson już tak nie jest: musisz faktycznie zmodyfikować kod projektu. Nie widzę powodu, dla którego simplejson nie miałby obsługiwać namedtuples, więc rozwidliłem projekt, dodałem obsługę namedtuple i obecnie czekam, aż moja gałąź zostanie wciągnięta z powrotem do głównego projektu . Jeśli potrzebujesz teraz poprawek, po prostu wyciągnij z mojego widelca.

EDYCJA : Wygląda na to, że najnowsze wersje simplejsonteraz natywnie obsługują to z namedtuple_as_objectopcją, która domyślnie True.


3
Twoja zmiana jest poprawną odpowiedzią. simplejson serializuje namedtuples inaczej (moim zdaniem: lepiej) niż json. To naprawdę sprawia, że ​​wzorzec: "try: import simplejson as json oprócz: import json" jest ryzykowny, ponieważ możesz uzyskać różne zachowanie na niektórych maszynach w zależności od tego, czy zainstalowany jest simplejson. Z tego powodu potrzebuję teraz simplejson w wielu moich plikach konfiguracyjnych i powstrzymuję się od tego wzorca.
— marr75

1
@ marr75 - To samo dotyczy ujson, co jest jeszcze bardziej dziwaczne i nieprzewidywalne w takich skrajnych przypadkach ...
— mac

Udało mi się uzyskać cykliczną nazwę namedtuple zserializowaną do (ładnie wydrukowanego) json za pomocą:simplejson.dumps(my_tuple, indent=4)
— KFL

5

Napisałem w tym celu bibliotekę: https://github.com/ltworf/typedload

Może przechodzić zi do nazwanej krotki iz powrotem.

Obsługuje dość skomplikowane struktury zagnieżdżone, z listami, zestawami, wyliczeniami, sumami, wartościami domyślnymi. Powinien obejmować większość typowych przypadków.

edycja: biblioteka obsługuje również klasy dataclass i attr.


2

Rekurencyjnie konwertuje dane namedTuple na 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 zrobiłem prawie to samo. Ale twój powrót to dykt, a nie json. Musisz mieć „not”, a jeśli wartość w twoim obiekcie jest wartością logiczną, nie zostanie przekonwertowana na true. Myślę, że bezpieczniej jest przekształcić w dict, a następnie użyć json.dumps, aby przekonwertować na json.
— Fred Laurent

2

Bardziej wygodnym rozwiązaniem jest użycie dekoratora (korzysta z chronionego pola _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))

Nie rób tego, cały czas zmieniają wewnętrzne API. Moja biblioteka typedload ma kilka przypadków dla różnych wersji py.
— LtWorf

Tak, to jasne. Jednak nikt nie powinien migrować do nowszej wersji Pythona bez testowania. A inne rozwiązania używają _asdict, który jest również „chronionym” składnikiem klasy.
— Dmitry T.

1
LtWorf, twoja biblioteka jest na licencji GPL i nie działa z Frozensetami
— Thomas Grainger,

2
@LtWorf Twoja biblioteka również używa _fields;-) github.com/ltworf/typedload/blob/master/typedload/datadumper.py Jest to część publicznego interfejsu API namedtuple , właściwie: docs.python.org/3.7/library/ ... Ludzie są zdezorientowani przez podkreślenie (nic dziwnego!). To zły projekt, ale nie wiem, jaki mieli inny wybór.
— quant_dev

1
Jakie rzeczy? Gdy? Czy możesz cytować informacje o wersji?
— quant_dev

2

Jsonplus biblioteka zapewnia serializatora dla instancji NamedTuple. Użyj jego trybu zgodności, aby w razie potrzeby wyprowadzić proste obiekty, ale preferuj domyślny, ponieważ jest pomocny przy dekodowaniu z powrotem.


Przyjrzałem się innym rozwiązaniom tutaj i stwierdziłem, że samo dodanie tej zależności zaoszczędziło mi dużo czasu. Szczególnie dlatego, że miałem listę NamedTuples, które musiałem przekazać jako json w sesji. jsonplus pozwala w zasadzie dostać list nazwanych krotek do iz json z .dumps()i .loads()bez config po prostu działa.
— Rob

1

Niemożliwe jest poprawne serializowanie namedtuples z natywną biblioteką json języka Python. Zawsze będzie widzieć krotki jako listy i nie można zastąpić domyślnego serializatora, aby zmienić to zachowanie. Gorzej, jeśli obiekty są zagnieżdżone.

Lepiej użyć bardziej niezawodnej biblioteki, takiej jak 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
też jestem ich fanem orjson.
— CircleOnCircles

0

To jest stare pytanie. Jednak:

Sugestia dla wszystkich, którzy mają to samo pytanie, uważnie zastanówcie się nad wykorzystaniem jakichkolwiek prywatnych lub wewnętrznych cech tego, NamedTupleco mieli wcześniej i będą się zmieniać ponownie z czasem.

Na przykład, jeśli twój NamedTuplejest obiektem o płaskiej wartości i jesteś zainteresowany tylko jego serializacją, a nie w przypadkach, gdy jest on zagnieżdżony w innym obiekcie, możesz uniknąć problemów, które pojawią się przy __dict__usuwaniu lub _as_dict()zmianie i po prostu zrobić coś takiego (i tak, to jest Python 3, ponieważ ta odpowiedź dotyczy teraźniejszości):

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

Próbowałem użyć defaultwywoływalnego kwarg to dumps, aby wykonać to_dict()wywołanie, jeśli jest dostępne, ale nie zostało to wywołane, ponieważ NamedTuplemożna je zamienić na listę.


3
_asdictjest częścią publicznego interfejsu API namedtuple. Wyjaśniają powód podkreślenia docs.python.org/3.7/library/ ... „Oprócz metod dziedziczonych z krotek, nazwane krotki obsługują trzy dodatkowe metody i dwa atrybuty. Aby zapobiec konfliktom z nazwami pól, nazwy metod i atrybutów zacznij od podkreślenia ”.
— quant_dev

@quant_dev dzięki, nie widziałem tego wyjaśnienia. Nie jest to gwarancja stabilności API, ale pomaga zwiększyć wiarygodność tych metod. Lubię wyraźnej to_dict czytelności, ale widzę, wydaje się reimplementing _as_dict
— dlamblin

0

Oto moje podejście do problemu. Serializuje NamedTuple, dba o zawinięte NamedTuples i listy wewnątrz nich

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()zamiast wykonywać json.dumpswoją pracę. Może jednak działać wolniej.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.