views:

826

answers:

3

I'm trying to find the actual class of a django-model object, when using model-inheritance.

Some code to describe the problem:

class Base(models.model):
    def basemethod(self):
        ...

class Child_1(Base):
    pass

class Child_2(Base):
    pass

If I create various objects of the two Child classes and the create a queryset containing them all:

Child_1().save()
Child_2().save()
(o1, o2) = Base.objects.all()

I want to determine if the object is of type Child_1 or Child_2 in basemethod, I can get to the child object via o1.child_1 and o2.child_2 but that reconquers knowledge about the childclasses in the baseclass.

I have come up with the following code:

def concrete_instance(self):
    instance = None
    for subclass in self._meta.get_all_related_objects():
        acc_name = subclass.get_accessor_name()
        try:
            instance = self.__getattribute__(acc_name)
            return instance
        except Exception, e:
            pass

But it feels brittle and I'm not sure of what happens when if I inherit in more levels.

A: 

It feels brittle because it is. (This is a reprint of an answer in a different context. See C++ casting programmatically : can it be done ?)

Read up on polymorphism. Almost every "dynamic cast" situation is an example of polymorphism struggling to be implemented.

Whatever decision you're making in the dynamic cast has already been made. Just delegate the real work to the subclasses.

You left out the most important part of your example. The useful, polymorphic work.

When you said "I want to determine if the object is of type Child_1 or Child_2..." you left out the "so I can make the object do aMethod() in a way that's unique to each subclass". That method is the useful work, and it should simply be a method of both subclasses.

class Base(models.model):
    def aMethod(self):
        # base class implementation.

class Child_1(Base):
    def aMethod(self):
        # Child_1 override of base class behavior.

class Child_2(Base):
    def aMethod(self):
        supert( Child_2, self ).aMethod() # Invoke the base class version
        # Child_2 extension to base class behavior.

Same method, multiple implementations. Never a need to "run-time type identification" or determining the concrete class.

S.Lott
But he doesn't have a reference to a Child model's instance. The Base.objects manager only returns instances of Base.
Daniel
@Daniel: Usually we process Child_1.objects.all() and Child_2.objects.all(); there's almost no call for the union of both children subtypes.
S.Lott
Generally, but that wasn't the question.
Daniel
@Daniel: this is a pretty common n00b question that indicates a failure to exploit polymorphism. A design change that eliminates the problem might be a more helpful answer.
S.Lott
+4  A: 

Django implements model inheritance with a OneToOneField between the parent model's table and the child model's table. When you do Base.object.all(), Django is querying just the Base table, and so has no way of knowing what the child table is. Therefore, unfortunately, it's not possible to go directly to the child model instance without additional queries.

This snippet shows a common method of adding a ContentType field to the base model:

from django.contrib.contenttypes.models import ContentType

class Base(models.Model):

    content_type = models.ForeignKey(ContentType,editable=False,null=True)

    def save(self):
        if(not self.content_type):
            self.content_type = ContentType.objects.get_for_model(self.__class__)
        self.save_base()

    def as_leaf_class(self):
        content_type = self.content_type
        model = content_type.model_class()
        if(model == Base):
            return self
        return model.objects.get(id=self.id)

You can then say if Base.content_type.model_class() to determine the type.

Here is another snippet that adds a custom manager into the mix.

As you can see, both of these solutions have the potential to be extremely expensive. If you have a large number of instances, using the as_leaf_class() method will require one query on each item.

Instead, if you have a known set of child models, simply query each model separately and aggregate the instances into one list.

Daniel
Thanks, that solved my problem
Mr Shark
A: 

Well... My problem was. In a view, I had this principal model, lets say "Big_Model" and there were some "Small_Model" related to "Big_Model". So when I wanted to retrieve all "Small_Model" related to a certain instance of "Big_Model" I did that **_set.all() stuff. But the point is that Small_Model has Child Classes and I wanted, in views.py, to get which child class was each of the Small_Model instances related to. My trick was to define boolean methods in model Small_Model like is_child_1() and is_child_2(). And when it is true, you apply the actual child pointer instead of the Small_Model pointer.

Ok... Thats not clear enough, still I dont have much time to write a good example, so i'll just copy-paste my case here:

class Cache(models.Model):
  valor = models.DecimalField(max_digits=9, decimal_places=2, blank= True, null= True)
  evento=models.ForeignKey(Evento)
  def __unicode__(self):
    return u'%s: %s' % (self.evento, self.valor)
  class Meta:
    verbose_name='Cachê'
    verbose_name_plural='Cachês'
  def is_cb(self):
    try:
      self.cache_bilheteria
      return True
    except self.DoesNotExist:
      return False
  def is_co(self):
    try:
      self.cache_outro
      return True
    except self.DoesNotExist:
      return False
Dhiana Deva