Why is getattr() throwing an exception when an attribute doesn't exist?
Asked Answered
I

2

7

This one has me baffled. Consider the following Django models - representing zookeepers and the cages at the zoo that they are responsible for cleaning:

class Zookeeper(moodels.Model):
    name = models.CharField(max_length=40)

class Cage(models.Model):
    zookeeper = models.ForeignKey(Zookeeper)

Now suppose I want to connect a receiver to the Cage's post_init signal:

@receiver(models.signals.post_init, sender=Cage)
def on_cage_init(instance, **kwargs):
    print instance.zookeeper

As expected, this raises an exception since the Cage has not yet been assigned to a Zookeeper. Consider the following modification to the body of the receiver:

print getattr(instance, 'zookeeper', 'No Zookeeper')

One would expect this to print "No Zookeeper" since one has not been assigned to the instance. Instead, an exception is raised:

Traceback (most recent call last):
  File "../zoo/models.py", line 185, in on_cage_init
    print getattr(instance, 'zookeeper', 'No Zookeeper')
  File "/usr/local/lib/python2.7/dist-packages/django/db/models/fields/related.py", line 324, in __get__
    "%s has no %s." % (self.field.model.__name__, self.field.name))
DoesNotExist: Cage has no zookeeper.

Why is it raising an exception? Isn't getattr() supposed to return the provided default value if the attribute does not exist? I can prove that the attribute does not exist with:

print hasattr(instance, 'zookeeper')

...which prints False.

Inquisitionist answered 17/7, 2014 at 4:2 Comment(0)
H
7

@metatoaster's explanation is really good and this is basically what is happening. See __get__ magic method defined here.

As a solution, I would apply "Easier to ask for forgiveness than permission" principle. Try getting the attribute and catch the specific exception:

from django.core.exceptions import ObjectDoesNotExist

try:
    print instance.zookeeper
except ObjectDoesNotExist:
    print "No zookeeper"
Hyperboloid answered 17/7, 2014 at 4:11 Comment(1)
You could also avoid importing ObjectDoesNotExist by instead catching Zookeeper.DoesNotExist.Androclinium
B
10

Most likely that class has __getattribute__ defined. See this example:

>>> class O(object):
...     def __getattribute__(self, name):
...         raise Exception("can't get attribute")
...
>>> o = O()
>>> getattr(o, 'test', 'nothing')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in __getattribute__
Exception: can't get attribute

Note how getattr essentially calls o.__getattribute__ internally, and if that raises a generic exception, it will just fail with that exception.

However, if it was properly define to raise AttributeError, getattr will catch that properly.

>>> class O(object):
...     def __getattribute__(self, name):
...         raise AttributeError("can't get attribute")
...
>>> o = O()
>>> getattr(o, 'test', 'nothing')
'nothing'

So this could be considered as a bug in the definition of the DoesNotExist exception where it does not correctly inherit from AttributeError as it should have.

A more complete example to demonstrate all of the above:

>>> class O(object):
...     def __getattribute__(self, name):
...         if name == 'test':
...             return 'good value'
...         elif name == 'bad':
...             raise Exception("don't raise this")
...         else:
...             raise DoesNotExist()
...
>>> class DoesNotExist(AttributeError):
...     pass
...
>>> o = O()
>>> getattr(o, 'test', 'nothing')
'good value'
>>> getattr(o, 'something', 'nothing')
'nothing'
>>> getattr(o, 'bad', 'nothing')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in __getattribute__
Exception: don't raise this

Of course, all that above doesn't exactly help you in working around that bug. Rather than waiting for that bug to be resolved, just implement your getattr that traps that exception (or any other exceptions that you might be expecting). Something like this might work:

def safe_getattr(obj, name, default):
    try:
        return getattr(obj, name, default)
    except Exception:  # or your specific exceptions
        return default
Budge answered 17/7, 2014 at 4:8 Comment(2)
+1 good explanation ±0 horrible naming (o, O, O0, oO, Oo) ;)Josephson
@Josephson the O and C keys are literally next to each other, I swear!Budge
H
7

@metatoaster's explanation is really good and this is basically what is happening. See __get__ magic method defined here.

As a solution, I would apply "Easier to ask for forgiveness than permission" principle. Try getting the attribute and catch the specific exception:

from django.core.exceptions import ObjectDoesNotExist

try:
    print instance.zookeeper
except ObjectDoesNotExist:
    print "No zookeeper"
Hyperboloid answered 17/7, 2014 at 4:11 Comment(1)
You could also avoid importing ObjectDoesNotExist by instead catching Zookeeper.DoesNotExist.Androclinium

© 2022 - 2024 — McMap. All rights reserved.