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?

calvinkrishy
źródło

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"}}
samplebias
źródło
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}'
benselme
źródło
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.

singingwolfboy
źródło
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.

LtWorf
źródło
2

Rekurencyjnie konwertuje dane namedTuple na json.

print(m1)
## Message(id=2, agent=Agent(id=1, first_name='asd', last_name='asd', mail='[email protected]'), customer=Customer(id=1, first_name='asd', last_name='asd', mail='[email protected]', 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': '[email protected]',
'id': 1},
'content': 'text',
'customer': {'first_name': 'asd',
'last_name': 'asd',
'mail': '[email protected]',
'phone_number': 123123,
'id': 1},
'id': 2,
'la': 123123,
'ls': 4512313,
'media_url': 'h.com',
'type': 'image'}
Tolgahan ÜZÜN
źródło
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))
Dmitry T.
źródło
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.

Gonzalo
źródło
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
}
mikebridge
źródło
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ę.

dlamblin
źródło
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
Ciemny
źródło
0

simplejson.dump()zamiast wykonywać json.dumpswoją pracę. Może jednak działać wolniej.

Smit Johnth
źródło