通过__getattr__改变Django模型的行为
我想改变一个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 个回答
你可以试试这样的写法:
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
我遇到过和这里提到的类似问题,只有在我研究了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
考虑一下你在 __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)