通过__getattr__改变Django模型的行为

5 投票
4 回答
8504 浏览
提问于 2025-04-16 04:23

我想改变一个Django模型的行为,让我可以直接从父模型访问外键的属性,比如说:

cache.part_number  
vs  
cache.product.part_number

我试着重写了 __getattr__ 方法,但当我尝试访问外键的属性时,出现了递归错误。

class Product(models.Model):
    part_number = models.CharField(max_length=10)
    ...

class Cache(models.Model):
    product = models.ForeignKey(Product)
    ...

    def __getattr__(self, name):
        value = getattr(self.product, name, None)
        if value:
            return value
        else:
            raise AttributeError

我哪里做错了呢?

4 个回答

0

你可以试试这样的写法:

class Product(models.Model):
    part_number = models.CharField(max_length=10)
    ...

class Cache(models.Model):
    product = models.ForeignKey(Product)
    ...

    def __getattr__(self, name):
        prefix = 'product_'

        # Only deal with get_ calls
        if not name.startswith(prefix):
            raise AttributeError
        else:
            name = name.replace(prefix,'')
            value = getattr(self.product, name, None)
            if value:
                return value
            else:
                raise AttributeError

然后你可以这样调用它:

cache.product_part_number
1

我遇到过和这里提到的类似问题,只有在我研究了Python是如何访问对象属性的时候,才找到了答案。

我理解的是,当调用getattr()时,Python首先会调用getattribute(),如果getattribute()找不到这个属性,Python才会使用你定义的getattr函数。

我尽量不在我的函数中使用getattr,因为这样会导致无限递归,具体可以参考这个链接:https://stackoverflow.com/a/3278104/2319915

所以:

class Product(models.Model):
   part_number = models.CharField(max_length=10)


class Cache(models.Model):
   product = models.ForeignKey(Product)

   def __getattr__(self, name):
      try:
         return getattribute(self, name)
      except AttributeError:
         try:
            return Product.objects.get(part_no=self.product.part_no)
         except ObjectDoesNotExist:
            raise AttributeError
10

考虑一下你在 __getattr__ 方法里的代码:

value = getattr(self.product, name, None)

试着猜猜当你调用 self.product 时会发生什么。我给你个提示:这涉及到调用 __getattr__。详细信息可以参考文档

当在常规地方找不到属性时(也就是说,它既不是实例属性,也不在类树中),就会调用这个方法。name 是属性的名称。这个方法应该返回计算出的属性值,或者抛出一个 AttributeError 异常。

你有没有想过,为什么 self.product 能正确找到对应的 Product 实例,尽管你并没有在任何地方设置它?

注意,如果通过正常方式找到了属性,就不会调用 __getattr__()

Django 做了一些魔法,正如你猜到的,涉及到拦截 __getattr__。这样一来,self 自动就有了一个属性 product。但是因为你重写了 __getattr__ 方法,Django 的魔法就失效了,取而代之的是你自己的版本。由于 self.product 不是一个实例属性,所以又会调用 __getattr__,这样不断循环下去,最终导致无限循环。

你最好使用一个property 来实现这个功能。

class Cache(models.Model):
    product = models.ForeignKey(Product)
    ...

    def _get_part_number(self):
        part_number = self.product.part_number
        if not part_number:
            raise AttributeError
        return part_number
    part_number = property(_get_part_number)

撰写回答