A serializar a instância de classe para JSON
estou a tentar criar uma representação de cadeia JSON de uma instância de classe e a ter dificuldade. Digamos que a classe é assim:
class testclass:
value1 = "a"
value2 = "b"
Uma chamada para o json.as lixeiras são feitas assim:
t = testclass()
json.dumps(t)
Está a falhar e a dizer-me que a prova não é serializável pelo JSON.
TypeError: <__main__.testclass object at 0x000000000227A400> is not JSON serializable
também tentei usar o módulo de pickles:
t = testclass()
print(pickle.dumps(t, pickle.HIGHEST_PROTOCOL))
e dá informações de instância de classe, mas não um conteúdo serializado da instância de classe.
b'\x80\x03c__main__\ntestclass\nq\x00)\x81q\x01}q\x02b.'
O que foi? estou a fazer mal?
10 answers
O problema básico é que o codificador JSON json.dumps()
só sabe como serializar um conjunto limitado de tipos de objectos por omissão, todos os tipos incorporados. Lista aqui: https://docs.python.org/3.3/library/json.html#encoders-and-decoders
Uma boa solução seria fazer a sua classe herdar de JSONEncoder
e depois implementar a função JSONEncoder.default()
, e fazer com que essa função emitisse o JSON correcto para a sua classe.
Uma solução simples seria ligar json.dumps()
para o membro de .__dict__
aquele caso. Esse é um Python padrão dict
e se sua classe é simples, ele será serializável JSON.
class Foo(object):
def __init__(self):
self.x = 1
self.y = 2
foo = Foo()
s = json.dumps(foo) # raises TypeError with "is not JSON serializable"
s = json.dumps(foo.__dict__) # s set to: {"x":1, "y":2}
A abordagem acima é discutida neste Blog postando:
Serializar objectos em Python arbitrários para JSON usando _ _ dict__
Nota: eu editei esta resposta; a versão original só discutiu a abordagem de serialização .__dict__
.
json.dumps()
pode obter um parâmetro opcional por omissão onde poderá indicar uma função de serialização personalizada para tipos desconhecidos, a qual, no meu caso, se parece com
def serialize(obj):
"""JSON serializer for objects not serializable by default json code"""
if isinstance(obj, date):
serial = obj.isoformat()
return serial
if isinstance(obj, time):
serial = obj.isoformat()
return serial
return obj.__dict__
Os dois primeiros ifs são para a serialização da data e da hora.
e depois há um obj.__dict__
devolvido para qualquer outro objecto.
json.dumps(myObj, default=serialize)
É especialmente bom quando se está a serializar uma colecção e não se quer chamar __dict__
explicitamente para cada objecto. Aqui está feito para ti automaticamente.
Até agora, funcionou tão bem para mim, à espera dos teus pensamentos.
Eu apenas faço:
data=json.dumps(myobject.__dict__)
Esta não é a resposta completa, e se você tiver algum tipo de classe de objetos complicados você certamente não terá tudo. No entanto, uso isto para alguns dos meus objectos simples.
Um que funciona muito bem é a classe "opções" que você obtém a partir do módulo OptionParser. Aqui está junto com o próprio pedido JSON.
def executeJson(self, url, options):
data=json.dumps(options.__dict__)
if options.verbose:
print data
headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
return requests.post(url, data, headers=headers)
Pode indicar o parâmetro default
nomeado na função json.dumps()
:
json.dumps(obj, default=lambda x: x.__dict__)
Explicação:
Formem os documentos.(2.7, 3.6):
``default(obj)`` is a function that should return a serializable version
of obj or raise TypeError. The default simply raises TypeError.
(funciona em Python 2.7 e Python 3.x)
Nota: neste caso você precisa de instance
variáveis e não class
variáveis, como o exemplo na pergunta tenta fazer. (Estou assumindo que o asker quis dizer class instance
ser um objeto de uma classe)
Aprendi isto primeiro com a resposta de @phihag Aqui. Encontrei-o seja a maneira mais simples e limpa de fazer o trabalho.
Utilizar jsonpickle
import jsonpickle
object = YourClass()
json_object = jsonpickle.encode(object)
O JSON não se destina a serializar objectos arbitrários em Python. É ótimo para serializar objetos {[[0]}, mas o módulo pickle
é realmente o que você deveria estar usando em geral. A saída de pickle
não é realmente legível pelo homem, mas deve ser implicável perfeitamente. Se você insistir em usar o JSON, você pode verificar o Módulo jsonpickle
, que é uma abordagem híbrida interessante.
Uso isto para o tipo de configuração porque posso adicionar novos membros às aulas sem ajustes de código.
import json
class SimpleClass:
def __init__(self, a=None, b=None, c=None):
self.a = a
self.b = b
self.c = c
def serialize_json(instance=None, path=None):
dt = {}
dt.update(vars(instance))
with open(path, "w") as file:
json.dump(dt, file)
def deserialize_json(cls=None, path=None):
def read_json(_path):
with open(_path, "r") as file:
return json.load(file)
data = read_json(path)
instance = object.__new__(cls)
for key, value in data.items():
setattr(instance, key, value)
return instance
# Usage: Create class and serialize under Windows file system.
write_settings = SimpleClass(a=1, b=2, c=3)
serialize_json(write_settings, r"c:\temp\test.json")
# Read back and rehydrate.
read_settings = deserialize_json(SimpleClass, r"c:\temp\test.json")
# results are the same.
print(vars(write_settings))
print(vars(read_settings))
# output:
# {'c': 3, 'b': 2, 'a': 1}
# {'c': 3, 'b': 2, 'a': 1}
def jsonDefEncoder(obj):
if hasattr(obj, 'jsonEnc'):
return obj.jsonEnc()
else: #some default behavior
return obj.__dict__
E depois ter uma função jsonEnc()
em cada classe que você quer serializar. por exemplo
class A(object):
def __init__(self,lengthInFeet):
self.lengthInFeet=lengthInFeet
def jsonEnc(self):
return {'lengthInMeters': lengthInFeet * 0.3 } # each foot is 0.3 meter
Então liga-me.json.dumps(classInstance,default=jsonDefEncoder)
Python3.x
A melhor abordagem que consegui alcançar com o meu conhecimento foi esta.Note que este código tratar conjunto() também.
Esta abordagem é genérica apenas precisando da extensão da classe (no segundo exemplo).
Note que eu estou apenas fazendo isso para arquivos, mas é fácil de modificar o comportamento ao seu gosto. No entanto, isto é um CoDec. Com um pouco mais de trabalho você pode construir sua classe de outras maneiras. Eu assumo que um construtor padrão para a instância, então eu atualizo o dict da classe.
import json
import collections
class JsonClassSerializable(json.JSONEncoder):
REGISTERED_CLASS = {}
def register(ctype):
JsonClassSerializable.REGISTERED_CLASS[ctype.__name__] = ctype
def default(self, obj):
if isinstance(obj, collections.Set):
return dict(_set_object=list(obj))
if isinstance(obj, JsonClassSerializable):
jclass = {}
jclass["name"] = type(obj).__name__
jclass["dict"] = obj.__dict__
return dict(_class_object=jclass)
else:
return json.JSONEncoder.default(self, obj)
def json_to_class(self, dct):
if '_set_object' in dct:
return set(dct['_set_object'])
elif '_class_object' in dct:
cclass = dct['_class_object']
cclass_name = cclass["name"]
if cclass_name not in self.REGISTERED_CLASS:
raise RuntimeError(
"Class {} not registered in JSON Parser"
.format(cclass["name"])
)
instance = self.REGISTERED_CLASS[cclass_name]()
instance.__dict__ = cclass["dict"]
return instance
return dct
def encode_(self, file):
with open(file, 'w') as outfile:
json.dump(
self.__dict__, outfile,
cls=JsonClassSerializable,
indent=4,
sort_keys=True
)
def decode_(self, file):
try:
with open(file, 'r') as infile:
self.__dict__ = json.load(
infile,
object_hook=self.json_to_class
)
except FileNotFoundError:
print("Persistence load failed "
"'{}' do not exists".format(file)
)
class C(JsonClassSerializable):
def __init__(self):
self.mill = "s"
JsonClassSerializable.register(C)
class B(JsonClassSerializable):
def __init__(self):
self.a = 1230
self.c = C()
JsonClassSerializable.register(B)
class A(JsonClassSerializable):
def __init__(self):
self.a = 1
self.b = {1, 2}
self.c = B()
JsonClassSerializable.register(A)
A().encode_("test")
b = A()
b.decode_("test")
print(b.a)
print(b.b)
print(b.c.a)
Editar
Com um pouco mais de pesquisa, encontrei uma forma de generalizar sem a necessidade da chamada de método de Registo superclass, usando um metaclass
import json
import collections
REGISTERED_CLASS = {}
class MetaSerializable(type):
def __call__(cls, *args, **kwargs):
if cls.__name__ not in REGISTERED_CLASS:
REGISTERED_CLASS[cls.__name__] = cls
return super(MetaSerializable, cls).__call__(*args, **kwargs)
class JsonClassSerializable(json.JSONEncoder, metaclass=MetaSerializable):
def default(self, obj):
if isinstance(obj, collections.Set):
return dict(_set_object=list(obj))
if isinstance(obj, JsonClassSerializable):
jclass = {}
jclass["name"] = type(obj).__name__
jclass["dict"] = obj.__dict__
return dict(_class_object=jclass)
else:
return json.JSONEncoder.default(self, obj)
def json_to_class(self, dct):
if '_set_object' in dct:
return set(dct['_set_object'])
elif '_class_object' in dct:
cclass = dct['_class_object']
cclass_name = cclass["name"]
if cclass_name not in REGISTERED_CLASS:
raise RuntimeError(
"Class {} not registered in JSON Parser"
.format(cclass["name"])
)
instance = REGISTERED_CLASS[cclass_name]()
instance.__dict__ = cclass["dict"]
return instance
return dct
def encode_(self, file):
with open(file, 'w') as outfile:
json.dump(
self.__dict__, outfile,
cls=JsonClassSerializable,
indent=4,
sort_keys=True
)
def decode_(self, file):
try:
with open(file, 'r') as infile:
self.__dict__ = json.load(
infile,
object_hook=self.json_to_class
)
except FileNotFoundError:
print("Persistence load failed "
"'{}' do not exists".format(file)
)
class C(JsonClassSerializable):
def __init__(self):
self.mill = "s"
class B(JsonClassSerializable):
def __init__(self):
self.a = 1230
self.c = C()
class A(JsonClassSerializable):
def __init__(self):
self.a = 1
self.b = {1, 2}
self.c = B()
A().encode_("test")
b = A()
b.decode_("test")
print(b.a)
# 1
print(b.b)
# {1, 2}
print(b.c.a)
# 1230
print(b.c.c.mill)
# s
- E se a instância estiver aninhada dentro de uma grande estrutura de dados?
E se também quiser o nome da classe?
E se quiseres desertar a instância?
E se estiver a usar
__slots__
em vez de __dict__
?
E se não o quiseres fazer sozinho?
class MyTestCls:
def __init__(self, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)
cls_instance = MyTestCls(s='ub', dct={'7': 7})
json = dumps(cls_instance, indent=4)
instance = loads(json)
Vais recuperar a tua instância. Aqui o json parece assim:
{
"__instance_type__": [
"json_tricks.test_class",
"MyTestCls"
],
"attributes": {
"s": "ub",
"dct": {
"7": 7
}
}
}
Se você gosta de fazer a sua própria solução, você pode olhar para a fonte de json-tricks
de modo a não esquecer alguns casos especiais (como __slots__
).
Ele também faz outros tipos como matrizes numpy, datetimes, números complexos; ele também permite comentários.