Reputation: 647
Like the title says, I'm trying to write a custom decoder for an object whose class I've defined which contains other objects whose class I've defined. The "outer" class is an Edge, defined like so:
class Edge:
def __init__(self, actor, movie):
self.actor = actor
self.movie = movie
def __eq__(self, other):
if (self.movie == other.movie) & (self.actor == other.actor):
return True
else:
return False
def __str__(self):
print("Actor: ", self.actor, " Movie: ", self.movie)
def get_actor(self):
return self.actor
def get_movie(self):
return self.movie
with the "inner" classes actor and movies are defined like so:
class Movie:
def __init__(self, title, gross, soup, year):
self.title = title
self.gross = gross
self.soup = soup
self.year = year
def __eq__(self, other):
if self.title == other.title:
return True
else:
return False
def __repr__(self):
return self.title
def __str__(self):
return self.title
def get_gross(self):
return self.gross
def get_soup(self):
return self.soup
def get_title(self):
return self.title
def get_year(self):
return self.year
class Actor:
def __init__(self, name, age, soup):
self.name = name
self.age = age
self.soup = soup
def __eq__(self, other):
if self.name == other.name:
return True
else:
return False
def __repr__(self):
return self.name
def __str__(self):
return self.name
def get_age(self):
return self.age
def get_name(self):
return self.name
def get_soup(self):
return self.soup
(soup is just a beautifulsoup object for that movie/actor's Wikipedia page, it can be ignored). I've written a custom encoder for the edge class as well:
class EdgeEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, Edge):
return {
"Actor": {
"Name": o.get_actor().get_name(),
"Age": o.get_actor().get_age()
},
"Movie": {
"Title": o.get_movie().get_title(),
"Gross": o.get_movie().get_gross(),
"Year": o.get_movie().get_year()
}
}
return json.JSONEncoder.default(self, o)
which I've tested, and it properly serializes a list of edges into a JSON file. Now my problem comes when trying to write an edge decoder. I've used the GitHub page here as a reference, but my encoder deviates from him and I'm wondering if it's necessary to change it. Do I need to explicitly encode an object's type as its own key-value pair within its JSON serialization the way he does, or is there some way to grab the "Actor" and "Movie" keys with the serialization of the edge? Similarly, is there a way to grab "Name"? "Age", etc, so that I can reconstruct the Actor/Movie object, and then use those to reconstruct the edge? Is there a better way to go about encoding my objects instead? I've also tried following this tutorial, but I found the use of object dicts confusing for their encoder, and I wasn't sure how to extend that method to a custom object which contains custom objects.
Upvotes: 37
Views: 30674
Reputation: 109
MY ALTERNATIVE CONTRIBUTION: Without "Encoders" or "Decoders"
Use the "_base.Common" class in your object class and inherit methods to manipulate JSON. You do not need to explicitly set the name of the attributes.
Note: All parent and child objects must extend the "_base.Common" class.
Code:
_base.py
import json
class Common:
def toJson(self):
return json.dumps(self.toDict())
def toDict(self): # Fix converter to 'dict' on base of structure of custom class (extended of Common class)
null = "" # or None
d = dict()
for k, v in self.__dict__.items():
if hasattr(v, "__dict__") and issubclass(self.__getattribute__(k).__class__, Common):
d[k] = v.toDict() #v.__dict__
else:
d[k] = null if v is None else (v.__str__() if isinstance(v, (int, str, bool)) else (v if isinstance(v, (list, dict)) else str(v)))
return d
def fromDict(self, odict: dict):
self.__dict__ = odict
return self
def fromJson(self, ojson: str):
if ojson is None : return self
for k, v in json.loads(ojson).items():
if isinstance(v, dict):
self.__setattr__(k, self.__getattribute__(k).fromDict(v)) # object and sub-objects extents Common class for use method ".fromDict(...)"
else:
self.__setattr__(k, v)
return self
def isEmpty(self):
for k , v in self.__dict__.items():
if v is not None:
if isinstance(v, str) and not __isNullable(v):
return False
elif isinstance(v, list) and len(v) > 0:
return False
elif isinstance(v, dict) and __isNullable(dict(v).__repr__()):
return False
return True
def __isNullable(ostr : str): # Private method
NULL_VALUES = (None, "", "null", "none", "nan", "[]") # Customizable
try:
if ostr is None: return True
return NULL_VALUES.__contains__(ostr.lower() if ostr is not None else ostr)
except:
print("Class object : %s" % ostr.__class__)
raise "Type object can't evaluate!!"
custom_objects.py
from _base import Common
class ParentObject(Common):
def __init__(self):
self.attr_one = None
self.attr_two = None
self.child = ChildObject()
self.colors : lis[ColorObject] = []
class ChildObject(Common):
def __init__(self):
self.attr_one_ch = None
self.attr_two_ch = None
class ColorObject(Common):
def __init__(self):
self.name = None
self.desc = None
Now you can do:
main.py
from custom_objects import ParentObject, ChildObject
# String OBJECT JSON
sjson = '{ "attr_one" : "some_one", "attr_two" : "some_two", "child" : { "attr_one_ch" : "child_some_one", "attr_two_ch" : "child_some_two" }, "colors" : [{"name":"red", "desc" : "is red"},{"name":"blue", "desc" : "is blue"}] }'
pobject = ParentObject().fromJson(sjson)
#
print("\n== OUTPUT ==\n")
#
print(pobject.toJson())
#
print(pobject.attr_one)
#
print(pobject.child.attr_one_ch) # Normal
# or
print(pobject.child.__getattribute__("attr_one_ch")) # By name attr
# or
print(pobject.child.__dict__["attr_one_ch"]) # By name in dict of object
#
print(pobject.colors)
OUTPUT:
== OUTPUT ==
{"attr_one": "some_one", "attr_two": "some_two", "child": {"attr_one_ch": "child_some_one", "attr_two_ch": "child_some_two"}, "colors": [{"name": "red", "desc": "is red"}, {"name": "blue", "desc": "is blue"}]}
some_one
child_some_one
child_some_one
child_some_one
[{'name': 'red', 'desc': 'is red'}, {'name': 'blue', 'desc': 'is blue'}]
Upvotes: 0
Reputation: 1888
The encoder/decoder example you reference (here) could be easily extended to allow different types of objects in the JSON input/output.
However, if you just want a simple decoder to match your encoder (only having Edge objects encoded in your JSON file), use this decoder:
class EdgeDecoder(json.JSONDecoder):
def __init__(self, *args, **kwargs):
json.JSONDecoder.__init__(self, object_hook=self.object_hook, *args, **kwargs)
def object_hook(self, dct):
if 'Actor' in dct:
actor = Actor(dct['Actor']['Name'], dct['Actor']['Age'], '')
movie = Movie(dct['Movie']['Title'], dct['Movie']['Gross'], '', dct['Movie']['Year'])
return Edge(actor, movie)
return dct
Using the code from the question to define classes Movie
, Actor
, Edge
, and EdgeEncoder
, the following code will output a test file, then read it back in:
filename='test.json'
movie = Movie('Python', 'many dollars', '', '2000')
actor = Actor('Casper Van Dien', 49, '')
edge = Edge(actor, movie)
with open(filename, 'w') as jsonfile:
json.dump(edge, jsonfile, cls=EdgeEncoder)
with open(filename, 'r') as jsonfile:
edge1 = json.load(jsonfile, cls=EdgeDecoder)
assert edge1 == edge
Upvotes: 23
Reputation: 346
This problem can be solved without the usage of JSONEncoder
or JSONDecoder
.
to_dict()
method to each class (takes care of the conversion from python object
to JSON dict
)bool, str, int, and float
check whether the passed parameters are of type dict
, if that's the case you have to construct the object yourself (see constructor of Edge
)Shortened your example a bit:
class Edge:
def __init__(self, actor, movie):
if type(actor) is Actor:
self.actor = actor
else: # type == dict
self.actor = Actor(**actor)
if type(movie) is Movie:
self.movie = movie
else: # type == dict
self.movie = Movie(**movie)
def __eq__(self, other):
return (self.movie == other.movie) & (self.actor == other.actor)
def __str__(self):
return "".join(["Actor: ", str(self.actor), " /// Movie: ", str(self.movie)])
def to_dict(self):
return {"actor": self.actor.to_dict(), "movie": self.movie.to_dict()}
class Movie:
def __init__(self, title, gross, soup, year):
self.title = title
self.gross = gross
self.soup = soup
self.year = year
def __eq__(self, other):
return self.title == other.title
def __str__(self):
return self.title
def to_dict(self):
return {"title": self.title, "gross": self.gross, "soup": self.soup, "year": self.year}
class Actor:
def __init__(self, name, age, soup):
self.name = name
self.age = age
self.soup = soup
def __eq__(self, other):
return self.name == other.name
def __str__(self):
return self.name
def to_dict(self):
return {"name": self.name, "age": self.age, "soup": self.soup}
if __name__ == '__main__':
edge_obj = Edge(Actor("Pierfrancesco Favino", 50, "id0"), Movie("Suburra", 10, "id1", 2015))
edge_dict = edge_obj.to_dict()
edge_obj_new = Edge(**edge_dict)
print("manual edge\t\t", edge_obj)
print("edge to json\t", edge_dict)
print("auto edge\t\t", edge_obj_new)
print("edges equal?\t", edge_obj == edge_obj_new)
Returns:
manual edge Actor: Pierfrancesco Favino /// Movie: Suburra
edge to json {'actor': {'name': 'Pierfrancesco Favino', 'age': 50, 'soup': 'id0'}, 'movie': {'title': 'Suburra', 'gross': 10, 'soup': 'id1', 'year': 2015}}
auto edge Actor: Pierfrancesco Favino /// Movie: Suburra
edges equal? True
As you can see both Edge
objects are equal and the second line outputs the Edge
as an dict
in JSON
notation.
Upvotes: -2