14

Simple repro:

class VocalDescriptor(object):
    def __get__(self, obj, objtype):
        print('__get__, obj={}, objtype={}'.format(obj, objtype))
    def __set__(self, obj, val):
        print('__set__')

class B(object):
    v = VocalDescriptor()

B.v # prints "__get__, obj=None, objtype=<class '__main__.B'>"
B.v = 3 # does not print "__set__", evidently does not trigger descriptor
B.v # does not print anything, we overwrote the descriptor

This question has an effective duplicate, but the duplicate was not answered, and I dug a bit more into the CPython source as a learning exercise. Warning: i went into the weeds. I'm really hoping I can get help from a captain who knows those waters. I tried to be as explicit as possible in tracing the calls I was looking at, for my own future benefit and the benefit of future readers.

I've seen a lot of ink spilled over the behavior of __getattribute__ applied to descriptors, e.g. lookup precedence. The Python snippet in "Invoking Descriptors" just below For classes, the machinery is in type.__getattribute__()... roughly agrees in my mind with what I believe is the corresponding CPython source in type_getattro, which I tracked down by looking at "tp_slots" then where tp_getattro is populated. And the fact that B.v initially prints __get__, obj=None, objtype=<class '__main__.B'> makes sense to me.

What I don't understand is, why does the assignment B.v = 3 blindly overwrite the descriptor, rather than triggering v.__set__? I tried to trace the CPython call, starting once more from "tp_slots", then looking at where tp_setattro is populated, then looking at type_setattro. type_setattro appears to be a thin wrapper around _PyObject_GenericSetAttrWithDict. And there's the crux of my confusion: _PyObject_GenericSetAttrWithDict appears to have logic that gives precedence to a descriptor's __set__ method!! With this in mind, I can't figure out why B.v = 3 blindly overwrites v rather than triggering v.__set__.

Disclaimer 1: I did not rebuild Python from source with printfs, so I'm not completely sure type_setattro is what's being called during B.v = 3.

Disclaimer 2: VocalDescriptor is not intended to exemplify "typical" or "recommended" descriptor definition. It's a verbose no-op to tell me when the methods are being called.

Michael Carilli
  • 371
  • 2
  • 12
  • 1
    For me this prints 3 at the last line... The code works fine – Jab Oct 16 '19 at 19:14
  • 3
    Descriptors apply when accessing attributes from an *instance*, not the class itself. To me, the mystery is why `__get__` worked at all, rather than why `__set__` didn't. – jasonharper Oct 16 '19 at 19:18
  • 1
    @Jab OP is expecting to still invoke the `__get__` method. `B.v = 3` has effectively overwritten the attribute with an `int`. – r.ook Oct 16 '19 at 19:21
  • 2
    @jasonharper Attribute access determines whether `__get__` is called, and the default implementations of `object.__getattribute__` and `type.__getattribute__` invoke `__get__` when using an instance or the class. *Assigning* via `__set__` is instance-only. – chepner Oct 16 '19 at 19:32
  • 1
    @jasonharper I believe descriptors' `__get__` methods are supposed to trigger when invoked from the class itself. This is how @classmethods and @staticmethods are implemented, according to the [how-to guide](https://docs.python.org/3/howto/descriptor.html#static-methods-and-class-methods). @Jab I'm wondering why `B.v = 3` is able to overwrite the class descriptor. Based on the CPython implementation, I expected `B.v = 3` to also trigger `__set__`. – Michael Carilli Oct 16 '19 at 19:33
  • `B.v = 3` is equivalent to `type.__setattr__(B, "v", 3)`, which doesn't perform attribute lookup and so doesn't trigger the `__set__` method. – chepner Oct 16 '19 at 19:34
  • `B.v`, on the other hand, is `type.__getattribute__(B, "v")`, which *does* invoke `__get__`. – chepner Oct 16 '19 at 19:36

3 Answers3

8

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.

wim
  • 338,267
  • 99
  • 616
  • 750
  • @wim Magnificent!! In parallel I was looking once more at the type_setattro call chain. I see that the call to [_PyObject_GenericSetAttrWithDict](https://github.com/python/cpython/blob/3.7/Objects/typeobject.c#L3286) supplies the type (at that point B, in my case). – Michael Carilli Oct 16 '19 at 19:51
  • Within `_PyObject_GenericSetAttrWithDict`, it [pulls the Py_TYPE of B](https://github.com/python/cpython/blob/3.7/Objects/object.c#L1313) as `tp`, which is B's metaclass (`type` in my case), then it's the _metaclass_ `tp` that is treated by the [descriptor short-circuiting logic](https://github.com/python/cpython/blob/3.7/Objects/object.c#L1331-L1340). So the descriptor defined directly on `B` _is not_ seen by that short-circuiting logic (therefore in my original code `__set__` is not called), but a descriptor defined on the metaclass _is_ seen by the short-circuiting logic. – Michael Carilli Oct 16 '19 at 19:51
  • Therefore, in your case where the metaclass has a descriptor, the `__set__` method of that descriptor _is_ called. – Michael Carilli Oct 16 '19 at 19:52
  • “The reason for `B.v` invoking the getter is to allow returning the descriptor instance itself.” I disagree with this. The descriptor instance is returned anyway if the descriptor does not have a `__get__` method, so the `__get__` call on the class descriptor was not introduced for that. The reason for calling `__get__` in this case is precisely to allow a different behaviour than returning the descriptor instance, for example returning a method bound to the class, like `classmethod` does (cf. [this answer](https://stackoverflow.com/a/66790797/2326961)). – Géry Ogam Apr 13 '21 at 07:36
  • @Maggyero Yeah, although I had mentioned *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*, the part before was poorly worded. Updated. – wim Apr 13 '21 at 15:24
4

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__.

chepner
  • 497,756
  • 71
  • 530
  • 681
0

I think that none of the current answers actually answer your question.

Why does setting a descriptor on a class overwrite the descriptor?

Setting or deleting an attribute on a class (or on a subclass of the class) owning a descriptor (e.g. cls.descr = 3 or del cls.descr) overrides that descriptor because it would be impossible to change a faulty descriptor otherwise (e.g. descr.__set__(None, cls, 3) or descr.__delete__(None, cls) raising an exception) since a class dictionary (e.g. cls.__dict__) is a read-only types.MappingProxyType. You can always define a descriptor on the metaclass if you want to override setting or deleting an attribute on a class which is an instance of that metaclass. So __set__ and __delete__ are always passed an instance of the class owning the descriptor, that is why they do not have an owner parameter.

Getting an attribute on a class (or on a subclass of the class) owning a descriptor (e.g. cls.descr) does not override that descriptor because it does not prevent changing a faulty descriptor (e.g. descr.__get__(None, cls) raising an exception). So __get__ is passed either an instance of the class owning the descriptor, or the class (or a subclass of the class) itself, that is why it has an owner parameter.

More information in this answer.

Géry Ogam
  • 6,336
  • 4
  • 38
  • 67