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

百般思念 提交于 2019-12-11 06:35:20

问题


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.


回答1:


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.




回答2:


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



来源:https://stackoverflow.com/questions/57273021/setting-class-attributes-does-not-use-descriptor-setter

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!