Serializando um Python nomeado duplamente para json

87

Qual é a maneira recomendada de serializar um namedtuplepara json com os nomes de campo retidos?

Serializar a namedtuplepara json resulta na serialização apenas dos valores e na perda dos nomes dos campos na tradução. Eu gostaria que os campos também fossem mantidos quando jsonizados e, portanto, fiz o seguinte:

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

O código acima é serializado para json conforme eu espero e se comporta como namedtupleem outros lugares que eu uso (acesso de atributo, etc.), exceto com resultados não-tupla durante a iteração (o que é bom para meu caso de uso).

Qual é a "maneira correta" de converter para JSON com os nomes dos campos retidos?

calvinkrishy
fonte
para python 2.7: stackoverflow.com/questions/16938456/…
lowtech

Respostas:

55

Isso é bastante complicado, pois namedtuple()é uma fábrica que retorna um novo tipo derivado de tuple. Uma abordagem seria fazer com que sua classe também herde de UserDict.DictMixin, mas tuple.__getitem__já está definida e espera um número inteiro denotando a posição do elemento, não o nome de seu atributo:

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

Em sua essência, o namedtuple é um ajuste estranho para JSON, pois é realmente um tipo customizado cujos nomes de chave são fixados como parte da definição de tipo , ao contrário de um dicionário onde os nomes de chave são armazenados dentro da instância. Isso evita que você faça um "round-trip" de um namedtuple, por exemplo, você não pode decodificar um dicionário de volta para um namedtuple sem alguma outra informação, como um marcador de tipo específico do aplicativo no dicionário {'a': 1, '#_type': 'foobar'}, que é um pouco hackeado.

Isso não é o ideal, mas se você só precisa codificar namedtuples em dicionários, outra abordagem é estender ou modificar seu codificador JSON para casos especiais desses tipos. Aqui está um exemplo de subclasse de Python json.JSONEncoder. Isso resolve o problema de garantir que as duplicatas nomeadas aninhadas sejam convertidas corretamente em dicionários:

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
fonte
12
Em sua essência, o namedtuple é um ajuste estranho para JSON, uma vez que é realmente um tipo customizado cujos nomes de chave são fixados como parte da definição de tipo, ao contrário de um dicionário onde os nomes de chave são armazenados dentro da instância. Comentário muito perspicaz. Eu não tinha pensado nisso. Obrigado. Eu gosto de namedtuples, pois eles fornecem uma estrutura imutável com conveniência de nomenclatura de atributos. Vou aceitar sua resposta. Dito isso, o mecanismo de serialização do Java fornece mais controle sobre como o objeto é serializado e estou curioso para saber por que esses ganchos parecem não existir no Python.
calvinkrishy
Essa foi a minha primeira abordagem, mas na verdade não funciona (para mim de qualquer maneira).
zeekay
1
>>> json.dumps(foobar('x', 'y'), cls=MyEncoder) <<< '["x", "y"]'
zeekay
19
Ah, em python 2.7+ _iterencode não é mais um método de JSONEncoder.
zeekay
2
@calvin Obrigado, acho o namedtuple útil também, gostaria que houvesse uma solução melhor para codificá-lo recursivamente para JSON. @zeekay Sim, parece que no 2.7+ eles o escondem, então não pode mais ser substituído. Isso é decepcionante.
samplebias
77

Se for apenas um que namedtuplevocê deseja serializar, usar seu _asdict()método funcionará (com 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
fonte
4
Estou recebendo AttributeError: o objeto 'FB' não tem atributo ' dict ' ao executar esse código em Python 2.7 (x64) no Windows. No entanto, fb._asdict () funciona bem.
geographika
5
fb._asdict()ou vars(fb)seria melhor.
jpmc26,
1
@ jpmc26: Você não pode usar varsem um objeto sem um __dict__.
Rufflewind
@Rufflewind Você também não pode usar neles __dict__. =)
jpmc26
4
Em python 3 __dict__foi removido. _asdictparece funcionar em ambos.
Andy Hayden
21

Parece que você costumava ser capaz de criar uma subclasse simplejson.JSONEncoderpara fazer isso funcionar, mas com o código simplejson mais recente, esse não é mais o caso: você realmente tem que modificar o código do projeto. Não vejo razão para que o simplejson não deva oferecer suporte a namedtuples, então eu fiz um fork do projeto, adicionei o suporte namedtuple e estou atualmente esperando que meu branch seja puxado de volta para o projeto principal . Se você precisar de correções agora, basta puxar do meu garfo.

EDITAR : Parece que as versões mais recentes do simplejsonagora suportam nativamente isso com a namedtuple_as_objectopção, cujo padrão é True.

cantando menino lobo
fonte
3
Sua edição é a resposta correta. simplejson serializa namedtuples de maneira diferente (minha opinião: melhor) do que json. Isso realmente torna o padrão: "try: import simplejson as json except: import json", arriscado, pois você pode obter comportamentos diferentes em algumas máquinas, dependendo se o simplejson está instalado. Por esse motivo, agora eu exijo o simplejson em muitos dos meus arquivos de configuração e me abstenho desse padrão.
março de 75,
1
@ marr75 - Idem ujson, que é ainda mais bizarro e imprevisível em casos extremos ...
mac
Consegui obter um namedtuple recursivo serializado para (bem impresso) json usando:simplejson.dumps(my_tuple, indent=4)
KFL
5

Eu escrevi uma biblioteca para fazer isso: https://github.com/ltworf/typedload

Ele pode ir de e para tupla nomeada e voltar.

Ele suporta estruturas aninhadas bastante complicadas, com listas, conjuntos, enums, uniões, valores padrão. Deve abranger os casos mais comuns.

editar: A biblioteca também oferece suporte a classes de dados e classes de atributos.

LtWorf
fonte
2

Ele converte recursivamente os dados namedTuple em 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
fonte
1
1 eu fiz quase o mesmo. Mas seu retorno é um ditado, não um json. Você deve ter "não" e se um valor em seu objeto for um booleano, ele não será convertido em verdadeiro. Acho que é mais seguro transformar em dict, então use json.dumps para converter em json.
Fred Laurent
2

Existe uma solução mais conveniente é usar o decorador (ele usa o campo protegido _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.
fonte
Não faça isso, eles mudam a API interna o tempo todo. Minha biblioteca typedload tem vários casos para diferentes versões de py.
LtWorf
Sim, está claro. No entanto, ninguém deve migrar para uma versão mais recente do Python sem testar. E as outras soluções usam _asdict, que também é um membro da classe "protegido".
Dmitry T.
1
LtWorf, sua biblioteca é GPL e não funciona com frozensets
Thomas Grainger
2
@LtWorf Sua biblioteca também usa _fields;-) github.com/ltworf/typedload/blob/master/typedload/datadumper.py Faz parte da API pública do namedtuple, na verdade: docs.python.org/3.7/library/… As pessoas ficam confusas com o sublinhado (não é de admirar!). É um projeto ruim, mas não sei que outra escolha eles tinham.
quant_dev
1
Que coisas? Quando? Você pode citar notas de lançamento?
quant_dev
2

A biblioteca jsonplus fornece um serializador para instâncias NamedTuple. Use seu modo de compatibilidade para gerar objetos simples, se necessário, mas prefira o padrão, pois é útil para a decodificação de volta.

Gonzalo
fonte
Eu olhei para as outras soluções aqui e descobri que simplesmente adicionar essa dependência me economizou muito tempo. Principalmente porque eu tinha uma lista de NamedTuples que precisava passar como json na sessão. jsonplus permite basicamente obter listas de tuplas nomeadas dentro e fora do json com .dumps()e .loads()sem configuração. Ele simplesmente funciona.
Rob de
1

É impossível serializar namedtuples corretamente com a biblioteca python json nativa. Ele sempre verá as tuplas como listas, e é impossível substituir o serializador padrão para alterar esse comportamento. É pior se os objetos estiverem aninhados.

Melhor usar uma biblioteca mais robusta como 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
fonte
1
eu também sou fã orjson.
CircleOnCircles de
0

Esta é uma velha questão. Contudo:

Uma sugestão para todos aqueles com a mesma pergunta, pense cuidadosamente sobre como usar qualquer um dos recursos privados ou internos do NamedTupleporque eles já o fizeram e irão mudar novamente com o tempo.

Por exemplo, se seu NamedTupleé um objeto de valor plano e você está interessado apenas em serializá-lo e não nos casos em que está aninhado em outro objeto, você pode evitar os problemas que surgiriam com __dict__a remoção ou _as_dict()alteração e apenas fazer algo como (e sim, este é Python 3 porque esta resposta é para o presente):

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

Tentei usar o defaultcallable kwarg to dumpspara fazer a to_dict()chamada, se disponível, mas não foi chamado porque NamedTupleé conversível para uma lista.

dlamblin
fonte
3
_asdictfaz parte da API pública namedtuple. Eles explicam o motivo do sublinhado docs.python.org/3.7/library/… "Além dos métodos herdados das tuplas, as tuplas nomeadas suportam três métodos adicionais e dois atributos. Para evitar conflitos com nomes de campo, o método e os nomes de atributo comece com um sublinhado. "
quant_dev
@quant_dev obrigado, não vi essa explicação. Não é uma garantia de estabilidade da API, mas ajuda a tornar esses métodos mais confiáveis. Eu gosto da legibilidade de to_dict explícita, mas posso ver que parece uma reimplementação de _as_dict
dlamblin
0

Aqui está minha opinião sobre o problema. Ele serializa a NamedTuple, cuida das NamedTuples e Listas dobradas dentro delas

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
Dim
fonte
0

simplejson.dump()em vez de json.dumpfazer o trabalho. Pode ser mais lento.

Smit Johnth
fonte