将Django Model对象转换成字典并保留所有字段

463

如何将Django模型对象转换为包含所有字段的字典?最好包括外键和editable=False的字段。

让我详细解释一下。假设我有一个像以下这样的Django模型:

from django.db import models

class OtherModel(models.Model): pass

class SomeModel(models.Model):
    normal_value = models.IntegerField()
    readonly_value = models.IntegerField(editable=False)
    auto_now_add = models.DateTimeField(auto_now_add=True)
    foreign_key = models.ForeignKey(OtherModel, related_name="ref1")
    many_to_many = models.ManyToManyField(OtherModel, related_name="ref2")

在终端上,我已经完成了以下操作:

other_model = OtherModel()
other_model.save()
instance = SomeModel()
instance.normal_value = 1
instance.readonly_value = 2
instance.foreign_key = other_model
instance.save()
instance.many_to_many.add(other_model)
instance.save()

我想将这个转换为以下字典:

{'auto_now_add': datetime.datetime(2015, 3, 16, 21, 34, 14, 926738, tzinfo=<UTC>),
 'foreign_key': 1,
 'id': 1,
 'many_to_many': [1],
 'normal_value': 1,
 'readonly_value': 2}

无满意答案的问题:

Django:将模型对象集转换为单个字典

如何将Django模型对象转换为字典并仍然保留其外键?


2
你可以声明一个名为 to_dict 的方法,并按照你想要的方式处理它。 - karthikr
2
@karthikr 是的,我可以。问题是如何创建这样的方法。手动从模型的所有字段构建字典不是一个合适的答案。 - Zags
我会利用现有的ReST库,如Django Rest Framework,Tastypie或Piston,因为它们都提供将Django模型实例转换为序列化原语的机制。如果您更好奇其中的原理,可以查看它们的代码,但主要是通过遍历模型的'_meta'定义来查找与模型相关联的字段,并在实例上检索它们的值。 - Kevin Stone
18个回答

966

有很多方法可以将一个实例转换成字典,这些方法的处理边缘情况和接近期望结果程度不同。


1. instance.__dict__

instance.__dict__

返回 which

{'_foreign_key_cache': <OtherModel: OtherModel object>,
 '_state': <django.db.models.base.ModelState at 0x7ff0993f6908>,
 'auto_now_add': datetime.datetime(2018, 12, 20, 21, 34, 29, 494827, tzinfo=<UTC>),
 'foreign_key_id': 2,
 'id': 1,
 'normal_value': 1,
 'readonly_value': 2}

到目前为止,这是最简单的方法,但缺少 many_to_manyforeign_key 的名称不正确,并且其中还有两个不需要的额外内容。


2. model_to_dict

from django.forms.models import model_to_dict
model_to_dict(instance)

返回

{'foreign_key': 2,
 'id': 1,
 'many_to_many': [<OtherModel: OtherModel object>],
 'normal_value': 1}

这是唯一一个拥有many_to_many的,但缺少不可编辑字段。


3. model_to_dict(..., fields=...)

from django.forms.models import model_to_dict
model_to_dict(instance, fields=[field.name for field in instance._meta.fields])

返回

{'foreign_key': 2, 'id': 1, 'normal_value': 1}

这比标准的model_to_dict调用严格更糟糕。


4. query_set.values()

SomeModel.objects.filter(id=instance.id).values()[0]

返回

{'auto_now_add': datetime.datetime(2018, 12, 20, 21, 34, 29, 494827, tzinfo=<UTC>),
 'foreign_key_id': 2,
 'id': 1,
 'normal_value': 1,
 'readonly_value': 2}

这与instance.__dict__输出相同,但没有额外的字段。foreign_key_id仍然错误,many_to_many仍然缺失。


5. 自定义函数

Django的model_to_dict代码已经包含了大部分答案。它明确删除了不可编辑的字段,因此删除该检查并获取多对多字段的外键ID将得到以下代码,其行为符合要求:

from itertools import chain

def to_dict(instance):
    opts = instance._meta
    data = {}
    for f in chain(opts.concrete_fields, opts.private_fields):
        data[f.name] = f.value_from_object(instance)
    for f in opts.many_to_many:
        data[f.name] = [i.id for i in f.value_from_object(instance)]
    return data

虽然这是最复杂的选项,但调用 to_dict(instance) 正好可以给我们所需的结果:

{'auto_now_add': datetime.datetime(2018, 12, 20, 21, 34, 29, 494827, tzinfo=<UTC>),
 'foreign_key': 2,
 'id': 1,
 'many_to_many': [2],
 'normal_value': 1,
 'readonly_value': 2}

6. 使用序列化器

Django Rest Framework的ModelSerializer能够自动从模型中构建一个序列化器。

from rest_framework import serializers
class SomeModelSerializer(serializers.ModelSerializer):
    class Meta:
        model = SomeModel
        fields = "__all__"

SomeModelSerializer(instance).data

返回

{'auto_now_add': '2018-12-20T21:34:29.494827Z',
 'foreign_key': 2,
 'id': 1,
 'many_to_many': [2],
 'normal_value': 1,
 'readonly_value': 2}

这几乎和自定义函数一样好,但是auto_now_add是一个字符串而不是datetime对象。


额外福利回合:更好的模型打印

如果您想要一个在Python命令行中有更好显示效果的Django模型,请将您的模型作为以下模型的子类:

from django.db import models
from itertools import chain

class PrintableModel(models.Model):
    def __repr__(self):
        return str(self.to_dict())

    def to_dict(instance):
        opts = instance._meta
        data = {}
        for f in chain(opts.concrete_fields, opts.private_fields):
            data[f.name] = f.value_from_object(instance)
        for f in opts.many_to_many:
            data[f.name] = [i.id for i in f.value_from_object(instance)]
        return data

    class Meta:
        abstract = True

因此,例如,如果我们将我们的模型定义为:

class OtherModel(PrintableModel): pass

class SomeModel(PrintableModel):
    normal_value = models.IntegerField()
    readonly_value = models.IntegerField(editable=False)
    auto_now_add = models.DateTimeField(auto_now_add=True)
    foreign_key = models.ForeignKey(OtherModel, related_name="ref1")
    many_to_many = models.ManyToManyField(OtherModel, related_name="ref2")

现在调用SomeModel.objects.first()将会得到以下输出:

{'auto_now_add': datetime.datetime(2018, 12, 20, 21, 34, 29, 494827, tzinfo=<UTC>),
 'foreign_key': 2,
 'id': 1,
 'many_to_many': [2],
 'normal_value': 1,
 'readonly_value': 2}

4
谢谢您的提问!为了解答第5个解决方案(以及奖励部分)中的isinstance测试,建议修改为if f.many_to_many - dhobbs
1
@dhobbs 我的代码是基于 Django 的 model_to_dict 代码模型编写的,该代码使用了 isinstance。我不确定他们为什么做出这个选择,但可能有一个很好的理由(例如在后续版本中引入了 many_to_many 属性)。 - Zags
1
我想知道这些方法将如何处理带注释/聚合字段? - mehmet
2
有一件事情是检查 get_FOO_display,并返回该值,而不是实际存在的任何值。 - Bobort
1
关于 model_to_dict 的一个重要事项:它不仅仅呈现对象中已有的数据。它可能会进行数据库查询。如果您的对象是使用 QuerySet.raw() 获取的部分对象,则它将获取您故意省略的所有其他列。 - Adamantish
显示剩余7条评论

14
我找到了一种不错的方法来得到结果:
假设你有一个模型对象o
只需调用:
type(o).objects.filter(pk=o.pk).values().first()

25
这只是我回答中的第四个选项。 - Zags

12

最简单的方法,

  1. 如果您的查询是Model.Objects.get():

    get()将返回单个实例,因此您可以直接使用您的实例中的__dict__

    model_dict = Model.Objects.get().__dict__

  2. 对于filter()/all():

    all()/filter()将返回实例列表,因此您可以使用values()来获取对象列表。

    model_values = Model.Objects.all().values()


1
model_dict = Model.Objects.get().__dict__ 给出一个键名为 _state 的项和其余的项。 - ruthuparna k

9

只需使用 vars(obj) 命令,就可以列出对象的全部值。

>>> obj_attrs = vars(obj)
>>> obj_attrs
 {'_file_data_cache': <FileData: Data>,
  '_state': <django.db.models.base.ModelState at 0x7f5c6733bad0>,
  'aggregator_id': 24,
  'amount': 5.0,
  'biller_id': 23,
  'datetime': datetime.datetime(2018, 1, 31, 18, 43, 58, 933277, tzinfo=<UTC>),
  'file_data_id': 797719,
 }

你可以添加这个。
>>> keys = obj_attrs.keys()
>>> temp = [obj_attrs.pop(key) if key.startswith('_') else None for key in keys]
>>> del temp
>>> obj_attrs
   {
    'aggregator_id': 24,
    'amount': 5.0,
    'biller_id': 23,
    'datetime': datetime.datetime(2018, 1, 31, 18, 43, 58, 933277, tzinfo=<UTC>),
    'file_data_id': 797719,
   }

9

更新

@zags发布的新聚合答案比我的更完整、更优雅。请参考那个答案。

原始内容

如果您愿意像@karthiker建议的那样定义自己的to_dict方法,那么这只是一个集合问题。

>>># Returns a set of all keys excluding editable = False keys
>>>dict = model_to_dict(instance)
>>>dict

{u'id': 1L, 'reference1': 1L, 'reference2': [1L], 'value': 1}


>>># Returns a set of editable = False keys, misnamed foreign keys, and normal keys
>>>otherDict = SomeModel.objects.filter(id=instance.id).values()[0]
>>>otherDict

{'created': datetime.datetime(2014, 2, 21, 4, 38, 51, tzinfo=<UTC>),
 u'id': 1L,
 'reference1_id': 1L,
 'value': 1L,
 'value2': 2L}

我们需要从otherDict中删除错误标记的外键。
为此,我们可以使用循环创建一个新字典,其中包含除下划线项以外的每个项。或者,为节省时间,我们可以将这些项添加到原始dict中,因为字典在内部只是集合。
>>>for item in otherDict.items():
...    if "_" not in item[0]:
...            dict.update({item[0]:item[1]})
...
>>>

因此,我们得到以下的字典:
>>>dict
{'created': datetime.datetime(2014, 2, 21, 4, 38, 51, tzinfo=<UTC>),
 u'id': 1L,
 'reference1': 1L,
 'reference2': [1L],
 'value': 1,
 'value2': 2L}

然后你只需返回它。

不足之处在于,您无法在editable=false字段名称中使用下划线。好的一面是,这将适用于任何一组字段,其中用户制作的字段不包含下划线。

这不是最好的做法,但它可以作为临时解决方案,直到找到更直接的方法。

对于下面的示例,dict将基于model_to_dict形成,而otherDict将通过filter的values方法形成。我本来会用模型自己来完成这个操作的,但我无法让我的机器接受otherModel。

>>> import datetime
>>> dict = {u'id': 1, 'reference1': 1, 'reference2': [1], 'value': 1}
>>> otherDict = {'created': datetime.datetime(2014, 2, 21, 4, 38, 51), u'id': 1, 'reference1_id': 1, 'value': 1, 'value2': 2}
>>> for item in otherDict.items():
...     if "_" not in item[0]:
...             dict.update({item[0]:item[1]})
...
>>> dict
{'reference1': 1, 'created': datetime.datetime(2014, 2, 21, 4, 38, 51), 'value2': 2, 'value': 1, 'id': 1, 'reference2': [1]}
>>>

我希望这能给你提供一个大致的答案,希望对你有所帮助。

1
不确定您在这里尝试使用're'做什么。如果是为了过滤掉带有下划线的键,那么这既不是正确的代码,也不是正确的行为。re.match("_", "reference1_id")返回None,数据库中的合法列名可能包含下划线。 - Zags
re.match("_", "reference1_id") 返回了 None,应该是:re.match(".*_.*", "reference1_id")。 - Gadget
我做了一些更改,删除了不好的示例并包含了一个更好的示例。我还更改了一些内容,以表达这将是所有模型子集的临时解决方案。我不知道对于editable=false字段中带有下划线的模型,你会怎么做。我只是试图提供一些东西,让你能够在更加正式的解决方案到来之前使用。 - Gadget
在这种情况下,也许可以使用字符串中的“_”,而不是使用re。 - Zags
是的,这将是一个更容易的方法。我没有想到可以这样使用它,但现在它完全有道理了。我已经改变了答案,使用in代替re - Gadget

8

@Zags的解决方案非常棒!

不过,我想补充一下,为了使其符合JSON格式,需要对日期字段添加条件。

额外奖励

如果你想要一个Django模型,它在Python命令行中显示更好,那么可以让你的模型子类化以下内容:

from django.db import models
from django.db.models.fields.related import ManyToManyField

class PrintableModel(models.Model):
    def __repr__(self):
        return str(self.to_dict())

    def to_dict(self):
        opts = self._meta
        data = {}
        for f in opts.concrete_fields + opts.many_to_many:
            if isinstance(f, ManyToManyField):
                if self.pk is None:
                    data[f.name] = []
                else:
                    data[f.name] = list(f.value_from_object(self).values_list('pk', flat=True))
            elif isinstance(f, DateTimeField):
                if f.value_from_object(self) is not None:
                    data[f.name] = f.value_from_object(self).timestamp()
            else:
                data[f.name] = None
            else:
                data[f.name] = f.value_from_object(self)
        return data

    class Meta:
        abstract = True

因此,例如,如果我们将模型定义为以下内容:
class OtherModel(PrintableModel): pass

class SomeModel(PrintableModel):
    value = models.IntegerField()
    value2 = models.IntegerField(editable=False)
    created = models.DateTimeField(auto_now_add=True)
    reference1 = models.ForeignKey(OtherModel, related_name="ref1")
    reference2 = models.ManyToManyField(OtherModel, related_name="ref2")

现在调用SomeModel.objects.first()会得到以下输出:

{'created': 1426552454.926738,
'value': 1, 'value2': 2, 'reference1': 1, u'id': 1, 'reference2': [1]}

1
如果你想要进行JSON格式的转换,你可以考虑使用Django Rest Framework或者像这个链接中提到的方法:https://dev59.com/rGct5IYBdhLWcg3wuPq6#22238613 - Zags
当然可以!但是对你的代码进行这个小改变会带来很大的便利! - dfcoelho

5

我在将Django网站转换为API的过程中,遇到了这个问题。通常情况下,Django从数据库返回三种类型的对象,包括查询集、模型实例和分页器对象。在我的情况下,这些都需要进行转换。

查询集

在Django中,查询集类似于一个模型对象列表。以下是将其转换为字典的代码。

model_data=Model.object.all()# This returns a queryset object
model_to_dict=[model for model in model_data.values()]
return Response(model_to_dict,status=status.HTTP_200_OK)

模型实例

模型实例是一个模型的单个对象。

model_instance=Model.objects.get(pk=1)# This will return only a single model object
model_to_dict=model_to_dict(model_instance)
return Response(model_to_dict,status=status.HTTP_200_OK)

分页器对象

分页器对象是一个包含特定页面模型对象的对象。

model_queryset=Model.objects.all()
paginator = Paginator(model_queryset, 10)
try:
    selected_results = paginator.page(page)
except Exception:
    selected_results=result
paginator_to_dict=list(selected_results.object_list.values())
return Response(selected_results,status=status.HTTP_200_OK)

至少这就是我解决问题的方法。


5
更简单的方法是使用基础Python中的pprint
import pprint
item = MyDjangoModel.objects.get(name = 'foo')
pprint.pprint(item.__dict__, indent = 4)

这将输出类似于json.dumps(..., indent = 4)的结果,但它正确处理嵌入在模型实例中的奇怪数据类型,例如ModelState和UUID 等。

已在Python 3.7上进行测试


2

这里有很多有趣的解决方案。我的解决方案是在我的模型中添加一个as_dict方法,使用字典推导式。

def as_dict(self):
    return dict((f.name, getattr(self, f.name)) for f in self._meta.fields)

作为一种额外的解决方案,结合查询的列表推导式可以很好地实现将模型导出到另一个库中。例如,将您的模型转储到Pandas数据框中:
pandas_awesomeness = pd.DataFrame([m.as_dict() for m in SomeModel.objects.all()])

1
这对于像字符串和整数这样的值字段运行良好,但是在外键甚至多对多字段方面会遇到一些问题。 - Zags
非常好的观点!特别是对于多对多关系。人们希望加入一些条件来适当处理这些情况,或将此解决方案限制为简单模型。谢谢。 - t1m0

2

抱歉,我无意发表评论。

好的,在这个问题上并不完全依赖于类型。如果我误解了原始问题,请原谅。如果您创建serliazers.py,则在其中创建具有元类的类。

Class MyModelSerializer(serializers.ModelSerializer):
    class Meta:
        model = modelName
        fields =('csv','of','fields')

然后,在视图类中获取数据后,您可以进行以下操作:
model_data - Model.objects.filter(...)
serializer = MyModelSerializer(model_data, many=True)
return Response({'data': serilaizer.data}, status=status.HTTP_200_OK)

这在许多地方都是我所拥有的,它通过JSONRenderer返回漂亮的JSON。

正如我所说,这要归功于DjangoRestFramework,因此值得研究一下。


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