Django 管理界面未使用子类的 __unicode__()

5 投票
6 回答
5083 浏览
提问于 2025-04-11 18:08

(Django 1.x, Python 2.6.x)

我有一些模型,像这样:

class Animal(models.Model):
  pass

class Cat(Animal):
  def __unicode__(self):
    return "This is a cat"

class Dog(Animal):
  def __unicode__(self):
    return "This is a dog"

class AnimalHome(models.Model):
  animal = models.ForeignKey(Animal)

我没有实例化任何动物,因为这应该是一个虚拟类。我实例化了猫和狗,但在AnimalHome的管理页面上,我的动物选项显示为“Animal object”(我想这是默认的__unicode__()),而不是我为这两个子类定义的__unicode__。求助。


我觉得抽象基类的问题跟这个问题没什么关系。即使Animal不应该是抽象的,我仍然面临一个问题:由于ForeignKey是在Animal上定义的,而不是它的子类,所以调用的是父类的方法,而不是子类的方法。在面向对象编程中,当你调用object.method()时,应该得到最低子类的实现,而要获取任何父类的实现需要额外的工作。那么,为什么在子类上定义的__unicode__不够呢?实际上,问题可能是__unicode__根本没有被调用,因为对Animal类的检查显示它没有定义。所以,也许如果我为Animal定义__unicode__并让它调用子类的__unicode__,我就能得到想要的效果。


好的,我想我理解ORM的问题了。这两个答案帮助我理解了这一点,谢谢。在实验过程中,我发现当Django保存一个子类模型时,它做了两件事:(1)在父类的表中为子类对象创建一行,和(2)使子类表中的主键(PK)与父类表中分配的主键相同。子类表中的这个主键被称为superclass_ptr。基于此,我想出了以下内容。希望能得到反馈。

Class Animal(models.Model)
  def __unicode__(self):
    if Dog.objects.filter(pk=self.pk).count() > 0:
      return unicode(Dog.objects.get(pk=self.pk))
    elif Cat.objects.filter(pk=self.pk).count() > 0:
      return unicode(Cat.objects.get(pk=self.pk))
    else:
      return "An Animal!"

看起来Lawrence对这个问题的理解最到位。猫和狗会有不同的主键集合(任何Animal的子类都会有一个与其父类记录相同的主键),但不幸的是,Django在后台并没有做任何工作,比如:“我是一个动物。我知道动物有子类狗和猫。具体来说,我是动物编号3,而且我刚检查过,还有一只猫编号3。这意味着我实际上是猫编号3。”尽管这看起来完全可能且非常合理(因为猫不会做任何动物自己不能做的事情),但使用Python的反射功能并没有实现。谢谢大家。

6 个回答

3

Django(还有一般的关系型数据库)并不是这样工作的。即使使用像Django这样的ORM(对象关系映射工具),你也不会用这种方式来处理类的层级关系。

针对你的问题,有两种可能的解决方案:

(1) 给动物模型添加一个“名字”属性,然后用['狗', '猫']这样的名字来添加实体。这样在外键选择框中就会显示动物的名字。

(2) 如果你真的需要把外键链接到不同的模型(这其实不是关系型数据库的常规用法),你应该去看看文档中关于通用关系的内容。

不过,我的建议是选择(1)。

6

你想要一个抽象基类(在Python中“虚拟”这个词没有实际意义)。

根据文档:

class CommonInfo(models.Model):
    name = models.CharField(max_length=100)
    age = models.PositiveIntegerField()

    class Meta:
        abstract = True

编辑

“在面向对象编程中,当你调用对象的方法时,应该得到最低子类的实现。”

这是真的,但并不是全部。

这不是面向对象的问题,甚至不是Python或Django的问题。这是一个ORM(对象关系映射)的问题。

问题是“在外键引用结束时重建了哪个对象?”答案是没有标准的、明显的方式来处理从外键值到对象的转换。

我在AnimalHome表中有一行,animals的值是42。它指向Animal.objects.get(pk=42)。那么,具体是哪个Animal的子类呢?是猫吗?是狗吗?ORM层怎么知道应该执行Dog.objects.get(pk=42)还是Cat.objects.get(pk=42)呢?

“等等,”你会说。“它应该获取Animal对象,而不是Dog或Cat对象。”你可以这样希望,但这不是Django ORM的工作方式。每个类都是一个独立的表。猫和狗——从定义上讲——是不同的表,有各自的查询。你并不是在使用对象存储,而是在使用ORM与关系表。


编辑

首先,你的查询只有在狗和猫共享一个公共的键生成器,并且没有重叠的主键时才有效。

如果你有一只主键为42的狗和一只主键也为42的猫,那就麻烦了。而且由于你无法轻易控制键的生成,你的解决方案就无法奏效。

运行时类型识别(RTTI)不好。在很多方面它并不是面向对象的。几乎任何可以避免RTTI的方法都比不断扩展的if语句序列要好,这种序列用来区分子类。

然而,你试图构建的模型对于ORM系统来说,确实是一个病态问题。实际上,具体到如此病态,我几乎敢打赌这是作业题。[纯SQL系统也有病态问题,它们通常也会作为作业出现。]

问题在于ORM无法做到你认为它应该做到的事情。所以你有两个选择。

  • 停止使用Django。
  • 做一些Django直接支持的事情。
  • 打破面向对象设计的原则,使用像RTTI这样的脆弱方法,这会让你在添加另一个动物子类时变得异常困难。

考虑这种方式来进行RTTI——它包括类名和主键

KIND_CHOICES = (
   ( "DOG", "Dog" ),
   ( "CAT", "Cat" ),
)

class Animal( models.Model ):
    kind = models.CharField( max_length= 1, choices=KIND_CHOICES )
    fk = models.IntegerField()
    def get_kind( self ):
        if kind == "DOG":
            return Dog.objects.get( pk = fk )
        elif kind == "CAT":
            return Cat.objects.get( pk = fk )
6

ForeignKey(Animal) 就是指向 Animal 表中某一行的外键引用。底层的 SQL 结构并没有表明这个表是作为一个父类在使用,所以你得到的就是一个 Animal 对象。

为了绕过这个问题:

首先,你需要确保基类不是抽象类。这对于外键来说是必要的,同时也能确保 Dog 和 Cat 这两个类有不同的主键集合。

接下来,Django 使用 OneToOneField 来实现继承。因为这个原因,一个基类的实例如果有子类的实例,就会得到一个对那个子类实例的引用,并且这个引用会有合适的名称。 这意味着你可以这样做:

class Animal(models.Model):
    def __unicode__(self):
        if hasattr(self, 'dog'):
            return self.dog.__unicode__()
        elif hasattr(self, 'cat'):
            return self.cat.__unicode__()
        else:
            return 'Animal'

这也回答了你问 Ber 的关于 unicode() 的问题,它依赖于其他子类的属性。现在你实际上是在调用子类实例上的合适方法。

现在,这确实暗示着,由于 Django 在后台已经在寻找子类实例,代码可以直接返回一个 Cat 或 Dog 的实例,而不是 Animal。这个问题你得去问开发者们。:)

撰写回答