【问题标题】:Recursively convert python object graph to dictionary递归地将python对象图转换为字典
【发布时间】: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% 的时间

标签: python python-2.6


【解决方案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加载对象,看这个:stackoverflow.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()
                猜你喜欢
                • 2018-02-21
                • 2022-06-15
                • 1970-01-01
                • 2011-11-26
                • 2019-07-21
                • 2022-10-02
                • 2014-07-07
                • 2011-03-16
                • 1970-01-01
                相关资源
                最近更新 更多