Saturday, April 2, 2011

Polymorphism in django models

Django models are very powerful ORM, but they do not play nice with any kind of polymorphism out of the box. The problem cames when you want to add a model that have some generic properties set, but it can differ in implementations of some methods (because of theirs type/specialization).

Lets consider follwoing example:
class Animal(models.Model):
 name = models.CharField(max_length=15)
 
 def __unicode__(self):
  return self.name
 def make_sound(self):
  print "Some sound"

I would like to store different types of Animals here: a dog, a cat, a cow. All I am interested with is theirs name, so I don't want to make a separate models for them. Also I would like to make querysets regardless of a kind of an animal - just from all of the animals I have.

>>> cat = Animal(name="Cattie").save()
>>> dog = Animal(name="Doggie").save()
>>> cow = Animal(name="Cowwie").save()
>>> Animal.objects.all()
[<Animal: Cattie>, <Animal: Doggie>, <Animal: Cowwie>]
>>> for a in Animal.objects.all():
...    a.make_sound()
... 
Some sound
Some sound
Some sound

Let's do some metaclass magic python programming.

class InheritanceMetaclass(ModelBase):
 def __call__(cls, *args, **kwargs):
  obj = super(InheritanceMetaclass, cls).__call__(*args, **kwargs)
  obj.__class__ = obj._get_class()
  return obj


class Animal(models.Model):
 __metaclass__ = InheritanceMetaclass

 TYPES=( ('AnimalDog', 'Dog'), 
   ('AnimalCat', 'Cat'), 
   ('AnimalCow', 'Cow'))

 name = models.CharField(max_length=15)
 type = models.CharField(max_length=15, choices=TYPES)
 
 def __unicode__(self):
  return self.name

 def make_sound(self):
  print "Some sound"

 def _get_class(self):
  if not self.type:
   return self.__class__
  else:
   return getattr(sys.modules[self.__module__], self.type)

class AnimalDog(Animal):
 def make_sound(self):
  print "Hauu!"
 class Meta:
  proxy = True

class AnimalCat(Animal):
 def make_sound(self):
  print "Meow!"
 class Meta:
  proxy = True

class AnimalCow(Animal):
 def make_sound(self):
  print "Mooo!"
 class Meta:
  proxy = True
  
  

Drop all from the database for previous example and create new models. Then try this:

>>> Animal(name='Cattie', type='AnimalCat').save()
>>> Animal(name='Doggie', type='AnimalDog').save()
>>> Animal(name='Cowwie', type='AnimalCow').save()
>>> Animal.objects.all()
[<AnimalCat: Cattie>, <AnimalDog: Doggie>, <AnimalCow: Cowwie>]
>>> for a in Animal.objects.all():
...    a.make_sound()
... 
Meow!
Hauu!
Mooo!

Background notes:

  • because we create proxy models, there is only one database table for the Animal,
  • every time you query for Animal object you get an object casted to appropriate class (not the Animal), but in this same time this will work flawless with django, because of duck typing. AnimalDog will behave exactly the same as parent Animal class in case of using it in django admin and so on,
  • we store in the attribute Animal.type a name of django class model name, 
  • we can easily extend our Animal types by adding new values to Animal.TYPES,
  • if you need real model inheritance (e.g. add custom model fields depending on a model type) use django model inheritance - this is not described here case,
  • you can also store type as Integer field, but then it needs some further hacking in _get_class() method, see also how to avoid hardcoding with Perfect choice.

No comments:

Post a Comment