Серіалізація Python з назвою tuple до json


85

Який рекомендований спосіб серіалізації a namedtupleдо json із збереженими іменами полів?

Серіалізація a namedtupleдо json призводить до того, що серіалізуються лише значення та імена полів втрачаються при перекладі. Я хотів би, щоб поля також зберігалися при json-ized і, отже, робили наступне:

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

Вищезазначене серіалізується до json, як я очікував, і поводиться так само, як і namedtupleв інших місцях, які я використовую (доступ до атрибутів тощо), за винятком результатів, не схожих на кортеж, під час ітерації (що добре для мого випадку використання).

Який "правильний спосіб" перетворення в json із збереженими іменами полів?


Відповіді:


56

Це досить складно, оскільки namedtuple()це фабрика, яка повертає новий тип, похідний від tuple. Одним із підходів було б, щоб ваш клас також успадковував UserDict.DictMixin, але tuple.__getitem__вже визначений і очікує ціле число, що позначає позицію елемента, а не ім'я його атрибута:

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

У своїй основі namedtuple дивно підходить для JSON, оскільки це справді спеціально створений тип, імена ключів якого закріплені як частина визначення типу , на відміну від словника, де імена ключів зберігаються всередині екземпляра. Це заважає вам "обертати" іменовану пару, наприклад, ви не можете декодувати словник назад у іменовану пару без будь-якої іншої інформації, як-от маркер типу конкретного додатка в дикті {'a': 1, '#_type': 'foobar'}, який трохи хакерський.

Це не ідеально, але якщо вам потрібно лише кодувати іменовані набори у словники, інший підхід полягає в розширенні або модифікації кодера JSON для особливих випадків цих типів. Ось приклад підкласу Python json.JSONEncoder. Це вирішує проблему забезпечення належного перетворення вкладених іменних пар у словники:

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
У своїй основі namedtuple дивно підходить для JSON, оскільки це справді спеціально створений тип, імена ключів якого закріплені як частина визначення типу, на відміну від словника, де імена ключів зберігаються всередині екземпляра. Дуже проникливий коментар. Я про це не думав. Дякую. Мені подобаються namedtuples, оскільки вони забезпечують приємну незмінну структуру зі зручністю іменування атрибутів. Я прийму вашу відповідь. Сказавши це, механізм серіалізації Java забезпечує більший контроль над тим, як об'єкт серіалізується, і мені цікаво знати, чому подібні хуки, здається, не існують у Python.
calvinkrishy

Це був мій перший підхід, але він насправді не працює (у мене все одно).
zeekay

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

19
Ах, у python 2.7+ _iterencode більше не є методом JSONEncoder.
zeekay

2
@calvin Дякую, я також знаходжу namedtuple корисним, хотів би бути кращим рішенням для його рекурсивного кодування в JSON. @zeekay Так, схоже, у версії 2.7+ вони це приховують, тому його більше неможливо замінити. Це невтішно.
samplebias

77

Якщо це лише один, який namedtupleви хочете серіалізувати, використання його _asdict()методу буде працювати (з Python> = 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
Я отримую AttributeError: об'єкт 'FB' не має атрибута ' dict ' під час запуску цього коду в Python 2.7 (x64) у Windows. Однак fb._asdict () відмінно працює.
geographika

5
fb._asdict()або vars(fb)було б краще.
jpmc26,

1
@ jpmc26: Ви не можете використовувати varsоб'єкт без __dict__.
Rufflewind

@Rufflewind Ви також не можете використовувати __dict__їх. =)
jpmc26

4
У python 3 __dict__видалено. _asdictздається, працює на обох.
Енді Хайден,

21

Схоже, раніше ви мали змогу здійснити підклас, simplejson.JSONEncoderщоб зробити цю роботу, але з останнім кодом simplejson це вже не так: вам потрібно фактично змінити код проекту. Я не бачу причини, чому simplejson не повинен підтримувати namedtuples, тому я роздвоїв проект, додав підтримку namedtuple, і зараз я чекаю, поки моя гілка буде повернута назад до основного проекту . Якщо вам потрібні виправлення зараз, просто витягніть з моєї вилки.

РЕДАГУВАТИ : Схоже, останні версії simplejsonтепер підтримують це за допомогою namedtuple_as_objectопції, яка за замовчуванням True.


3
Ваша редакція - правильна відповідь. simplejson серіалізує namedtuples по-різному (на мій погляд: краще), ніж json. Це дійсно робить шаблон: "try: import simplejson як json, за винятком: import json", ризикованим, оскільки на деяких машинах ви можете отримати різну поведінку залежно від того, чи встановлено simplejson. З цієї причини зараз я вимагаю simplejson у багатьох моїх файлах налаштування та утримуюся від цього шаблону.
marr75

1
@ marr75 - Так само ujson, що ще більш химерно і непередбачувано в таких крайніх випадках ...
mac

Мені вдалося отримати рекурсивний namedtuple, серіалізований до (досить надрукованого) json, використовуючи:simplejson.dumps(my_tuple, indent=4)
KFL

5

Я написав бібліотеку для цього: https://github.com/ltworf/typedload

Він може переходити від і до іменованого кортежу і назад.

Він підтримує досить складні вкладені структури зі списками, наборами, переліченнями, об'єднаннями, значеннями за замовчуванням. Він повинен охоплювати найпоширеніші випадки.

редагувати: Бібліотека також підтримує класи класів даних та attr.


2

Він рекурсивно перетворює названі дані Tuple в 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 Я зробив майже те саме. Але ваше повернення - це dict, а не json. Ви повинні мати "not", і якщо значення у вашому об'єкті є логічним значенням, воно не буде перетворено в true. Я думаю, що безпечніше перетворитися на dict, тоді використовуйте json.dumps, щоб перетворити на json.
Фред Лоран

2

Є більш зручне рішення - використовувати декоратор (він використовує захищене поле _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))

Не робіть цього, вони постійно змінюють внутрішній API. У моїй бібліотеці typedload є кілька випадків для різних версій py.
LtWorf

Так, це зрозуміло. Однак ніхто не повинен переходити на новішу версію Python без тестування. І, інші рішення використовують _asdict, який також є "захищеним" членом класу.
Дмитро Т.

1
LtWorf, ваша бібліотека є GPL і не працює з фрозенсетами
Thomas Grainger

2
@LtWorf Ваша бібліотека також використовує _fields;-) github.com/ltworf/typedload/blob/master/typedload/datadumper.py Це частина суспільного API namedtuple в, на самому ділі: docs.python.org/3.7/library / ... Люди плутаються підкреслення (не дивно!). Це поганий дизайн, але я не знаю, який ще вибір у них був.
quant_dev

1
Які речі? Коли? Чи можете ви цитувати примітки до випуску?
quant_dev

2

Бібліотека jsonplus забезпечує серіалізатор для примірників NamedTuple. Використовуйте режим сумісності для виведення простих об'єктів, якщо це необхідно, але віддайте перевагу типовому, оскільки це корисно для декодування назад.


Я подивився інші рішення тут і виявив, що просто додавання цієї залежності заощадило мені багато часу. Зокрема тому, що у мене був список NamedTuples, який мені потрібно було передати як json під час сеансу. jsonplus дозволяє в основному отримувати списки іменованих кортежів у json .dumps()і з нього, .loads()без конфігурації, він просто працює.
Роб

1

Неможливо правильно серіалізувати іменовані коси за допомогою власної бібліотеки python json. Він завжди бачитиме кортежі як списки, і неможливо замінити серіалізатор за замовчуванням, щоб змінити цю поведінку. Гірше, якщо об’єкти вкладені.

Краще використовувати більш надійну бібліотеку, як 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
я теж фанат orjson.
CircleOnCircles

0

Це давнє запитання. Однак:

Пропозиція для тих, хто має одне і те ж питання, добре подумайте про використання будь-якої приватної чи внутрішньої особливості NamedTuple тому що вони були раніше, і з часом вони знову зміняться.

Наприклад, якщо ваш NamedTupleоб'єкт є плоскою вартістю, і вас цікавить лише його серіалізація, а не випадки, коли він вкладений в інший об'єкт, ви можете уникнути проблем, які можуть виникнути при __dict__видаленні або _as_dict()зміні, і просто зробити щось на зразок (і так, це Python 3, оскільки ця відповідь на сьогодні):

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

Я намагався використати defaultвикликаний kwarg для dumpsтого, щоб здійснити to_dict()дзвінок, якщо він є, але це не було викликано, оскільки NamedTupleконвертоване в список.


3
_asdictє частиною загальнодоступного API namedtuple. Вони пояснюють причину підкреслення docs.python.org/3.7/library/… "На додаток до методів, успадкованих від кортежів, іменовані кортежі підтримують три додаткові методи та два атрибути. Щоб запобігти конфліктам з іменами полів, імена методів та атрибутів починайте з підкреслення ".
quant_dev

@quant_dev дякую, я не бачив цього пояснення. Це не гарантія стабільності API, але допомагає зробити ці методи більш надійними. Мені подобається явна читабельність to_dict, але я бачу, що це здається зміною _as_dict
dlamblin

0

Ось мій погляд на проблему. Він серіалізує NamedTuple, дбає про складені NamedTuples та списки всередині них

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.