¿Por qué establecer un descriptor en una clase sobrescribe el descriptor?

10

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.

Michael Carilli
fuente
1
Para mí, esto imprime 3 en la última línea ... El código funciona bien
Jab
3
Los descriptores se aplican al acceder a los atributos de una instancia , no a la clase en sí. Para mí, el misterio es por qué __get__funcionó, en lugar de por qué __set__no.
jasonharper
1
@Jab OP espera seguir invocando el __get__método. B.v = 3ha sobrescrito efectivamente el atributo con un int.
r.ook
2
@jasonharper El acceso al atributo determina si __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.
chepner
@jasonharper Creo que los __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__.
Michael Carilli

Respuestas:

6

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, en type(B).

>>> class BMeta(type): 
...     v = VocalDescriptor() 
... 
>>> class B(metaclass=BMeta): 
...     pass 
... 
>>> B.v = 3 
__set__

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:

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__')

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 estado B.v.__dict__se comparte entre todas las instancias de B.

Por supuesto, depende del código del usuario definir exactamente lo que quiere B.vhacer, regresar selfes solo el patrón común.

wim
fuente
1
Para completar esta respuesta, agregaría que __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__
sanyash
@wim Magnífico !! En paralelo, estaba mirando una vez más la cadena de llamadas type_setattro. Veo que la llamada a _PyObject_GenericSetAttrWithDict proporciona el tipo (en ese punto B, en mi caso).
Michael Carilli
Dentro _PyObject_GenericSetAttrWithDict, extrae el Py_TYPE de B como tp, que es la metaclase de B ( typeen mi caso), luego es la metaclase tp que es tratada por la lógica de cortocircuito del descriptor . Por lo que el descriptor definido directamente en B 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.
Michael Carilli
Por lo tanto, en su caso donde la metaclase tiene un descriptor, se llama al __set__método de ese descriptor .
Michael Carilli
@sanyash no dude en editar directamente.
wim
3

Salvo cualquier anulación, B.ves equivalente a type.__getattribute__(B, "v"), mientras que b = B(); b.ves equivalente a object.__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.vpasa Nonecomo primer argumento, mientras B().vpasa la instancia en sí. En ambos casos Bse pasa como segundo argumento.

B.v = 3, por otro lado, es equivalente a type.__setattr__(B, "v", 3), que no invoca __set__.

chepner
fuente