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


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.


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

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.