Right way to return proxy model instance from a base model instance in Django?

Question:

Say I have models:

class Animal(models.Model):
    type = models.CharField(max_length=255)

class Dog(Animal):
    def make_sound(self):
        print "Woof!"
    class Meta:
        proxy = True

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

Let’s say I want to do:

 animals = Animal.objects.all()
 for animal in animals:
     animal.make_sound()

I want to get back a series of Woofs and Meows. Clearly, I could just define a make_sound in the original model that forks based on animal_type, but then every time I add a new animal type (imagine they’re in different apps), I’d have to go in and edit that make_sound function. I’d rather just define proxy models and have them define the behavior themselves. From what I can tell, there’s no way of returning mixed Cat or Dog instances, but I figured maybe I could define a “get_proxy_model” method on the main class that returns a cat or a dog model.

Surely you could do this, and pass something like the primary key and then just do Cat.objects.get(pk = passed_in_primary_key). But that’d mean doing an extra query for data you already have which seems redundant. Is there any way to turn an animal into a cat or a dog instance in an efficient way? What’s the right way to do what I want to achieve?

Asked By: sotangochips

||

Answers:

You can perhaps make Django models polymorphic using the approach described here. That code is in early stages of development, I believe, but worth investigating.

Answered By: Vinay Sajip

the only way known to the human kind is to use Metaclass programming.

Here is short answer:

from django.db.models.base import ModelBase

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

class Animal(models.Model):
    __metaclass__ = InheritanceMetaclass
    type = models.CharField(max_length=255)
    object_class = models.CharField(max_length=20)

    def save(self, *args, **kwargs):
        if not self.object_class:
            self.object_class = self._meta.module_name
        super(Animal, self).save( *args, **kwargs)
    def get_object(self):
        if not self.object_class or self._meta.module_name == self.object_class:
            return self
        else:
            return getattr(self, self.object_class)

class Dog(Animal):
    def make_sound(self):
        print "Woof!"


class Cat(Animal):
    def make_sound(self):
        print "Meow!"

and the desired result:

shell$ ./manage.py shell_plus
From 'models' autoload: Animal, Dog, Cat
Python 2.6.5 (r265:79063, Apr 16 2010, 13:57:41) 
[GCC 4.4.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> dog1=Dog(type="Ozzie").save()
>>> cat1=Cat(type="Kitty").save()
>>> dog2=Dog(type="Dozzie").save()
>>> cat2=Cat(type="Kinnie").save()
>>> Animal.objects.all()
[<Dog: Dog object>, <Cat: Cat object>, <Dog: Dog object>, <Cat: Cat object>]
>>> for a in Animal.objects.all():
...    print a.type, a.make_sound()
... 
Ozzie Woof!
None
Kitty Meow!
None
Dozzie Woof!
None
Kinnie Meow!
None
>>> 

How does it work?

  1. Store information about class
    name of the animal – we use
    object_class for that
  2. Remove “proxy” meta attribute – we need to
    reverse relation in Django (the bad
    side of this we create extra DB
    table for every child model and
    waste additional DB hit for that,
    the good side we can add some child
    model dependent fields)
  3. Customize save() for Animal to save the class
    name in object_class of the object
    that invoke save.
  4. Method get_object is needed for referencing
    through reverse relation in Django
    to the Model with name cached in
    object_class.
  5. Do this .get_object() “casting” automatically
    every time Animal is instantiate by
    redefining Metaclass of Animal
    model. Metaclass is something like a
    template for a class (just like a
    class is a template for an object).

More information about Metaclass in Python: http://www.ibm.com/developerworks/linux/library/l-pymeta.html

Answered By: thedk

This answer may be side-stepping the question somewhat because it doesn’t use proxy models. However, as the question asks, it does let one write the following (and without having to update the Animal class if new types are added)–

animals = Animal.objects.all()
for animal in animals:
    animal.make_sound()

To avoid metaclass programming, one could use composition over inheritance. For example–

class Animal(models.Model):

    type = models.CharField(max_length=255)

    @property
    def type_instance(self):
        """Return a Dog or Cat object, etc."""
        return globals()[self.type]()

    def make_sound(self):
        return self.type_instance.make_sound()

class Dog(object):
    def make_sound(self):
        print "Woof!"

class Cat(object):
    def make_sound(self):
        print "Meow!"

If the Dog and Cat classes need access to the Animal instance, you could also adjust the type_instance() method above to pass what it needs to the class constructor (e.g. self).

Answered By: cjerdonek

The Metaclass approach proposed by thedk is indeed a very powerful way to go, however, I had to combine it with an answer to the question here to have the query return a proxy model instance. The simplified version of the code adapted to the previous example would be:

from django.db.models.base import ModelBase

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

class Animal(models.Model):
    __metaclass__ = InheritanceMetaclass
    type = models.CharField(max_length=255)
    object_class = models.CharField(max_length=20)

    def save(self, *args, **kwargs):
        if not self.object_class:
            self.object_class = self._meta.module_name
        super(Animal, self).save( *args, **kwargs)

    def get_object(self):
        if self.object_class in SUBCLASSES_OF_ANIMAL:
            self.__class__ = SUBCLASSES_OF_ANIMAL[self.object_class]
        return self

class Dog(Animal):
    class Meta:
        proxy = True
    def make_sound(self):
        print "Woof!"


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


SUBCLASSES_OF_ANIMAL = dict([(cls.__name__, cls) for cls in ANIMAL.__subclasses__()])

The advantage of this proxy approach is that no db migration is required upon creation of new subclasses. The drawback is that no specific fields can be added to the subclasses.

I would be happy to have feedback on this approach.

Answered By: Samuel

I played around with a lot of ways to do this. In the end the most simple seems to be the way forward.
Override __init__ of the base class.

class Animal(models.Model):
    type = models.CharField(max_length=255)
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.__class__ = eval(self.type)

I know eval can be dangerous, bla bla bla, but you can always add safeguarding/validation on the type choice to ensure it’s what you want to see.
Besdies that, I can’t think of any obvious pitfalls but if i find any i’ll mention them/ delete the answer! (yeah i know the question is super old, but hopefully this’ll help others with the same problem)

Answered By: rbennell