Сериализация Python namedtuple в json

87

Каков рекомендуемый способ сериализации a namedtupleв json с сохранением имен полей?

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

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

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

Каков «правильный способ» преобразования в json с сохранением имен полей?

Calvinkrishy
источник
для python 2.7: stackoverflow.com/questions/16938456/…
lowtech

Ответы:

55

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

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

По сути, namedtuple не подходит для JSON, поскольку на самом деле это настраиваемый тип, имена ключей которого фиксируются как часть определения типа , в отличие от словаря, в котором имена ключей хранятся внутри экземпляра. Это предохраняет вас от «кругового обхода» именованного кортежа, например, вы не можете декодировать словарь обратно в именованный кортеж без какой-либо другой части информации, такой как маркер типа для конкретного приложения в dict {'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, поскольку на самом деле это настраиваемый тип, имена ключей которого фиксируются как часть определения типа, в отличие от словаря, в котором имена ключей хранятся внутри экземпляра. Очень проницательный комментарий. Я не думал об этом. Благодарю. Мне нравятся именованные кортежи, поскольку они предоставляют красивую неизменяемую структуру с удобством именования атрибутов. Я приму твой ответ. Сказав , что механизм сериализации 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}'
Benselme
источник
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 сериализует именованные кортежи иначе (мое мнение: лучше), чем json. Это действительно делает шаблон: «попробуйте: импортировать simplejson как json except: import json» рискованным, поскольку на некоторых машинах поведение может отличаться в зависимости от того, установлен ли simplejson. По этой причине я теперь требую simplejson во многих моих установочных файлах и воздерживаюсь от этого шаблона.
marr75 05
1
@ marr75 - То же самое для ujson, что еще более странно и непредсказуемо в таких крайних случаях ...
Mac
Мне удалось получить рекурсивный namedtuple, сериализованный в (красиво напечатанный) json, используя:simplejson.dumps(my_tuple, indent=4)
KFL
5

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

Он может переходить от и к именованному кортежу и обратно.

Он поддерживает довольно сложные вложенные структуры со списками, наборами, перечислениями, объединениями и значениями по умолчанию. Он должен охватывать наиболее распространенные случаи.

edit: библиотека также поддерживает классы dataclass и attr.

LtWorf
источник
2

Он рекурсивно преобразует данные namedTuple в 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'}
Толгахан ÜZÜN
источник
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 и не работает с frozensets
Томас Грейнджер,
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

Невозможно правильно сериализовать именованные кортежи с помощью собственной библиотеки json python. Он всегда будет видеть кортежи в виде списков, и невозможно изменить сериализатор по умолчанию, чтобы изменить это поведение. Хуже, если объекты вложены друг в друга.

Лучше использовать более надежную библиотеку, например 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
источник
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можно преобразовать в список.

dlamblin
источник
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
Тусклый
источник
0

simplejson.dump()вместо того json.dump, чтобы делать работу. Хотя это может быть медленнее.

Смит Джонт
источник