在Django中如何从基模型实例返回代理模型实例?

33 投票
5 回答
10830 浏览
提问于 2025-04-15 19:00

假设我有一些模型:

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的函数,根据动物类型来分支,但每次我添加一个新的动物类型(想象一下它们在不同的应用里),我就得去修改这个make_sound函数。我更希望能定义一些代理模型,让它们自己定义行为。从我能看到的,似乎没有办法返回混合的猫或狗实例,但我想也许我可以在主类上定义一个“get_proxy_model”的方法,返回一个猫或狗的模型。

当然,你可以这样做,传入一些像主键这样的东西,然后就可以用Cat.objects.get(pk = 传入的主键)来获取。但这就意味着要多做一次查询,而这些数据你其实已经有了,这样看起来有点多余。有没有什么办法可以有效地把一个动物转变成猫或狗的实例?我想实现的目标有什么正确的方法吗?

5 个回答

5

人类已知的唯一方法是使用元类编程。

这里是简短的答案:

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. 移除“代理”元属性 - 我们需要在 Django 中反向关系(这样做的坏处是每个子模型都会创建额外的数据库表,并且会多一次数据库访问,好的方面是我们可以添加一些依赖于子模型的字段)。
  3. 定制 Animal 的 save() 方法,以便在调用 save 的对象中保存类名到 object_class。
  4. 需要一个 get_object 方法,用于通过 Django 中的反向关系引用到 object_class 中缓存的模型名称。
  5. 每次实例化 Animal 时,自动进行这个 .get_object() 的“转换”,通过重新定义 Animal 模型的元类来实现。元类就像是类的模板(就像类是对象的模板一样)。

关于 Python 中元类的更多信息: http://www.ibm.com/developerworks/linux/library/l-pymeta.html

12

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

这个代理方法的好处是,创建新子类时不需要进行数据库迁移。缺点是,子类不能添加特定的字段。

我很乐意听听大家对这个方法的反馈。

2

你可以尝试使用这里提到的方法,让Django模型具有多态性。这个代码目前还在早期开发阶段,但值得一看。

撰写回答