Simple repro:
class VocalDescriptor(object):
def __get__(self, obj, objtype):
print(\'__get__, obj={}, obj
You are correct that B.v = 3
simply overwrites the descriptor with an integer (as it should).
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
invoking the getter is to allow returning the descriptor instance itself. Usually you would do that, to allow access on the descriptor via the class object:
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.
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__
.