Repro simple:
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
Esta pregunta tiene un duplicado efectivo , pero el duplicado no fue respondido, y busqué un poco más en la fuente de CPython como ejercicio de aprendizaje. Advertencia: entré en la maleza. Realmente espero poder obtener ayuda de un capitán que conozca esas aguas . Intenté ser lo más explícito posible al rastrear las llamadas que estaba viendo, para mi propio beneficio futuro y el beneficio de futuros lectores.
He visto mucha tinta derramada sobre el comportamiento de los __getattribute__descriptores aplicados, por ejemplo, la precedencia de búsqueda. El fragmento de código Python en "Invocación descriptores" justo debajo For classes, the machinery is in type.__getattribute__()...está de acuerdo más o menos en mi mente con lo que creo es la correspondiente fuente CPython en type_getattroque localicé examinado "tp_slots" a continuación, donde tp_getattro está poblado . Y el hecho de que B.vinicialmente imprima __get__, obj=None, objtype=<class '__main__.B'>tiene sentido para mí.
Lo que no entiendo es, ¿por qué la tarea B.v = 3sobrescribe ciegamente el descriptor, en lugar de desencadenar v.__set__? Traté de rastrear la llamada CPython, comenzando una vez más desde "tp_slots" , luego mirando dónde está poblado tp_setattro y luego mirando type_setattro . type_setattro parece ser una envoltura delgada alrededor de _PyObject_GenericSetAttrWithDict . Y ahí está el quid de mi confusión: ¡ _PyObject_GenericSetAttrWithDictparece tener una lógica que da prioridad al __set__método de un descriptor ! Con esto en mente, no puedo entender por qué B.v = 3sobrescribe ciegamente en vlugar de disparar v.__set__.
Descargo de responsabilidad 1: no reconstruí Python desde el origen con printfs, por lo que no estoy completamente seguro de type_setattrocómo se llama durante B.v = 3.
Descargo de responsabilidad 2: VocalDescriptorno pretende ejemplificar la definición de descriptor "típico" o "recomendado". Es un no-op detallado para decirme cuándo se llaman los métodos.
__get__funcionó, en lugar de por qué __set__no.
__get__método. B.v = 3ha sobrescrito efectivamente el atributo con un int.
__get__se llama y las implementaciones predeterminadas object.__getattribute__e type.__getattribute__invocan __get__cuando se usa una instancia o la clase. La asignación a través de __set__es solo de instancia.
__get__métodos de los descriptores deben activarse cuando se invocan desde la clase misma. Así es como se implementan @classmethods y @staticmethods, de acuerdo con la guía práctica . @Jab Me pregunto por qué B.v = 3es posible sobrescribir el descriptor de la clase. Según la implementación de CPython, esperaba B.v = 3que también se activara __set__.