Правильный способ вернуть экземпляр прокси-модели из экземпляра базовой модели в Django? - PullRequest
24 голосов
/ 08 февраля 2010

Скажите, у меня есть модели:

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

Допустим, я хочу сделать:

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

Я хочу вернуть серию Гавов и Мяу. Ясно, что я мог бы просто определить make_sound в исходной модели, которая разветвляется на основе animal_type, но затем каждый раз, когда я добавляю новый тип животных (представьте, что они в разных приложениях), мне нужно было бы входить и редактировать эту функцию make_sound , Я бы предпочел просто определить прокси-модели и сделать так, чтобы они сами определяли поведение. Из того, что я могу сказать, нет никакого способа вернуть смешанные экземпляры Cat или Dog, но я подумал, что я мог бы определить метод "get_proxy_model" для основного класса, который возвращает модель cat или dog.

Конечно, вы могли бы сделать это и передать что-то вроде первичного ключа, а затем просто сделать Cat.objects.get (pk = pass_in_primary_key). Но это будет означать выполнение дополнительного запроса к уже имеющимся данным, который кажется излишним. Есть ли способ эффективно превратить животное в кошку или собаку? Как правильно делать то, чего я хочу достичь?

Ответы [ 5 ]

9 голосов
/ 14 мая 2014

Подход Metaclass, предложенный thedk, действительно является очень мощным способом, однако мне пришлось объединить его с ответом на вопрос здесь , чтобы запрос вернул прокси экземпляр модели. Упрощенная версия кода, адаптированная к предыдущему примеру, будет:

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__()])

Преимущество этого прокси-подхода заключается в том, что при создании новых подклассов не требуется перенос БД. Недостаток заключается в том, что к подклассам нельзя добавлять конкретные поля.

Я был бы рад получить отзыв об этом подходе.

4 голосов
/ 14 марта 2011

единственный известный человечеству способ - это использовать программирование метаклассов.

Вот краткий ответ:

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!"

и желаемый результат:

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

Как это работает?

  1. Хранить информацию о классе название животного - используем object_class для этого
  2. Удалить мета-атрибут proxy - нам нужно обратное отношение в Джанго (плохое сторона этого мы создаем дополнительную БД таблица для каждой детской модели и тратить дополнительные БД на это, хорошая сторона, мы можем добавить ребенка зависимые от модели поля)
  3. Настройте save () для Animal, чтобы сохранить класс имя в object_class объекта это вызывает сохранить.
  4. Метод get_object необходим для ссылки через обратную связь в Джанго к модели с именем в кеше object_class.
  5. Сделайте это .get_object () "кастинг" автоматически каждый раз, когда животное инстанцируется переопределение метакласса животных модель. Метакласс это что-то вроде шаблон для класса (так же, как класс - это шаблон для объекта).

Дополнительная информация о метаклассе в Python: http://www.ibm.com/developerworks/linux/library/l-pymeta.html

1 голос
/ 15 ноября 2018

Я играл с множеством способов сделать это. В конце концов, самым простым кажется путь вперед. Переопределить __init__ базового класса.

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

Я знаю, что eval может быть опасным, бла-бла-бла, но вы всегда можете добавить защиту / проверку правильности выбора типа, чтобы убедиться, что это то, что вы хотите увидеть. Кроме того, я не могу думать ни о каких очевидных подводных камнях, но если я найду их, я упомяну их / удалю ответ! (да, я знаю, что вопрос очень старый, но, надеюсь, это поможет другим с той же проблемой)

1 голос
/ 08 февраля 2010

Возможно, вы сделаете модели Django полиморфными, используя подход, описанный здесь . Я полагаю, что этот код находится на ранней стадии разработки, но его стоит изучить.

0 голосов
/ 22 февраля 2014

Этот ответ может несколько обойти вопрос, потому что он не использует прокси-модели. Однако, как следует из вопроса, он позволяет написать следующее (и без необходимости обновлять класс Animal, если добавляются новые типы) -

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

Чтобы избежать программирования метаклассов, можно использовать композицию вместо наследования . Например -

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!"

Если классам Dog и Cat требуется доступ к экземпляру Animal, вы также можете настроить приведенный выше метод type_instance(), чтобы передать то, что ему нужно, в конструктор класса (например, self).

...