I follow the tutorial out of the docs and an example by fluent Python. In the book they teach me how to avoid the AttributeError
by get, (e.g., when you do z = Testing.x
) and I wanted to do something simliar with the set method. But it seems like, it lead to a broken class with no error.
To be more specific about the issue:
Testing.x = 1
it invoke the __set__
methods.#Testing.x = 1
it does not invoke the __set__
methods.Can someone teach me why it behaves this way?
import abc
class Descriptor:
def __init__(self):
cls = self.__class__
self.storage_name = cls.__name__
def __get__(self, instance, owner):
if instance is None:
return self
else:
return getattr(instance, self.storage_name)
def __set__(self, instance, value):
print(instance,self.storage_name)
setattr(instance, self.storage_name, value)
class Validator(Descriptor):
def __set__(self, instance, value):
value = self.validate(instance, value)
super().__set__(instance, value)
@abc.abstractmethod
def validate(self, instance, value):
"""return validated value or raise ValueError"""
class NonNegative(Validator):
def validate(self, instance, value):
if value <= 0:
raise ValueError(f'{value!r} must be > 0')
return value
class Testing:
x = NonNegative()
def __init__(self,number):
self.x = number
#Testing.x = 1
t = Testing(1)
t.x = 1
Attribute access is generally handled by object.__getattribute__
and type.__getattribute__
(for instances of type
, i.e. classes). When an attribute lookup of the form a.x
involves a descriptor as x
, then various binding rules come into effect, based on what x
is:
a.x
is transformed into the call: type(a).__dict__['x'].__get__(a, type(a))
.A.x
is transformed into the call: A.__dict__['x'].__get__(None, A)
.For the scope of this question, only (2) is relevant. Here, Testing.x
invokes the descriptor via __get__(None, Testing)
. Now one might ask why this is done instead of simply returning the descriptor object itself (as if it was any other object, say an int
). This behavior is useful to implement the classmethod decorator. The descriptor HowTo guide provides an example implementation:
class ClassMethod:
def __init__(self, f):
self.f = f
def __get__(self, obj, cls=None):
print(f'{obj = }, {cls = }')
return self.f.__get__(cls, cls) # simplified version
class Test:
@ClassMethod
def func(cls, x):
pass
Test().func(2) # call from instance
Test.func(1) # this requires binding without any instance
We can observe that for the second case Test.func(1)
there is no instance involved, but the ClassMethod
descriptor can still bind to the cls
.
Given that __get__
is used for both, instance and class binding, one might ask why this isn't the case for __set__
. Specifically, for x.y = z
, if y
is a data descriptor, why doesn't it invoke y.__set__(None, z)
? I guess the reason is that there is no good use case for that and it unnecessarily complicates the descriptor API. What would the descriptor do with that information anyway? Typically, managing how attributes are set is done by the class (or metaclass for types), via object.__setattr__
or type.__setattr__
.
So to prevent Testing.x
from being replaced by a user, you could use a custom metaclass:
class ProtectDataDescriptors(type):
def __setattr__(self, name, value):
if hasattr(getattr(self, name, None), '__set__'):
raise AttributeError(f'Cannot override data descriptor {name!r}')
super().__setattr__(name, value)
class Testing(metaclass=ProtectDataDescriptors):
x = NonNegative()
def __init__(self, number):
self.x = number
Testing.x = 1 # now this raises AttributeError
However, this is not an absolute guarantee as users can still use type.__setattr__
directly to override that attribute:
type.__setattr__(Testing, 'x', 1) # this will bypass ProtectDataDescriptors.__setattr__