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

一曲冷凌霜 提交于 2019-11-30 00:04:54

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.

Samuel

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.

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

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)

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).

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!