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_getattro
que localicé examinado "tp_slots" a continuación, donde tp_getattro está poblado . Y el hecho de que B.v
inicialmente imprima __get__, obj=None, objtype=<class '__main__.B'>
tiene sentido para mí.
Lo que no entiendo es, ¿por qué la tarea B.v = 3
sobrescribe 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_GenericSetAttrWithDict
parece 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 = 3
sobrescribe ciegamente en v
lugar 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_setattro
cómo se llama durante B.v = 3
.
Descargo de responsabilidad 2: VocalDescriptor
no 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 = 3
ha 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 = 3
es posible sobrescribir el descriptor de la clase. Según la implementación de CPython, esperabaB.v = 3
que también se activara__set__
.Respuestas:
Estás en lo correcto y
B.v = 3
simplemente sobrescribe el descriptor con un número entero (como debería).Para
B.v = 3
invocar 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 = 3
lo hará.La razón para
B.v
invocar 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.v
devolverí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.v
hacer, regresarself
es 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 (type
en mi caso), luego es la metaclasetp
que es tratada por la lógica de cortocircuito del descriptor . Por lo que el descriptor definido directamente enB
que 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.v
es equivalente atype.__getattribute__(B, "v")
, mientras queb = B(); b.v
es 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.v
pasaNone
como primer argumento, mientrasB().v
pasa la instancia en sí. En ambos casosB
se pasa como segundo argumento.B.v = 3
, por otro lado, es equivalente atype.__setattr__(B, "v", 3)
, que no invoca__set__
.fuente