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

49

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

我找到了这个问题关于从对象字段创建字典, 但它不递归。

作为相对较新的Python用户,我担心我的解决方案可能很丑陋、不符合Python风格、有些隐晦的方式出错,或者只是老套的NIH。

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

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

这种方法似乎更加有效,而且不需要使用异常,但我仍然不确定是否存在我不知道的情况导致它失败。
非常感谢任何建议。

2
在Python中,使用异常并不是一件坏事,有时可以简化编码。采用一种Python风格的编程方式——EAFP(宁愿请求宽恕,而不是征得许可),能够让代码更加简洁易懂。 - Anurag Uniyal
特殊情况可能出现在对象具有__slots__时,编辑答案。 - Anurag Uniyal
1
理解你的观点,但异常处理是一个有争议的话题,我倾向于除非出现真正的异常情况,否则不应该抛出异常,而是应该按照预期的程序流程进行处理。在这个问题上,每个人都有自己的看法 :) - Shabbyrobe
1
是的,大多数情况下这只是一种风格,但有时异常确实可以简化代码,例如当最内层函数通过异常进行通信时,有时它可以加快速度,例如如果99.99%的对象都有字典,在这种情况下,不要检查99.99%的时间是否为字典,而是在0.01%的时间内检查异常。 - Anurag Uniyal
14个回答

61

我结合了自己的尝试以及从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

做得很好。目前只有这个实现方式完全符合我想要的要求。 - Murat Ayfer
4
谢谢,那个基本上有用。但是需要注意几点:在Python 3.5中,iteritems()应该改为items();第10行的[todict(v, classkey) for v in obj]尝试迭代字符串中的字符,可以通过 elif hasattr(obj, "__iter__") and not isinstance(obj, str) 进行修复。 - Sander Verhagen
这会将我的字符串值转换为None。一个小但重要的错误。 我通过捕获字符串类型的对象并在这些情况下返回str(obj)来解决了这个问题。 - Vaibhav Tripathi
尝试将一个包含嵌套对象的复杂对象转换时,我遇到了这个错误:RecursionError: maximum recursion depth exceeded while calling a Python object,并且回溯指向这一行代码:data = dict([(key, todict(value, classkey))。你有什么想法吗? - Cesar Flores

37

一行代码递归地将对象转换为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))

1
我可以问一下,是否有一种简洁的方式从JSON中加载对象吗? - Tengerye
你可以这样做 obj.dict = {'key': 'value'}。 - Archit Dwivedi
1
这个解决方案对我没有用。出现了一个 ValueError: Circular reference detected 的错误。由于已经接受了其他的解决方案,所以没有进一步调查。 - Arigion
可能是因为其中一个对象可能引用了父对象。 - Archit Dwivedi
1
如果您有复杂的对象,这种方法可能行不通。可能会弹出ValueError: Circular reference detected错误,或者您会收到指向类型为xxx的对象无法进行JSON序列化的错误提示。 - Cesar Flores
显示剩余2条评论

8

我不知道检查basestring或者object是为了什么?另外,除非有指向这些可调用对象的属性,否则dict不会包含任何可调用对象,但在这种情况下,那不是对象的一部分吗?

因此,与其检查各种类型和值,让todict转换对象,如果它引发异常,则使用原始值。

只有当obj没有dict时,todict才会引发异常,例如:

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}}。还有其他一些需要考虑的情况,但这可能是一个很好的开始。

特殊情况 如果一个对象使用slots,则无法获取dict,例如:

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

对于插槽的情况,解决方法可以是使用dir()而不是直接使用dict


感谢您的帮助和启发。我刚意识到它不能处理对象列表,所以我更新了我的版本来测试__iter__。不确定这是否是一个好主意。 - Shabbyrobe
看起来会变得更加棘手,因为对于一个提供迭代列表属性的对象,你已经将其放入字典中,可能没有通用解决方案。 - Anurag Uniyal
感谢@AnuragUniyal! 你让我的一天变得美好了!

noinspection PyProtectedMember

def object_to_dict(obj): data = {} if getattr(obj, 'dict', None): for key, value in obj.dict.items(): try: data[key] = object_to_dict(value) except AttributeError: data[key] = value return data else: return obj
- Alexey Korolkov

4

我知道这个答案晚了几年,但是我认为它值得分享,因为它是对@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))

4
使用 jsonpickle 将对象转换为 JSON 字符串,然后使用 json.loads 将其转换回 Python 字典是一种缓慢但简单的方法: dict = json.loads(jsonpickle.encode(obj, unpicklable=False))

作者注:现在不建议这样做。 - Tom

2
无需自定义实现。可以使用jsons库。
import jsons

object_dict = jsons.dump(object_instance)

2
在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 的情况,我相信还有很多情况它无法覆盖,并且可能无法覆盖。 :)


很不幸,这会崩溃并显示“subobj未定义”。 - Emil Stenström

1

感谢@AnuragUniyal! 你让我开心了! 这是我自己的代码变体,对我来说很有效:

# noinspection PyProtectedMember
def object_to_dict(obj):
    data = {}
    if getattr(obj, '__dict__', None):
        for key, value in obj.__dict__.items():
            try:
                data[key] = object_to_dict(value)
            except AttributeError:
                data[key] = value
        return data
    else:
        return obj

0
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

0

之前的答案在类字段是类实例时无法工作。请使用以下代码:

from dataclasses import dataclass, field

@dataclass
class BaseNumber:
    number:str = ''
    probability:float = 0.

@dataclass
class ContainerInfo:
    type:str = ''
    height:int = ''
    width:str = ''
    length:str = ''

@dataclass
class AdditionalNumber:
    number:str = ''
    prob:float = 0.
    info:ContainerInfo = ContainerInfo()

@dataclass  
class ContainerData:
    container_number = BaseNumber()
    container_type = AdditionalNumber()
    errors:list = field(default_factory=list)

    def todict(self, obj='sadasdas'):
        if obj == 'sadasdas':
            obj = self
            
        if isinstance(obj, dict):
            data = {}
            for (k, v) in obj.items():
                data[k] = self.todict(v)
            return data
        elif hasattr(obj, "_ast"):
            return self.todict(obj._ast())
        elif hasattr(obj, "__iter__") and not isinstance(obj, str):
            return [self.todict(v) for v in obj]
        elif hasattr(obj, "__dict__"):
            aaa = dir(obj)
            data = dict([(key, self.todict(value)) 
                for key, value in {field: getattr(obj, field) for field in dir(obj)}.items()
                if not callable(value) and not key.startswith('_')
            ])
            return data
        else:
            return obj

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接