Reputation: 2798
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?
Upvotes: 34
Views: 11376
Reputation: 1174
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)
Upvotes: 3
Reputation: 479
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.
Upvotes: 12
Reputation: 6269
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
).
Upvotes: 0
Reputation: 1959
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?
More information about Metaclass in Python: http://www.ibm.com/developerworks/linux/library/l-pymeta.html
Upvotes: 5
Reputation: 99530
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.
Upvotes: 2