¿Cómo accedo a las clases secundarias de un objeto en django sin saber el nombre de la clase secundaria?

81

En Django, cuando tiene una clase principal y varias clases secundarias que heredan de ella, normalmente accedería a un niño a través de parentclass.childclass1_set o parentclass.childclass2_set, pero ¿qué pasa si no sé el nombre de la clase secundaria específica que quiero?

¿Hay alguna manera de obtener los objetos relacionados en la dirección padre-> hijo sin saber el nombre de la clase secundaria?

Gabriel Hurley
fuente
79
@ S.Lott Este tipo de respuestas realmente envejecen. El hecho de que no pueda pensar en un caso de uso no significa que el autor de la pregunta no tenga uno. Si está utilizando subclases para cualquier tipo de comportamiento polimórfico (¿sabe, uno de los principales supuestos beneficios de la programación orientada a objetos?), Esta pregunta es una necesidad muy natural y obvia.
Carl Meyer
82
@ S.Lott En ese caso, siéntase libre de practicar algunas versiones no groseras, como "No estoy seguro de entender el contexto. ¿Podría explicar su caso de uso?"
Carl Meyer

Respuestas:

84

( Actualización : para Django 1.2 y versiones posteriores, que pueden seguir consultas select_related en relaciones OneToOneField inversas (y, por lo tanto, jerarquías de herencia descendentes), hay una técnica mejor disponible que no requiere el real_typecampo agregado en el modelo principal. Está disponible como InheritanceManager en el proyecto django-model-utils .)

La forma habitual de hacer esto es agregar una ForeignKey a ContentType en el modelo principal que almacena el tipo de contenido de la clase "hoja" adecuada. Sin esto, es posible que tenga que hacer una gran cantidad de consultas en tablas secundarias para encontrar la instancia, dependiendo de qué tan grande sea su árbol de herencia. Así es como lo hice en un proyecto:

from django.contrib.contenttypes.models import ContentType
from django.db import models

class InheritanceCastModel(models.Model):
    """
    An abstract base class that provides a ``real_type`` FK to ContentType.

    For use in trees of inherited models, to be able to downcast
    parent instances to their child types.

    """
    real_type = models.ForeignKey(ContentType, editable=False)

    def save(self, *args, **kwargs):
        if self._state.adding:
            self.real_type = self._get_real_type()
        super(InheritanceCastModel, self).save(*args, **kwargs)
    
    def _get_real_type(self):
        return ContentType.objects.get_for_model(type(self))
            
    def cast(self):
        return self.real_type.get_object_for_this_type(pk=self.pk)
    
    class Meta:
        abstract = True

Esto se implementa como una clase base abstracta para que sea reutilizable; también puede poner estos métodos y el FK directamente en la clase padre en su jerarquía de herencia particular.

Esta solución no funcionará si no puede modificar el modelo principal. En ese caso, está prácticamente atascado comprobando todas las subclases manualmente.

Carl Meyer
fuente
Gracias. Esto es hermoso y definitivamente me ahorró tiempo.
Spike
Esto ha sido muy útil, pero me pregunto por qué querría definir el FK con null = True. Copiamos el código más o menos tal como está, y nos picó un error que se habría detectado y resuelto fácilmente si el FK fuera obligatorio (tenga en cuenta también que el método cast () lo trata como obligatorio).
Shai Berger
1
@ShaiBerger Excelente pregunta. Más de tres años después, no tengo idea de por qué fue así originalmente :-) Editando para eliminar el nulo = Verdadero.
Carl Meyer
2
@sunprophit Necesitas volver a leer mi respuesta con más atención. Sí, por supuesto que puede hacerlo self.child_object()usando el real_typecampo (que está en el código anterior como cast()método), y eso solo tomará una consulta para una instancia. Pero si tiene un conjunto de consultas lleno de instancias, eso se convierte en N consultas. La única forma de obtener los datos de subclase para un conjunto de consultas completo de objetos en una sola consulta es usar las uniones que hace InheritanceManager.
Carl Meyer
1
Mi error de hecho, la culpa es mía. Gracias por notarlo y arreglarlo.
Antoine Pinsard
23

En Python, dada una clase X ("nuevo estilo"), puede obtener sus subclases (directas) con X.__subclasses__(), que devuelve una lista de objetos de clase. (Si desea "más descendientes", también tendrá que llamar __subclasses__a cada una de las subclases directas, etc., etc., si necesita ayuda sobre cómo hacerlo de manera efectiva en Python, ¡solo pregunte!).

Una vez que haya identificado de alguna manera una clase secundaria de interés (tal vez todas, si desea instancias de todas las subclases secundarias, etc.), getattr(parentclass,'%s_set' % childclass.__name__)debería ayudar (si el nombre de la clase secundaria es 'foo', esto es como acceder parentclass.foo_set, ni más, ni menos ). Nuevamente, si necesita aclaraciones o ejemplos, ¡pregunte!

Alex Martelli
fuente
1
Esta es una gran información (no sabía acerca de las subclases ), pero creo que la pregunta es mucho más específica sobre cómo los modelos de Django implementan la herencia. Si consulta la tabla "Parent", obtendrá una instancia de Parent. De hecho, puede ser una instancia de SomeChild, pero Django no se da cuenta de eso automáticamente (podría ser costoso). Puede acceder a la instancia de SomeChild a través de un atributo en la instancia de Parent, pero solo si ya sabe que es SomeChild lo que desea, en contraposición a alguna otra subclase de Parent.
Carl Meyer
2
Lo siento, no está claro cuando digo "puede ser de hecho una instancia de SomeChild". El objeto que tiene es una instancia de Parent en Python, pero puede tener una entrada relacionada en la tabla SomeChild, lo que significa que puede preferir trabajar con él como una instancia de SomeChild.
Carl Meyer
Es gracioso ... No necesitaba esta información específica en ese momento, pero estaba pensando en un tema diferente y resultó ser exactamente lo que necesitaba, ¡así que gracias de nuevo!
Gabriel Hurley
Esta es la respuesta REAL . Django es solo Python al final del día.
snakesNbronies
5

La solución de Carl es buena, aquí hay una forma de hacerlo manualmente si hay varias clases secundarias relacionadas:

def get_children(self):
    rel_objs = self._meta.get_all_related_objects()
    return [getattr(self, x.get_accessor_name()) for x in rel_objs if x.model != type(self)]

Utiliza una función fuera de _meta, que no se garantiza que sea estable a medida que django evoluciona, pero hace el truco y se puede usar sobre la marcha si es necesario.

Paul McMillan
fuente
5

Puedes usar django-polymorphic para eso.

Permite convertir automáticamente las clases derivadas a su tipo real. También proporciona soporte de administración de Django, manejo de consultas SQL más eficiente y modelo de proxy, soporte en línea y conjuntos de formularios.

El principio básico parece reinventarse muchas veces (incluido el de Wagtail .specifico los ejemplos descritos en esta publicación). Sin embargo, se necesita más esfuerzo para asegurarse de que no resulte en un problema de N-query, o se integre bien con el administrador, formsets / inlines o aplicaciones de terceros.

vdboor
fuente
1
Esto se ve bien y quiero probarlo. Al migrar, ¿es suficiente completar los campos polymorphic_ctype con los tipos de contenido apropiados (en una migración al sur)?
Josué
1
@joshua: sí, eso es exactamente lo único que tendrías que hacer.
vdboor
Puede desarrollar un poco su respuesta explicando la diferencia con el enfoque adoptado en django-model-utils (consulte la respuesta de Carl Meyer).
Ryne Everett
1
Las respuestas aceptadas están desactualizadas, esta es la mejor respuesta ahora.
Pierre.Sassoulas
2

Aquí está mi solución, nuevamente se usa, _metapor lo que no se garantiza que sea estable.

class Animal(models.model):
    name = models.CharField()
    number_legs = models.IntegerField()
    ...

    def get_child_animal(self):
        child_animal = None
        for r in self._meta.get_all_related_objects():
            if r.field.name == 'animal_ptr':
                child_animal = getattr(self, r.get_accessor_name())
        if not child_animal:
            raise Exception("No subclass, you shouldn't create Animals directly")
        return child_animal

class Dog(Animal):
    ...

for a in Animal.objects.all():
    a.get_child_animal() # returns the dog (or whatever) instance
cerberos
fuente
0

Puede lograr esto buscando todos los campos en el padre que son una instancia de django.db.models.fields.related.RelatedManager. De su ejemplo, parece que las clases secundarias de las que está hablando no son subclases. ¿Correcto?

Chirayu Patel
fuente
0

En esta publicación de blog se puede encontrar un enfoque alternativo que usa proxies . Como las otras soluciones, tiene sus beneficios y pasivos, que están muy bien puestos al final del post.

Filipe Correia
fuente