Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Correct handling of AttributeError in __getattr__ when using property

Tags:

python

I have a difficulty implementing properties and __getattr__ so that when an error happens, it is reported correctly. This is my MWE (python 3.6):

class A:

    @property
    def F(self):
        return self.moo # here should be an error

    @property
    def G(self):
        return self.F

    def __getattr__(self, name):
        print('call of __getattr__ with name =', name)
        if name == 'foo':
            return 0
        raise AttributeError("'{}' object has no attribute '{}'".format(type(self).__name__, name))

a = A()
print(a.G)

The output is as follows:

call of __getattr__ with name = moo
call of __getattr__ with name = F
call of __getattr__ with name = G
Traceback (most recent call last):
  line 18 in <module>
    print(a.G)
  line 15, in __getattr__
    raise AttributeError("'{}' object has no attribute '{}'".format(type(self).__name__, name))
AttributeError: 'A' object has no attribute 'G'

But the error that should be raised is:

AttributeError: 'A' object has no attribute 'moo'

I know that properties and attributes in the __dict__ are attempted before __getattr__ is called in an error-free scenario.

  1. It seems incorrect to me that when a property exists but fails, __getattr__ is still attempted instead of letting the error from the property to go through. How can this be avoided?

  2. The initial error message that was generated about failing to get attribute 'foo' has been lost. The final error message 'A' object has no attribute 'G' is particularly misleading and annoying. How to implement __getattr__ in order to see the initial error?

  3. (EDIT) A related problem is simultaneously to achieve that hasattr(a, 'moo') returns False while hasattr(a, 'G') returns True or raises an exception of the missing 'moo' attribute. Does that make sense?

like image 551
Alexander Shekhovtsov Avatar asked May 26 '18 11:05

Alexander Shekhovtsov


People also ask

What does __ Getattr __ do in Python?

Python getattr() function is used to get the value of an object's attribute and if no attribute of that object is found, default value is returned.

What is the point of Getattr?

Python | getattr() method Python getattr() function is used to access the attribute value of an object and also gives an option of executing the default value in case of unavailability of the key.

How does Python handle attribute errors?

To avoid the AttributeError in Python code, a check should be performed before referencing an attribute on an object to ensure that it exists. The Python help() function can be used to find out all attributes and methods related to the object. To resolve the AttributeError , a try-except block can be used.

Is Getattr slow python?

The getattr approach is slower than the if approach, reducing an insignificant ~3% the performance if bar is set and a considerable~35% if is not set.


1 Answers

What is happening?

First, a little heads up as to why this happens. From the doc on __getattr__:

Called when the default attribute access fails with an AttributeError [...] or __get__() of a name property raises AttributeError.

In this case, since you are using @property, we are looking at an AttributeError raised from the __get__ method of the property F when trying to recover self.moo. This is what your call stack looks like at that moment.

__main__
a.G.__get__
a.F.__get__
a.__getattr__ # called with 'moo' <-- this is where the error is raised

The attribute getter protocol sees an error being raised from inside a.F.__get__, it thus fallback on calling a.__getattr__('F') and that despite the fact the error had been raised because of 'moo'. The same then happens for a.G.__get__

This behaviour is considered normal in Python, since the top-most property that failed to return a value is indeed a.G.

Solution

Now what you want is for an AttributeError raised by a __get__ method to bubble up instead of being caught. To do that you need not to have a __getattr__ method.

Thus, in this particular case, what you want to use is __getattribute__ instead.

Of course, with this solution you have to make sure yourself not to override an existing attribute.

class A:

    @property
    def F(self):
        return self.moo # here should be an error

    @property
    def G(self):
        return self.F

    def __getattribute__(self, name):
        print('call of __getattribute__ with name =', name)
        if name == 'foo':
            return 0
        else:
            return super().__getattribute__(name)

Example

A().G

Output

call of __getattribute__ with name = G
call of __getattribute__ with name = F
call of __getattribute__ with name = moo

Traceback (most recent call last):
...
AttributeError: 'A' object has no attribute 'moo'
like image 60
Olivier Melançon Avatar answered Oct 25 '22 19:10

Olivier Melançon