web-dev-qa-db-fra.com

Convertir Django Objet Model à dicter avec tous les champs intacts

Comment convertir un objet Django Model en un dict avec tous de ses champs? Tous incluent idéalement des clés étrangères et des champs avec editable = False.

Laissez-moi élaborer. Disons que j'ai un modèle Django comme celui-ci:

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

Dans le terminal, j'ai fait ce qui suit:

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

Je veux convertir ceci dans le dictionnaire suivant:

{'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}

Questions avec des réponses insatisfaisantes:

Django: Conversion d'un ensemble complet d'objets du modèle en un seul dictionnaire

Comment puis-je transformer Django objets de modèle en dictionnaire tout en conservant leurs clés étrangères?

198
Zags

Il existe de nombreuses façons de convertir une instance en dictionnaire, avec différents degrés de traitement des majuscules et de proximité avec le résultat souhaité.


1. instance.__dict__

instance.__dict__

qui retourne

{'_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}

Ceci est de loin le plus simple, mais il manque many_to_many, foreign_key est mal nommé, et il contient deux éléments supplémentaires indésirables.


2. model_to_dict

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

qui retourne

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

C'est le seul avec many_to_many, mais il manque les champs non modifiables.


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

qui retourne

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

Ceci est strictement pire que l'invocation standard model_to_dict.


4. query_set.values()

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

qui retourne

{'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}

C'est la même sortie que instance.__dict__ mais sans les champs supplémentaires. foreign_key_id est toujours faux et many_to_many est toujours manquant.


5. Fonction personnalisée

Le code pour model_to_dict de Django avait la majeure partie de la réponse. Il a explicitement supprimé les champs non modifiables. Par conséquent, si vous supprimez cette vérification et obtenez les ID de clés étrangères pour plusieurs champs, le code suivant se comporte comme souhaité:

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

Bien que ceci soit l'option la plus compliquée, appeler to_dict(instance) nous donne exactement le résultat souhaité:

{'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. Utiliser les sérialiseurs

Django Rest Framework 'ModelSerialzer vous permet de créer un sérialiseur automatiquement à partir d'un modèle.

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

SomeModelSerializer(instance).data

résultats

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

C'est presque aussi bon que la fonction personnalisée, mais auto_now_add est une chaîne au lieu d'un objet datetime.


Bonus Round: meilleure impression de modèle

Si vous voulez un modèle Django ayant un meilleur affichage de ligne de commande python, faites en sorte que vos modèles soient de la classe enfant suivante:

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

Ainsi, par exemple, si nous définissons nos modèles en tant que tels:

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

L'appel de SomeModel.objects.first() donne maintenant une sortie comme celle-ci:

{'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}
382
Zags

J'ai trouvé une solution soignée pour arriver à un résultat:

Supposons que vous ayez un objet modèle o:

Il suffit d'appeler:

type(o).objects.filter(pk=o.pk).values().first()
10
Alfred Huang

La solution @Zags était magnifique!

J'ajouterais cependant une condition pour datefields afin de rendre JSON convivial.

Bonus Round

Si vous voulez un modèle Django ayant un meilleur affichage de ligne de commande python, faites en sorte que la classe enfant de votre modèle soit comme suit:

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

Ainsi, par exemple, si nous définissons nos modèles en tant que tels:

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

L'appel de SomeModel.objects.first() donne maintenant une sortie comme ceci:

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

Manière la plus simple,

  1. Si votre requête est Model.Objects.get ():

    get () retournera une instance unique afin que vous puissiez directement utiliser __dict__ de votre instance

    model_dict = Model.Objects.get().__dict__

  2. pour filter ()/all ():

    all ()/filter () retournera la liste des instances afin que vous puissiez utiliser values() pour obtenir la liste des objets.

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

3

juste vars(obj), il indiquera les valeurs entières de l'objet

{'_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,
 }
2
A.Raouf

Si vous êtes prêt à définir votre propre méthode to_dict, comme l'a suggéré @karthiker, le problème se résume à un ensemble.

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

Nous devons supprimer les clés étrangères mal étiquetées de otherDict.

Pour ce faire, nous pouvons utiliser une boucle qui crée un nouveau dictionnaire contenant tous les éléments sauf ceux avec des traits de soulignement. Ou, pour gagner du temps, nous pouvons simplement ajouter ceux-ci à l'original dict puisque les dictionnaires sont simplement placés sous le capot.

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

Il nous reste donc ce qui suit dict:

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

Et vous venez de retourner ça.

En revanche, vous ne pouvez pas utiliser de traits de soulignement dans les noms de champs modifiables = faux. En revanche, cela fonctionnera pour tout ensemble de champs où les champs créés par l'utilisateur ne contiennent pas de trait de soulignement.

--MODIFIER--

Ce n'est pas la meilleure façon de le faire, mais cela pourrait fonctionner comme une solution temporaire jusqu'à ce qu'une méthode plus directe soit trouvée.

Pour l'exemple ci-dessous, dict serait formé en fonction de model_to_dict et otherDict serait formé par la méthode values ​​de filter. J'aurais fait cela avec les modèles eux-mêmes, mais je ne parviens pas à faire accepter ma machine par 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]}
>>>

Cela devrait vous donner une idée approximative de la réponse à votre question, j'espère.

2
Gadget

(ne voulait pas faire le commentaire)

Ok, ça ne dépend pas vraiment des types de cette façon. J'ai peut-être mal compris la question initiale, alors pardonnez-moi si tel est le cas. Si vous créez serliazers.py, vous créez des classes qui ont des méta-classes.

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

Ensuite, lorsque vous obtenez les données dans la classe de vue, vous pouvez:

model_data - Model.objects.filter(...)
serializer = MyModelSerializer(model_data, many=True)
return Response({'data': serilaizer.data}, status=status.HTTP_200_OK)

C’est à peu près ce que j’ai dans une variété d’endroits et cela renvoie Nice JSON via JSONRenderer.

Comme je l'ai dit, ceci est une gracieuseté de DjangoRestFramework, il est donc intéressant de se pencher sur la question.

1
nathj07

La meilleure solution que vous ayez jamais vue.

Convertir l’instance Django.db.models.Model et tous les champs de fonction ForeignKey, ManyToManyField et @Property associés en 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: https://stackoverflow.com/questions/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
Shuge Lee

Peut-être que cela vous aide. Puisse cette relation ne pas dissimuler plusieurs relations à plusieurs, mais elle est très pratique lorsque vous souhaitez envoyer votre modèle au format 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
Pjl

Beaucoup de solutions intéressantes ici. Ma solution consistait à ajouter une méthode as_dict à mon modèle avec une compréhension dict.

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

En prime, cette solution, associée à une compréhension de liste sur une requête, constitue une solution intéressante si vous souhaitez exporter vos modèles dans une autre bibliothèque. Par exemple, vider vos modèles dans un pandas dataframe:

pandas_awesomeness = pd.DataFrame([m.as_dict() for m in SomeModel.objects.all()])
0
t1m0

La réponse de @zags est complète et devrait suffire mais la méthode # 5 (qui est la meilleure IMO) renvoie une erreur, j'ai donc amélioré la fonction d'assistance.

Comme l'OP a demandé de convertir les champs many_to_many en liste de clés primaires plutôt qu'en liste d'objets, j'ai amélioré la fonction afin que la valeur renvoyée soit désormais JSON sérialisable - en convertissant les objets datetime en str et many_to_many objets dans une liste d'identifiants.

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
0
Armin Hemati