Dynamic types in django 1.0

tags: programming (September 12, 2008)

This was originally posted to the django user group. Malcolm Tredinnick and Carl Meyer provided valuable improvements.

The problem: django models behave like C++ objects ...

I recently played around with model inheritance in django 1.0. In particular, I needed something that might be called dynamic casts (with reference to C++). Consider the following example type hierarchy:


    MediaObject
    |
    +-- ImageObject
    |   |
    |   +-- PngObject
    |   +-- GifObject
    |
    +-- AudioObject
        |
        +-- Mp3Object
        +-- WavObject

Imagine one wants to retrieve a set of MediaObjects from the database and treat them with respect to their actual type. For example, one might want to display them using specialized templates, like


    {% for object in objects %}
        {% include object.template %}
    {% endfor %}

(or using some more sophisticated template dispatcher).

The problem is that any subtypes of MediaObject are retrieved as plain MediaObjects from the database:


    obj = Mp3Object()
    obj.save()

    obj = get_objet_or_404(MediaObject,id=1)  # obj is now of type MediaObject

What happened to python's dynamic typing which we all love? When storing models in the database, they behave much more like C++ objects than like python objects. Neither can one pass the correct type to the query (as in get_object_or_404(Mp3Object,id=1)) or access the child object via an attribute (as in obj.mp3object), because we simply do not know the type of the object we are dealing with. It also does not help to define the base class abstract because than there would exist no database table MediaObject at all.

The solution: make them more pythonic!

The solution comes in form of the contenttypes application that is part of django. ContentTypes are wrappers for model classes: an instance of ContentType represents a class that is derived from django.db.model.Models.

We use contenttypes as follows: the base class MediaObject holds a field final_type which is the foreign key of a ContentType. We use this to save the actual instance type when saving an instance to the database. This is most easily done with the method: ContentType.object.get_for_model() to which we simply pass the actual model class type(self). Later (in the method dynamic_cast) we use final_type.get_object_for_that_type() to retrieve the correctly typed object from the database.


    from django.contrib.contenttypes.models import ContentType

    class MediaObject(models.Model) :
        final_type = models.ForeignKey(ContentType)

        def save(self,*args,**kwargs) :
            if not self.id:
                self.final_type = ContentType.objects.get_for_model(type(self))
            super(MediaObject,self).save(*args,**kwargs)

        def dynamic_cast(self) :
            return self.final_type.get_object_for_this_type(id=self.id)

    class Mp3Object(MediaObject) :
        pass

Our previous example now works with a minimal change (i.e. an explicit cast):


    obj = Mp3Object()
    obj.save()

    obj = get_objet_or_404(MediaObject,id=1).dynamic_cast()  # obj is now of type Mp3Object

Note that this hits the database twice. Once for retrieving the MediaObject and once for the derived Mp3Object.