Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Recursively convert python object graph to dictionary

I'm trying to convert the data from a simple object graph into a dictionary. I don't need type information or methods and I don't need to be able to convert it back to an object again.

I found this question about creating a dictionary from an object's fields, but it doesn't do it recursively.

Being relatively new to python, I'm concerned that my solution may be ugly, or unpythonic, or broken in some obscure way, or just plain old NIH.

My first attempt appeared to work until I tried it with lists and dictionaries, and it seemed easier just to check if the object passed had an internal dictionary, and if not, to just treat it as a value (rather than doing all that isinstance checking). My previous attempts also didn't recurse into lists of objects:

def todict(obj):
    if hasattr(obj, "__iter__"):
        return [todict(v) for v in obj]
    elif hasattr(obj, "__dict__"):
        return dict([(key, todict(value)) 
            for key, value in obj.__dict__.iteritems() 
            if not callable(value) and not key.startswith('_')])
    else:
        return obj

This seems to work better and doesn't require exceptions, but again I'm still not sure if there are cases here I'm not aware of where it falls down.

Any suggestions would be much appreciated.

like image 334
Shabbyrobe Avatar asked Jun 24 '09 04:06

Shabbyrobe


4 Answers

An amalgamation of my own attempt and clues derived from Anurag Uniyal and Lennart Regebro's answers works best for me:

def todict(obj, classkey=None):
    if isinstance(obj, dict):
        data = {}
        for (k, v) in obj.items():
            data[k] = todict(v, classkey)
        return data
    elif hasattr(obj, "_ast"):
        return todict(obj._ast())
    elif hasattr(obj, "__iter__") and not isinstance(obj, str):
        return [todict(v, classkey) for v in obj]
    elif hasattr(obj, "__dict__"):
        data = dict([(key, todict(value, classkey)) 
            for key, value in obj.__dict__.items() 
            if not callable(value) and not key.startswith('_')])
        if classkey is not None and hasattr(obj, "__class__"):
            data[classkey] = obj.__class__.__name__
        return data
    else:
        return obj
like image 182
Dougui Avatar answered Oct 17 '22 22:10

Dougui


One line of code to convert an object to JSON recursively.

import json

def get_json(obj):
  return json.loads(
    json.dumps(obj, default=lambda o: getattr(o, '__dict__', str(o)))
  )

obj = SomeClass()
print("Json = ", get_json(obj))
like image 27
Archit Dwivedi Avatar answered Oct 17 '22 21:10

Archit Dwivedi


I don't know what is the purpose of checking for basestring or object is? also dict will not contain any callables unless you have attributes pointing to such callables, but in that case isn't that part of object?

so instead of checking for various types and values, let todict convert the object and if it raises the exception, user the orginal value.

todict will only raise exception if obj doesn't have dict e.g.

class A(object):
    def __init__(self):
        self.a1 = 1

class B(object):
    def __init__(self):
        self.b1 = 1
        self.b2 = 2
        self.o1 = A()

    def func1(self):
        pass

def todict(obj):
    data = {}
    for key, value in obj.__dict__.iteritems():
        try:
            data[key] = todict(value)
        except AttributeError:
            data[key] = value
    return data

b = B()
print todict(b)

it prints {'b1': 1, 'b2': 2, 'o1': {'a1': 1}} there may be some other cases to consider, but it may be a good start

special cases if a object uses slots then you will not be able to get dict e.g.

class A(object):
    __slots__ = ["a1"]
    def __init__(self):
        self.a1 = 1

fix for the slots cases can be to use dir() instead of directly using the dict

like image 8
Anurag Uniyal Avatar answered Oct 17 '22 22:10

Anurag Uniyal


I realize that this answer is a few years too late, but I thought it might be worth sharing since it's a Python 3.3+ compatible modification to the original solution by @Shabbyrobe that has generally worked well for me:

import collections
try:
  # Python 2.7+
  basestring
except NameError:
  # Python 3.3+
  basestring = str 

def todict(obj):
  """ 
  Recursively convert a Python object graph to sequences (lists)
  and mappings (dicts) of primitives (bool, int, float, string, ...)
  """
  if isinstance(obj, basestring):
    return obj 
  elif isinstance(obj, dict):
    return dict((key, todict(val)) for key, val in obj.items())
  elif isinstance(obj, collections.Iterable):
    return [todict(val) for val in obj]
  elif hasattr(obj, '__dict__'):
    return todict(vars(obj))
  elif hasattr(obj, '__slots__'):
    return todict(dict((name, getattr(obj, name)) for name in getattr(obj, '__slots__')))
  return obj

If you're not interested in callable attributes, for example, they can be stripped in the dictionary comprehension:

elif isinstance(obj, dict):
  return dict((key, todict(val)) for key, val in obj.items() if not callable(val))
like image 4
hbristow Avatar answered Oct 17 '22 23:10

hbristow