Why does setting a descriptor on a class overwrite the descriptor?
Barring any overrides, B.v
is equivalent to type.__getattribute__(B, "v")
, while b = B(); b.v
is equivalent to object.__getattribute__(b, "v")
. Both definitions invoke the __get__
method of the result if defined.
Note, thought, that the call to __get__
differs in each case. B.v
passes None
as the first argument, while B().v
passes the instance itself. In both cases B
is passed as the second argument.
B.v = 3
, on the other hand, is equivalent to type.__setattr__(B, "v", 3)
, which does not invoke __set__
.
You are correct that B.v = 3
simply overwrites the descriptor with an integer (as it should). In the descriptor protocol, __get__
is designed to be called as instance attribute or class attribute, but __set__
is designed to be called only as instance attribute.
For B.v = 3
to invoke a descriptor, the descriptor should have been defined on the metaclass, i.e. on type(B)
.
>>> class BMeta(type):
... v = VocalDescriptor()
...
>>> class B(metaclass=BMeta):
... pass
...
>>> B.v = 3
__set__
To invoke the descriptor on B
, you would use an instance: B().v = 3
will do it.
The reason for B.v
also invoking the getter is to allow user's customization of what B.v
does, independently of whatever B().v
does. A common pattern is to allow direct access on the descriptor instance, by returning the descriptor itself when a class attribute access was used:
class VocalDescriptor(object):
def __get__(self, obj, objtype):
if obj is None:
return self
print('__get__, obj={}, objtype={}'.format(obj, objtype))
def __set__(self, obj, val):
print('__set__')
Now B.v
would return some instance like <mymodule.VocalDescriptor object at 0xdeadbeef>
which you can interact with. It is literally the descriptor object, defined as a class attribute, and its state B.v.__dict__
is shared between all instances of B
.
Of course it is up to user's code to define exactly what they want B.v
to do, returning self
is just the common pattern. A classmethod
is an example of a descriptor which does something different here, see the Descriptor HowTo Guide for a pure-python implementation of classmethod
.
Unlike __get__
, which can be used to customize B().v
and B.v
independently, __set__
is not invoked unless the attribute access is on an instance. I would suppose that the goal of customizing B().v = other
and B.v = other
using the same descriptor v
is not common or useful enough to complicate the descriptor protocol further, especially since the latter is still possible with a metaclass descriptor anyway, as shown in BMeta.v
above.