It's been a while, but it's a sequel to the article I wrote earlier. The one I made this time is uploaded to github. The one that can be installed with pip is for myself.
Put the actual code on github.
I made something similar before, but the code has changed and time has passed, so I will write from 1 without breaking ..
For example, suppose you have the following dict as the source of conversion. It is a structure that includes dict in dict.
src_dict = {
'value' : 'AAA',
'nested' : {
'nested_value' : 'BBB'
}
}
Suppose you want to convert this to a class like this: I want to have an instance of NestedClass in self.nested
of TastClass.
#Class 1
class TestClass():
def test_method(self):
return 'assigned value: ' + self.value
#Class 2-Image hanging in part 1
class NestedTestClass:
def test_method(self):
return 'nested assigned value: ' + self.nested_value
In order to perform the above conversion, I think that it is necessary to map which element of the container is converted to which class, so I thought about this type of dict.
mapping = {
'<MAPPING_ROOT>' : TestClass, #Because the top has no name'<MAPPING_ROOT>'To
'nested' : NestedClass
}
The mapping of [class / dict key]: [function]
is expressed by dict. The dict itself pointed to by src_dict
has no name, so use<MAPPING_ROOT>
as a key instead. The constructor is specified as a function.
Items not included in mapping
, such assrc_dict ['value']
in this example, are set as they are in the conversion destination.
I want to use it like this.
usage
#Take a mapping in the constructor
converter = ObjectConverter(mapping=mapping)
#Pass the conversion source data to the conversion method
converted_class = converter.convert(src_dict)
#Call a method of the converted class
converted_class.test_method()
I made it like this.
converter.py
class ObjectConverter:
#Receive mapping definition at generation
def __init__(self, *, mapping):
self.mapping = mapping
#Transform call method
def convert(self, src):
#Top element is mapping'<root>'Premise that always matches
return self._convert_value('<MAPPING_ROOT>', self.mapping['<MAPPING_ROOT>'], src)
#Determine the processing method according to the value
def _convert_value(self, key, func, value):
#In the case of a list, all the elements are converted with func
if isinstance(value, (list, tuple)):
return self._convert_sequence(key, func, value)
#In the case of dict, retrieve the key and value as they are
if isinstance(value, dict):
return self._convert_dict(key, func, value)
#For class__dict__And treat it as a dict
if isinstance(value, object) and hasattr(value, '__dict__'):
return self._convert_dict(key, func, value.__dict__)
#If none of the above applies, return it as is
return value
#Convert the contents of the dict
def _convert_dict(self, key, func, src):
# _call_Fill the object created by function
return self._assign_dict(self._call_function(key, func, src), key, src)
#Creation of the object specified by mapping
def _call_function(self, key, func, src):
return func()
#Take out the contents of the dict and apply it
def _assign_dict(self, dest, key, src):
for srcKey, value in src.items():
#key is defined in the mapping
if srcKey in self.mapping:
func = self.mapping[srcKey]
#Execute the mapped function and set the result
self._set_value(dest, srcKey, self._convert_value(srcKey, func, value))
#If the key is not in the mapping definition, set it as it is
else:
#Even if there is a mapping defined value in the value passed here, it will be ignored.
self._set_value(dest, srcKey, value)
#The state where the contents of src are reflected in created
return dest
#List processing
def _convert_sequence(self, key, func, sequence):
current = []
for value in sequence:
current.append(self._convert_value(key, func, value))
return current
#Value setter for both dict and class
def _set_value(self, dest, key, value):
if isinstance(dest, dict):
dest[key] = value
else:
setattr(dest, key, value)
#Utility method to get an instance for dict conversion
#
@classmethod
def dict_converter(cls, mapping, *, dict_object=dict):
reverse_mapping = {}
#Make all mapping destinations dict
for key in mapping.keys():
reverse_mapping[key] = dict_object
#Instance for converting to dict
return ObjectConverter(mapping=reverse_mapping)
** See source for full specifications. ** But nothing happens, it just scans the value that matches the key contained in the mapping. Roughly speaking, I am doing this.
<mapping_root>
.setattr
or assign it as the value of dict.By extracting __dict__
when a class is found, all you have to do is scan the dict. I'd like to avoid touching __dict__
if possible, but this time I'll avoid the effort of using other methods. Unless you are dealing with a special class, there should be no problem. [^ dont-touch-this]
The value not included in mapping is set to the conversion destination object as it is, but even if the value at this time is dict and there is a value with the name included in mapping in it, mapping processing is performed. It will not be. This is "processed dict included in mapping or included in mapping ue I don't like the case of "dict processing", and I think that the data structure that requires such conversion is not very beautiful.
For cases where you want to do a class to dict conversion after a dict to class conversion to get it back to its original form, we have a class method dict_converter
to make it easy to get a reverse converter. It's easy because all the conversion destinations on mapping are set to dict. [^ not-dict-something]
test_objectconverter.py
import unittest
import json
from objectonverter import ObjectConverter
#Test class 1
class TestClass():
def test_method(self):
return 'TestObject.test_method'
#Test class part 2
class NestedTestClass:
def test_method(self):
return 'NestedObject.test_method'
class ClassConverterTest(unittest.TestCase):
#Just set the properties of the root class
def test_object_convert(self):
dict_data = {
'value1' : 'string value 1'
}
converter = ObjectConverter(mapping={'<MAPPING_ROOT>' : TestClass})
result = converter.convert(dict_data)
self.assertEqual(result.value1, 'string value 1')
#Try to call the method of the generated class
self.assertEqual(result.test_method(), 'TestObject.test_method')
#Generate a nested class
def test_nested_object(self):
#dict that maps json keys and classes
object_mapping = {
'<MAPPING_ROOT>' : TestClass,
'nested' : NestedTestClass
}
#Source of origin
dict_data = {
'value1' : 'string value 1',
'nested' : {
'value' : 'nested value 1'
}
}
converter = ObjectConverter(mapping=object_mapping)
result = converter.convert(dict_data)
self.assertEqual(result.value1, 'string value 1')
self.assertIsInstance(result.nested, NestedTestClass)
self.assertEqual(result.nested.value, 'nested value 1')
#Just a dict if you don't specify a mapping
def test_nested_dict(self):
object_mapping = {
'<MAPPING_ROOT>' : TestClass
}
#Source of origin
dict_data = {
'value1' : 'string value 1',
'nested' : {
'value' : 'nested value 1'
}
}
converter = ObjectConverter(mapping = object_mapping)
result = converter.convert(dict_data)
self.assertEqual(result.value1, 'string value 1')
self.assertIsInstance(result.nested, dict)
self.assertEqual(result.nested['value'], 'nested value 1')
#List processing
def test_sequence(self):
mapping = {
'<MAPPING_ROOT>' : TestClass,
'nestedObjects' : NestedTestClass,
}
source_dict = {
"value1" : "string value 1",
"nestedObjects" : [
{'value' : '0'},
{'value' : '1'},
{'value' : '2'},
]
}
converter = ObjectConverter(mapping=mapping)
result = converter.convert(source_dict)
self.assertEqual(result.value1, 'string value 1')
self.assertEqual(len(result.nestedObjects), 3)
for i in range(3):
self.assertIsInstance(result.nestedObjects[i], NestedTestClass)
self.assertEqual(result.nestedObjects[i].value, str(i))
#If the root element itself is a list
def test_root_sequence(self):
object_mapping = {
'<MAPPING_ROOT>' : TestClass,
}
source_list = [
{'value' : '0'},
{'value' : '1'},
{'value' : '2'},
]
converter = ObjectConverter(mapping=object_mapping)
result = converter.convert(source_list)
self.assertIsInstance(result, list)
self.assertEqual(len(result), 3)
for i in range(3):
self.assertIsInstance(result[i], TestClass)
self.assertEqual(result[i].value, str(i))
# json -> class -> json
def test_json_to_class_to_json(self):
#Function used for mutual conversion from class to json
def default_method(item):
if isinstance(item, object) and hasattr(item, '__dict__'):
return item.__dict__
else:
raise TypeError
#dict that maps json keys and classes
object_mapping = {
'<MAPPING_ROOT>' : TestClass,
'nested' : NestedTestClass
}
#Source of origin-In one line for convenience of comparison
string_data = '{"value1": "string value 1", "nested": {"value": "nested value 1"}}'
dict_data = json.loads(string_data)
converter = ObjectConverter(mapping=object_mapping)
result = converter.convert(dict_data)
dump_string = json.dumps(result, default=default_method)
self.assertEqual(dump_string, string_data)
#The result is the same even if it is converted again
result = converter.convert(json.loads(dump_string))
self.assertEqual(result.value1, 'string value 1')
self.assertIsInstance(result.nested, NestedTestClass)
self.assertEqual(result.nested.value, 'nested value 1')
#conversion->Inverse conversion
def test_reverse_convert(self):
dict_data = {
'value1' : 'string value 1'
}
mapping = {'<MAPPING_ROOT>' : TestClass}
converter = ObjectConverter(mapping=mapping)
result = converter.convert(dict_data)
self.assertEqual(result.value1, 'string value 1')
#Generate inverse conversion converter
reverse_converter = ObjectConverter.dict_converter(mapping=mapping)
reversed_result = reverse_converter.convert(result)
self.assertEqual(result.value1, reversed_result['value1'])
if __name__ == '__main__':
unittest.main()
Well, I wonder if you can understand the basics
There is a test case with the long name test_json_to_class_to_json
, but this is because this class was originally very conscious of conversion to json.
It's been more than half a year since the last article, but in fact I finally got a job ... I didn't have time, so it was late.
[^ generic-conteiners]: Since it is long to write "dict / list" many times below, I will write it as a general-purpose container. By the way, a "class" is also an "instance of a class" to be exact, but it is a "class" because it is long.
[^ json-to-class]: The first thing I was thinking about was how to handle json, so I was trapped by json. As I chase things, I realize that I was actually looking for very simple results. It's a “something” flow, but you have to think more carefully.
[^ dont-touch-this]: It's easy, so I'll just do it, but __dict __
is like a back door, and when you retrieve the value from __dict __
, __getattribute__
may not be called, so the class It may behave differently than it was originally intended. I won't think about that here.
[^ not-dict-something]: I wrote it to support types other than Ichiou dict, but I can't think of any use for it.
Recommended Posts