Why does hasattr behave differently on classes and instances with @property method?

592 Views Asked by At

I implemented a write-only property in my class with @property. The weird thing is that hasattr behaves differently on the class and corresponding instance with this property.

from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin


class User(Base, UserMixin):
    # codes omitted...

    @property
    def password(self):
        raise AttributeError("password is a write-only attribute!")

    @password.setter
    def password(self, password):
        self.password_hash = generate_password_hash(password)

    def verify_password(self, password):
        return check_password_hash(self.password_hash, password)
In [6]: hasattr(User,'password')
Out[6]: True

In [7]: u1=User()

In [9]: hasattr(u1,'password')
Out[9]: False

In [12]: getattr(User,'password')
Out[12]: <property at 0x1118a84a8>

In [13]: getattr(u1,'password')
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-13-b1bb8901adc7> in <module>
----> 1 getattr(u1,'password')

~/workspace/python/flask_web_development/fisher/app/models.py in password(self)
     82     @property
     83     def password(self):
---> 84         raise AttributeError("password is a write-only attribute!")
     85
     86     @password.setter

AttributeError: password is a write-only attribute!

From the result of getattr, getattr(u1, 'password') tries to execute the method and raises an error, while getattr(User, 'password') doesn't execute the @property method. Why do they behave differently?

2

There are 2 best solutions below

1
On BEST ANSWER

Properties are descriptors.


Regarding getattr:

When you access an attribute via getattr or the dot-notation on an object (u1) and the class of that object (User) happens to have a descriptor going by the name you are trying to access, that descriptor's __get__ method is called1, as happens when you issue getattr(u1, 'password'). In your specific case, the logic you defined in your getter (raising the AttributeError) will be executed.

With getattr(User, 'password') the instance passed to the __get__ method is None, in which case __get__ just returns the descriptor itself instead of executing the getter logic you implemented.

1There are some special rules depending on whether you have a data or a non-data descriptor, as explained in the Descriptor HowTo.


Regarding hasattr:

hasattr(u1, 'password') returns False because getattr(u1, 'password') raises an error. See this question.

0
On

Adding to what @timgeb mentioned, there is lot happening in the background than what it appears.

Properties are implemented as Descriptors and the way attribute lookup happens is different when you access an attribute with object and class. When you access the attribute with object like obj.attr basically the rules for attribute lookup are as follows

  1. Looks inside __class__.__dict__ and see if this attribute is a data descriptor, if yes then the call the __get__, this translates to type(obj).__dict__['attr'].__get__(obj, type(obj)).
  2. Look in the __dict__ of the object and return obj.__dict__['attr']
  3. If the attribute is a non-data descriptor, call its __get__, this again translates to type(obj).__dict__['attr'].__get__(obj, type(obj)).
  4. Fetch the attribute from __dict__ of the class.
  5. Call the default implementation of getattr.

Now when you try to access the same attribute with class.attr the same rules apply with a slight difference that this time metaclass of the class is also involved, so here it looks

  1. Does the metaclass has data descriptor defined for this attribute, if yes then call return type(class).__dict__['attr']__get__(class, type(class)) on it.
  2. Look inside __dict__ of the class and see if this attribute is a descriptor of any type, if yes then fetch the attribute calling the __get__, if it is not a descriptor fetch the value from __dict__ of the class.

  3. If the attribute is a non-data descriptor in the metalcass, call its __get__.

  4. Fetch the attribute from __dict__ of the metaclass.
  5. Call the default implementation of getattr.

Further the default implementation of __get__ for properties has a check that when you access the attribute with the class, it returns the descriptor instance itself, however when you access the attribute with object it actually fires the code inside the __get__.

def __get__(self, instnace, class):
    if instance is None:
        return self
    else:
        # code

This also explains why hasattr(User, 'password') returns True because since you are calling the attribute with class the else is not getting executed and hence exception is not being raised and hasattr(u1, 'password') returns False as it encounters exception.