I'm trying to change the behaviour of a Django model to allow me to access a foreign key's properties directly from the parent, e.g.
cache.part_number
vs
cache.product.part_number
I've tried overriding the __getattr__
method as follows, but I get a recursion error when I try to access the foreign key's properties
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
What am I doing wrong?
Consider the code inside your __getattr__
method:
value = getattr(self.product, name, None)
Try guessing what happens when self.product
is invoked. I'll give you a clue: it involves a call to __getattr__
. The documentation has the details:
Called when an attribute lookup has not found the attribute in the usual places (i.e. it is not an instance attribute nor is it found in the class tree for self). name is the attribute name. This method should return the (computed) attribute value or raise an AttributeError exception.
Have you wondered how self.product
resolves to the correct Product
instance, even though you are not setting it anywhere?
Note that if the attribute is found through the normal mechanism, __getattr__()
is not called.
Django does some magic that involves intercepting, you guessed it, __getattr__
. Thereby self
automatically ends up with an attribute product
. Since you are overriding the __getattr__
method, Django's magic ceases to work and your version is used. Since self.product
is not an instance attribute, __getattr__
is called again, and again and so on, leading to an infinite loop.
You'd be better off using a property
to achieve this.
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)
I had a similar problem to the one posted here and only got the answer when I looked into the way Python accesses attributes of an object.
As i understand it when getattr() is called Python first calls getattribute(), if that attribute is not found by getattribute then python will use your getattr function.
I tried to stay away from using getattr within my function because it causes infinite recursions see: https://stackoverflow.com/a/3278104/2319915
so:
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
How about something like:
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
You can then call:
cache.product_part_number