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

__get__funcionó, en lugar de por qué__set__no.__get__método.B.v = 3ha sobrescrito efectivamente el atributo con unint.__get__se llama y las implementaciones predeterminadasobject.__getattribute__etype.__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, esperabaB.v = 3que también se activara__set__.Respuestas:
Estás en lo correcto y
B.v = 3simplemente sobrescribe el descriptor con un número entero (como debería).Para
B.v = 3invocar un descriptor, el descriptor debería haberse definido en la metaclase, es decir, entype(B).Para invocar el descriptor
B, usaría una instancia:B().v = 3lo hará.La razón para
B.vinvocar al captador es permitir que se devuelva la instancia del descriptor. Por lo general, haría eso para permitir el acceso al descriptor a través del objeto de clase:Ahora
B.vdevolvería alguna instancia con la<mymodule.VocalDescriptor object at 0xdeadbeef>que puede interactuar. Es literalmente el objeto descriptor, definido como un atributo de clase, y su estadoB.v.__dict__se comparte entre todas las instancias deB.Por supuesto, depende del código del usuario definir exactamente lo que quiere
B.vhacer, regresarselfes solo el patrón común.fuente
__get__está diseñado para ser llamado como atributo de instancia o atributo de clase, pero__set__está diseñado para ser llamado solo como atributo de instancia. Y documentos relevantes: docs.python.org/3/reference/datamodel.html#object.__get___PyObject_GenericSetAttrWithDict, extrae el Py_TYPE de B comotp, que es la metaclase de B (typeen mi caso), luego es la metaclasetpque es tratada por la lógica de cortocircuito del descriptor . Por lo que el descriptor definido directamente enBque no se ve por que un cortocircuito en la lógica (por lo tanto, en mi código original__set__no se conoce), pero un descriptor definido en la metaclase es visto por la lógica de cortocircuito.__set__método de ese descriptor .Salvo cualquier anulación,
B.ves equivalente atype.__getattribute__(B, "v"), mientras queb = B(); b.ves equivalente aobject.__getattribute__(b, "v"). Ambas definiciones invocan el__get__método del resultado si está definido.Tenga en cuenta, pensó, que la llamada a
__get__difiere en cada caso.B.vpasaNonecomo primer argumento, mientrasB().vpasa la instancia en sí. En ambos casosBse pasa como segundo argumento.B.v = 3, por otro lado, es equivalente atype.__setattr__(B, "v", 3), que no invoca__set__.fuente