Преобразовать объект модели Django, чтобы диктовать все поля без изменений


258

Как преобразовать объект модели Django в dict со всеми его полями? Все в идеале включает в себя внешние ключи и поля с 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 Model в словарь и сохранить их внешние ключи?


1
Вы можете объявить вызываемый метод to_dictи обрабатывать его так, как хотите.
karthikr

@ kartikr да, я мог. Вопрос в том, как создать такой метод. Создание словаря вручную из всех полей модели не является подходящим ответом.
Загс

Я бы использовал существующую библиотеку ReST, такую ​​как Django Rest Framework, Tastypie или Piston, поскольку все они предоставляют механизмы для преобразования экземпляров модели django в примитивы для сериализации. Если вам более любопытно, как, вы можете просмотреть их код, но в основном он проходит по _metaопределениям модели, чтобы найти поля, связанные с моделью, и получить их значения в экземпляре.
Кевин Стоун

Ответы:


526

Есть много способов преобразовать экземпляр в словарь, с различной степенью обработки угловых случаев и приближением к желаемому результату.


1. instance.__dict__

instance.__dict__

который возвращается

{'_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_many, foreign_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имел большую часть ответа. Он явно удалил нередактируемые поля, поэтому удаление этой проверки и получение идентификаторов внешних ключей для многих и многих полей приводит к следующему коду, который ведет себя как нужно:

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. Используйте Сериализаторы

ModelSerialzer в Django Rest Framework позволяет автоматически создавать сериализатор из модели.

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.


Бонус раунд: лучшая модель печати

Если вам нужна модель django с лучшим отображением командной строки на python, сделайте ваши модели дочерними по классу следующими:

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}

2
Спасибо за этот ответ! Вы можете изменить isinstanceтест в решении № 5 (и бонус) на if f.many_to_many.
dhobbs

1
@dhobbs Я смоделировал этот код из model_to_dictкода Джанго , который использует isinstance. Я не уверен, почему они сделали этот выбор, но для этого может быть веская причина (например, many_to_manyсвойство, появившееся в более поздней версии)
Zags

это также вернет @propertyполя?
Angrysumit

1
Интересно, как бы эти методы обрабатывали аннотированные / агрегированные поля?
Мехмет

Что-то, что я делаю, это проверяю get_FOO_display и возвращаю это значение вместо того, чтобы какое-либо значение могло быть на самом деле.
Боборт

9

Я нашел аккуратное решение, чтобы получить результат:

Предположим, у вас есть модельный объект o:

Просто позвони:

type(o).objects.filter(pk=o.pk).values().first()

10
Это просто вариант № 4 в моем ответе
Загс

7

Решение @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]}

Если вы хотите конвертировать в и из JSON, вы должны заглянуть в Django Rest Framework или использовать что-то вроде этого: stackoverflow.com/a/22238613/2800876
Zags

Конечно! Но это небольшое изменение в вашем коде добавляет много удобства!
Диего Фрейтас Коэльо

4

Самый простой способ,

  1. Если ваш запрос - Model.Objects.get ():

    получить () возвращает один экземпляр , так что вы можете направить использование __dict__от экземпляра

    model_dict = Model.Objects.get().__dict__

  2. для фильтра () / все ():

    all () / filter () вернет список экземпляров, чтобы вы могли использовать их values()для получения списка объектов.

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


4

просто 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,
   }

3

Обновить

Новый обобщенный ответ, опубликованный @zags, является более полным и элегантным, чем мой собственный. Пожалуйста, обратитесь к этому ответу вместо.

оригинал

Если вы хотите определить свой собственный метод to_dict, как предложил @karthiker, то это сводит эту проблему к задаче множеств.

>>># 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 .

Для этого мы можем использовать цикл, который создает новый словарь, в котором есть все элементы, кроме тех, в которых есть подчеркивания. Или, чтобы сэкономить время, мы можем просто добавить их в исходный текст, так как словари просто находятся под капотом.

>>>for item in otherDict.items():
...    if "_" not in item[0]:
...            dict.update({item[0]:item[1]})
...
>>>

Таким образом , мы остались со следующим Dict :

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

И вы просто верните это.

С другой стороны, вы не можете использовать подчеркивания в ваших редактируемых = ложных имен полей. С другой стороны, это будет работать для любого набора полей, где пользовательские поля не содержат подчеркивания.

Это не лучший способ сделать это, но он может работать как временное решение, пока не будет найден более прямой метод.

Для примера ниже, dict будет сформирован на основе model_to_dict, а otherDict будет сформирован методом значений фильтра. Я бы сделал это с самими моделями, но я не могу заставить свою машину принять другую модель.

>>> 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и допустимые столбцы в базе данных могут иметь подчеркивания в своих именах.
Загс

re.match ("_", "reference1_id") действительно возвращает None, это должно было быть: re.match (". * _. *", "reference1_id")
гаджет

Я внес некоторые изменения, чтобы убрать плохой пример и включить лучший. Я также изменил некоторые вещи, чтобы выразить, что это будет временное решение для подмножества всех моделей. Я понятия не имею, что бы вы сделали для моделей с подчеркиванием в своих editable=falseполях. Я просто пытался предоставить что-то, с чем вы могли бы работать, пока не было предоставлено более каноническое решение.
Гаджет

Может быть, использовать, "_" in stringа не reв этом случае.
Загс

Да, это был бы намного более легкий способ сделать это. Мне не приходило в голову использовать его таким образом, но теперь это имеет смысл. Я изменил ответ, чтобы использовать inвместо re.
гаджет

2

Здесь много интересных решений. Мое решение состояло в том, чтобы добавить метод as_dict к моей модели с пониманием 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

1

(не хотел комментировать)

Хорошо, это на самом деле не зависит от типов. Возможно, я неправильно понял первоначальный вопрос здесь, так что простите, если это так. Если вы создадите 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)

Это довольно много, что я имею в разных местах, и он возвращает хороший JSON через JSONRenderer.

Как я уже сказал, это любезно предоставлено DjangoRestFramework, так что стоит посмотреть на это.


1

Самый простой способ - просто использовать pprint, который есть в базовом Python

import pprint
item = MyDjangoModel.objects.get(name = 'foo')
pprint.pprint(item.__dict__, indent = 4)

Это дает вывод, который выглядит примерно так, json.dumps(..., indent = 4)но он правильно обрабатывает странные типы данных, которые могут быть встроены в экземпляр вашей модели, такие как ModelStateи UUIDт. Д.

Проверено на Python 3.7


0

Может быть, это поможет вам. Пусть это не скрывает многие отношения, но очень удобно, когда вы хотите отправить свою модель в формате json.

def serial_model(modelobj):
  opts = modelobj._meta.fields
  modeldict = model_to_dict(modelobj)
  for m in opts:
    if m.is_relation:
        foreignkey = getattr(modelobj, m.name)
        if foreignkey:
            try:
                modeldict[m.name] = serial_model(foreignkey)
            except:
                pass
  return modeldict

0

Лучшее решение, которое вы когда-либо видели.

Преобразуйте экземпляр django.db.models.Model и все связанные поля функций ForeignKey, ManyToManyField и @Property в dict.

"""
Convert django.db.models.Model instance and all related ForeignKey, ManyToManyField and @property function fields into dict.
Usage:
    class MyDjangoModel(... PrintableModel):
        to_dict_fields = (...)
        to_dict_exclude = (...)
        ...
    a_dict = [inst.to_dict(fields=..., exclude=...) for inst in MyDjangoModel.objects.all()]
"""
import typing

import django.core.exceptions
import django.db.models
import django.forms.models


def get_decorators_dir(cls, exclude: typing.Optional[set]=None) -> set:
    """
    Ref: /programming/4930414/how-can-i-introspect-properties-and-model-fields-in-django
    :param exclude: set or None
    :param cls:
    :return: a set of decorators
    """
    default_exclude = {"pk", "objects"}
    if not exclude:
        exclude = default_exclude
    else:
        exclude = exclude.union(default_exclude)

    return set([name for name in dir(cls) if name not in exclude and isinstance(getattr(cls, name), property)])


class PrintableModel(django.db.models.Model):

    class Meta:
        abstract = True

    def __repr__(self):
        return str(self.to_dict())

    def to_dict(self, fields: typing.Optional[typing.Iterable]=None, exclude: typing.Optional[typing.Iterable]=None):
        opts = self._meta
        data = {}

        # support fields filters and excludes
        if not fields:
            fields = set()
        else:
            fields = set(fields)
        default_fields = getattr(self, "to_dict_fields", set())
        fields = fields.union(default_fields)

        if not exclude:
            exclude = set()
        else:
            exclude = set(exclude)
        default_exclude = getattr(self, "to_dict_exclude", set())
        exclude = exclude.union(default_exclude)

        # support syntax "field__childField__..."
        self_fields = set()
        child_fields = dict()
        if fields:
            for i in fields:
                splits = i.split("__")
                if len(splits) == 1:
                    self_fields.add(splits[0])
                else:
                    self_fields.add(splits[0])

                    field_name = splits[0]
                    child_fields.setdefault(field_name, set())
                    child_fields[field_name].add("__".join(splits[1:]))

        self_exclude = set()
        child_exclude = dict()
        if exclude:
            for i in exclude:
                splits = i.split("__")
                if len(splits) == 1:
                    self_exclude.add(splits[0])
                else:
                    field_name = splits[0]
                    if field_name not in child_exclude:
                        child_exclude[field_name] = set()
                    child_exclude[field_name].add("__".join(splits[1:]))

        for f in opts.concrete_fields + opts.many_to_many:
            if self_fields and f.name not in self_fields:
                continue
            if self_exclude and f.name in self_exclude:
                continue

            if isinstance(f, django.db.models.ManyToManyField):
                if self.pk is None:
                    data[f.name] = []
                else:
                    result = []
                    m2m_inst = f.value_from_object(self)
                    for obj in m2m_inst:
                        if isinstance(PrintableModel, obj) and hasattr(obj, "to_dict"):
                            d = obj.to_dict(
                                fields=child_fields.get(f.name),
                                exclude=child_exclude.get(f.name),
                            )
                        else:
                            d = django.forms.models.model_to_dict(
                                obj,
                                fields=child_fields.get(f.name),
                                exclude=child_exclude.get(f.name)
                            )
                        result.append(d)
                    data[f.name] = result
            elif isinstance(f, django.db.models.ForeignKey):
                if self.pk is None:
                    data[f.name] = []
                else:
                    data[f.name] = None
                    try:
                        foreign_inst = getattr(self, f.name)
                    except django.core.exceptions.ObjectDoesNotExist:
                        pass
                    else:
                        if isinstance(foreign_inst, PrintableModel) and hasattr(foreign_inst, "to_dict"):
                            data[f.name] = foreign_inst.to_dict(
                                fields=child_fields.get(f.name),
                                exclude=child_exclude.get(f.name)
                            )
                        elif foreign_inst is not None:
                            data[f.name] = django.forms.models.model_to_dict(
                                foreign_inst,
                                fields=child_fields.get(f.name),
                                exclude=child_exclude.get(f.name),
                            )

            elif isinstance(f, (django.db.models.DateTimeField, django.db.models.DateField)):
                v = f.value_from_object(self)
                if v is not None:
                    data[f.name] = v.isoformat()
                else:
                    data[f.name] = None
            else:
                data[f.name] = f.value_from_object(self)

        # support @property decorator functions
        decorator_names = get_decorators_dir(self.__class__)
        for name in decorator_names:
            if self_fields and name not in self_fields:
                continue
            if self_exclude and name in self_exclude:
                continue

            value = getattr(self, name)
            if isinstance(value, PrintableModel) and hasattr(value, "to_dict"):
                data[name] = value.to_dict(
                    fields=child_fields.get(name),
                    exclude=child_exclude.get(name)
                )
            elif hasattr(value, "_meta"):
                # make sure it is a instance of django.db.models.fields.Field
                data[name] = django.forms.models.model_to_dict(
                    value,
                    fields=child_fields.get(name),
                    exclude=child_exclude.get(name),
                )
            elif isinstance(value, (set, )):
                data[name] = list(value)
            else:
                data[name] = value

        return data

https://gist.github.com/shuge/f543dc2094a3183f69488df2bfb51a52


0

Ответ от @zags является исчерпывающим и должен быть достаточным, но метод # 5 (который является лучшим из IMO) выдает ошибку, поэтому я улучшил вспомогательную функцию.

Поскольку OP запрашивал преобразование many_to_manyполей в список первичных ключей, а не в список объектов, я усовершенствовал функцию, чтобы возвращаемое значение теперь стало сериализуемым в JSON - путем преобразования datetimeобъектов strи many_to_manyобъектов в список идентификаторов.

import datetime

def ModelToDict(instance):
    '''
    Returns a dictionary object containing complete field-value pairs of the given instance

    Convertion rules:

        datetime.date --> str
        many_to_many --> list of id's

    '''

    concrete_fields = instance._meta.concrete_fields
    m2m_fields = instance._meta.many_to_many
    data = {}

    for field in concrete_fields:
        key = field.name
        value = field.value_from_object(instance)
        if type(value) == datetime.datetime:
            value = str(field.value_from_object(instance))
        data[key] = value

    for field in m2m_fields:
        key = field.name
        value = field.value_from_object(instance)
        data[key] = [rel.id for rel in value]

    return data

Какую ошибку вы получаете? Я рад обновить ответ
Загс

В настоящее время функциональность циклов concrete_fieldsи m2m_fieldsвыглядит одинаково, поэтому предполагается, что m2m_fieldsцикл имеет неправильную реализацию здесь.
Даниэль Химмельштейн

@ Загс ошибка, AttributeError: 'list' object has no attribute 'values_list' которую я не мог найти причину этого. Я использую Django 2.1.1
Армин Хемати Ник

@ daniel-himmelstein спасибо за указание, я исправил код. причина для идентичных циклов была из-за выполнения различных операций в моем локальном коде, и я забыл оптимизировать его для ответа SO.
Армин Хемати Ник

@ArminHemati Django изменил осуществление field.value_from_objectи в результате, из model_to_dict. Я обновил раздел 5 моего ответа, чтобы отразить это.
Загс
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.