递归地将python对象图转换为字典

Posted

技术标签:

【中文标题】递归地将python对象图转换为字典【英文标题】:Recursively convert python object graph to dictionary 【发布时间】:2010-11-05 09:41:34 【问题描述】:

我正在尝试将数据从简单的对象图转换为字典。我不需要类型信息或方法,也不需要能够再次将其转换回对象。

我找到了this question about creating a dictionary from an object's fields,但它并没有递归。

对 python 来说相对较新,我担心我的解决方案可能很丑陋,或者 unpythonic,或者以某种模糊的方式损坏,或者只是普通的旧 NIH。

我的第一次尝试似乎有效,直到我尝试使用列表和字典,并且检查传递的对象是否具有内部字典似乎更容易,如果没有,则将其视为一个值(而不是全部那是实例检查)。我之前的尝试也没有递归到对象列表中:

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

这似乎工作得更好,不需要例外,但我仍然不确定这里是否有我不知道它落在哪里的案例。

任何建议将不胜感激。

【问题讨论】:

在python中使用异常并不是那么糟糕,有时它可以简化编码,这是一种pythonic方式-EAFP(Easier to Ask Forgiveness than Permission) 特殊情况可能是当对象有 slots,已编辑答案 点了,但例外的事情有点像一场圣战,我倾向于永远不要抛出它们,除非有什么真正的例外,而不是预期的程序流程。每个人都有自己的那个:) 是的,主要是风格,但有时异常可以真正简单地编码,例如当内部大多数功能通过异常进行通信时,有时它可以加快速度,例如如果在这种情况下 99.99% 的对象有 dict,而不是检查 99.99% 的时间,检查异常 0.01% 的时间 【参考方案1】:

我自己的尝试和从 Anurag Uniyal 和 Lennart Regebro 的答案中获得的线索的融合最适合我:

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

【讨论】:

做得很好。到目前为止,唯一能按照我的意愿工作的实现。 谢谢,大部分都有效。几个注意事项:在 Python 3.5 中,iteritems() 应该是 items(),而 [todict(v, classkey) for v in obj](第 10 行)尝试迭代字符串中的字符,使用以下方法修复:elif hasattr(obj, "__iter__") and not isinstance(obj, str) 这会将我的字符串值转换为无。一个小而重要的错过。我通过捕获字符串类型的对象并在这些情况下返回 str(obj) 解决了这个问题。 尝试使用嵌套对象转换复杂对象,我收到此错误。 RecursionError: maximum recursion depth exceeded while calling a Python object 和回溯指向这一行 data = dict([(key, todict(value, classkey))。知道这可能是什么吗?【参考方案2】:

将对象递归转换为 JSON 的一行代码。

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))

【讨论】:

请问有没有这么简洁的从JSON加载对象的方式? 你可以这样做 obj.__dict__ = 'key': 'value' 从json加载对象,看这个:***.com/questions/6578986/… 这个解决方案对我不起作用。得到一个 ValueError:检测到循环引用。由于接受的解决方案有效,因此没有进一步调查。 可能是因为其中一个对象可能有对父对象的引用。【参考方案3】:

我不知道检查 basestring 或 object 的目的是什么? dict 也不会包含任何可调用对象,除非您有指向此类可调用对象的属性,但在这种情况下,那不是对象的一部分吗?

所以不是检查各种类型和值,而是让 todict 转换对象,如果它引发异常,则使用原始值。

tod​​ict 只会在 obj 没有 dict 时引发异常 例如

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)

它打印 'b1': 1, 'b2': 2, 'o1': 'a1': 1 可能还有其他一些情况需要考虑,但这可能是一个好的开始

特殊情况 如果一个对象使用槽,那么你将无法获得 dict 例如

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

插槽情况的修复可以是使用 dir() 而不是直接使用 dict

【讨论】:

感谢您的帮助和启发。我刚刚意识到它不处理对象列表,所以我更新了我的版本来测试 iter。不过不确定这是否是个好主意。 看起来会变得更棘手,因为对于提供迭代器来迭代您已经放入 dict 的列表属性的对象会发生什么情况,可能是不可能的通用解决方案。【参考方案4】:

一种缓慢但简单的方法是使用jsonpickle 将对象转换为JSON 字符串,然后使用json.loads 将其转换回python 字典:

dict = json.loads(jsonpickle.encode( obj, unpicklable=False ))

【讨论】:

【参考方案5】:

我意识到这个答案已经晚了几年,但我认为它可能值得分享,因为它是@Shabbyrobe 对原始解决方案的 Python 3.3+ 兼容修改,通常对我来说效果很好:

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

如果您对可调用属性不感兴趣,例如,可以在字典理解中剥离它们:

elif isinstance(obj, dict):
  return dict((key, todict(val)) for key, val in obj.items() if not callable(val))

【讨论】:

【参考方案6】:

在 Python 中,有很多方法可以使对象的行为略有不同,例如元类和诸如此类,它可以覆盖 getattr,从而具有您无法通过 dict 看到的“神奇”属性 等。简而言之,无论您使用何种方法,您都不太可能在通用案例中获得 100% 完整的图片。

因此,答案是:如果它适用于您现在拥有的用例,那么代码是正确的。 ;-)

要编写更通用的代码,您可以执行以下操作:

import types
def todict(obj):
    # Functions, methods and None have no further info of interest.
    if obj is None or isinstance(subobj, (types.FunctionType, types.MethodType))
        return obj

    try: # If it's an iterable, return all the contents
        return [todict(x) for x in iter(obj)]
    except TypeError:
        pass

    try: # If it's a dictionary, recurse over it:
        result = 
        for key in obj:
            result[key] = todict(obj)
        return result
    except TypeError:
        pass

    # It's neither a list nor a dict, so it's a normal object.
    # Get everything from dir and __dict__. That should be most things we can get hold of.
    attrs = set(dir(obj))
    try:
        attrs.update(obj.__dict__.keys())
    except AttributeError:
        pass

    result = 
    for attr in attrs:
        result[attr] = todict(getattr(obj, attr, None))
    return result            

类似的东西。不过,该代码未经测试。当您覆盖 getattr 时,这仍然不涵盖这种情况,而且我敢肯定还有更多的情况没有涵盖并且可能无法涵盖。 :)

【讨论】:

不幸的是,这会因“未定义子对象”而崩溃。【参考方案7】:

对 Shabbyrobe 的回答稍作更新,使其适用于 namedtuples:

def obj2dict(obj, classkey=None):
    if isinstance(obj, dict):
        data = 
        for (k, v) in obj.items():
            data[k] = obj2dict(v, classkey)
        return data
    elif hasattr(obj, "_asdict"):
        return obj2dict(obj._asdict())
    elif hasattr(obj, "_ast"):
        return obj2dict(obj._ast())
    elif hasattr(obj, "__iter__"):
        return [obj2dict(v, classkey) for v in obj]
    elif hasattr(obj, "__dict__"):
        data = dict([(key, obj2dict(value, classkey))
                     for key, value in obj.__dict__.iteritems()
                     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

【讨论】:

【参考方案8】:
def list_object_to_dict(lst):
    return_list = []
    for l in lst:
        return_list.append(object_to_dict(l))
    return return_list

def object_to_dict(object):
    dict = vars(object)
    for k,v in dict.items():
        if type(v).__name__ not in ['list', 'dict', 'str', 'int', 'float']:
                dict[k] = object_to_dict(v)
        if type(v) is list:
            dict[k] = list_object_to_dict(v)
    return dict

【讨论】:

【参考方案9】:

查看了所有解决方案,@hbristow 的答案与我所寻找的最接近。 添加了enum.Enum 处理,因为这会导致RecursionError: maximum recursion depth exceeded 错误,并且使用__slots__ 重新排序的对象优先于定义__dict__ 的对象。

def todict(obj):
  """
  Recursively convert a Python object graph to sequences (lists)
  and mappings (dicts) of primitives (bool, int, float, string, ...)
  """
  if isinstance(obj, str):
    return obj
  elif isinstance(obj, enum.Enum):
    return str(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, '__slots__'):
    return todict(dict((name, getattr(obj, name)) for name in getattr(obj, '__slots__')))
  elif hasattr(obj, '__dict__'):
    return todict(vars(obj))
  return obj

【讨论】:

【参考方案10】:

不需要自定义实现。可以使用jsons库。

import jsons

object_dict = jsons.dump(object_instance)

【讨论】:

【参考方案11】:

我会评论接受的答案,但我的代表不够高...... 接受的答案很好,但在 if 之后添加另一个 elif 以支持 NamedTuples 序列化以正确地进行 dict:

    elif hasattr(obj, "_asdict"):
        return todict(obj._asdict())

【讨论】:

【参考方案12】:

嗯。添加了将深度限制为@Shabbyrobe 答案的功能。认为循环返回的对象可能是值得的。

def todict(obj, limit=sys.getrecursionlimit(), classkey=None):
        if isinstance(obj, dict):
            if limit>=1:
                data = 
                for (k, v) in obj.items():
                    data[k] = todict(v, limit-1,classkey)
                return data
            else:
                return 'class:'+obj.__class__.__name__
        elif hasattr(obj, "_ast"):
            return todict(obj._ast(), limit-1) if limit>=1 else 'class:'+obj.__class__.__name__
        elif hasattr(obj, "__iter__") and not isinstance(obj, str):
            return [todict(v, limit-1, classkey) for v in obj] if limit>=1 else 'class:'+obj.__class__.__name__
        elif hasattr(obj, "__dict__"):
            if limit>=1:
                data = dict([(key, todict(value, limit-1, 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 'class:'+obj.__class__.__name__
        else:
            return obj

【讨论】:

如果对象没有正确执行 dict 方法,那么您需要使用 dir()

以上是关于递归地将python对象图转换为字典的主要内容,如果未能解决你的问题,请参考以下文章

一个可以递归遍历对象图的 NSPredicate?

从 SimpleXML 对象到数组的递归转换

使用元组键将 Pandas 数据框转换为字典以进行三元图

python 将Suds对象转换为Python字典

Python。将 2 个列表转换为一个字典对象 [重复]

来自嵌套字典的 Python 数据类