jo31
jo31

Reputation: 187

Convert dict to class dynamically in python

I know similar questions have been previously answered, but after spending hours browsing the web, I still cannot the right solution to my problem (or the 'python-way' solution)

I'm using the following 'database' module to insert any object into MongoDB:

from pymongo import MongoClient
import json

def insert(obj, collection_name):
    with MongoClient(27017, 'localhost') as client:
        if hasattr(obj, '__dict__'):
            doc = obj.__dict__
        else:
            doc = json.dumps(obj)
        return client['test'][collection_name].insert_one(doc).inserted_id

def get_one(collection_name, query):
    with MongoClient(27017, 'localhost') as client:
        return client['test'][collection_name].find_one(query)

This part works well as I can handle any class in any module and store the objects in a generic way.

The issue I'm facing is to reload an object from MongoDB and cast it back to the correct class.

I have a generic class 'Operation' (representing an action that can be done to bank/money account:

import database 

class Operation(object):
    def __init__(self):
        self._id = 'OPE-{}'.format(str(uuid.uuid1()))

    @property
    def id(self):
        return self._id

    def save(self):
        database.update(self, 'operations')

def create_from_dict(d):
    operation = Operation()
    for key, value in d.items():
        operation.__dict__[key] = value
    return operation

def get(operation_id):
    return create_from_dict(database.get_one('operations', {'_id': operation_id}))

Until this point everything works, I can instantiate any Operation object, insert it into MongoDB and load it back using it's id.

Now I have two new classes 'Fee' and 'Credit' derived from 'Operation'

import uuid
from operation import Operation

class Credit(Operation):
    def __init__(self):
        Operation.__init__(self)
        self._id = 'CRE-{}'.format(uuid.uuid1())

    @property
    def id(self):
        return self._id

And

import uuid
from operation import Operation

class Fee(Operation):
    def __init__(self):
        Operation.__init__(self)
        self._id = 'FEE-{}'.format(uuid.uuid1())

    @property
    def id(self):
        return self._id

The idea is to have keep the code to save/load objects in the 'Operation' class and create simple derived class whenever needed. So the 'Operation' class should be able to cast dynamically the loaded object from Operation to Credit/Fee/Transfer...

What I tried to do is add the following line on all classes in order to store the object type:

self.type = self.__class__

Or alternatively (since the class cannot be directly serialized to json)

self.type = self.__name__

But I don't know how I could achieve the :

def create_from_dict(d):
    operation = Operation()
    for key, value in d.items():
        operation.__dict__[key] = value
    operation.__class__ = get_type_from_name(operation.type) #This is the function I'm trying to implement
    return operation

Should I store both the module and the class name when encoding the object to cast it back from another module?

Sorry for the quite long question but I wanted to fully describe the logic behind my code I'm definitely quite open at any suggestion!

EDIT: Could locals() or globals() be the way to go? You pass the class name to the get_type_from_name() method and it returns the corresponding class? Additional question for advanded Pythonists: is that the right approach?

Upvotes: 1

Views: 1178

Answers (2)

jo31
jo31

Reputation: 187

I think I managed to make it work:

Adding this to the classes I want to (de)serialize:

self.type = self.__class__.__name__
self.module = self.__module__

Then once the json doc is loaded from PyMongo:

    if not hasattr(doc, 'module') or not hasattr(doc, 'type'):
        return obj
    for key, value in doc.items():
        obj.__dict__[key] = value
    mod = __import__(obj.module, fromlist=[obj.type])
    obj.__class__ = getattr(mod, obj.type)

The last two lines import the module containing the class and modify the object class dynamically.

Thanks user3030010 for your answer. But I'm still very curious to know if you guys think this a good design or that could lead to more issues.

Upvotes: 0

user3030010
user3030010

Reputation: 1857

Using the self.type = self.__name__ idea you suggested, you could try this implementation for the get_type_from_name function:

def get_type_from_name(name):
    name_to_type = {'Credit': Credit, 'Fee':Fee}
    return name_to_type[name]

I'm not quite sure that I fully understand your problem though, so let me know if this isn't what you want.

Upvotes: 1

Related Questions