Descriptores como atributos de instancia en python

A la pregunta:

¿Por qué los descriptores no pueden ser atributos de instancia?

Se ha contestado que:

Los objetos descriptores deben vivir en la clase, no en la instancia

porque esa es la forma en que se implementa el __getattribute__ .

Un ejemplo simple. Considere un descriptor:

 class Prop(object): def __get__(self, obj, objtype=None): if obj is None: return self return obj._value * obj._multiplier def __set__(self, obj, value): if obj is None: return self obj._value = value class Obj(object): val = Prop() def __init__(self): self._value = 1 self._multiplier = 0 

Considere el caso en el que cada obj tiene múltiples Prop: necesitaría usar nombres únicos para identificar los valores y los multiplicadores (como aquí . Tener un objeto descriptor por instancia permitiría almacenar el _multiplier (y el _value ) en el descriptor mismo, simplificando algunas cosas

Para implementar los atributos del descriptor de instancia, necesita:

  1. crear una clase por instancia Ver aquí
  2. anular __getattribute__ Ver aquí

Soy consciente de que se han planteado preguntas similares anteriormente, pero no he encontrado una explicación real:

  1. ¿Por qué Python está diseñado de esta manera?
  2. ¿Cuál es la forma sugerida para almacenar la información que necesita el descriptor, pero es por instancia?

Un montón de funcionalidad avanzada solo funciona cuando se define en una clase en lugar de una instancia; Todos los métodos especiales, por ejemplo. Además de hacer que la evaluación del código sea más eficiente, esto aclara la separación entre instancias y tipos que de otra manera tenderían a colapsarse (porque, por supuesto, todos los tipos son objetos).

No estoy seguro de qué tan recomendado es esto, pero en la instancia podría almacenar un mapeo de la instancia del descriptor al valor de atributo:

 class Prop(object): def __get__(self, obj, objtype=None): if obj is None: return self return obj._value * obj._multiplier[self] def __set__(self, obj, value): if obj is None: return self obj._value = value class Obj(object): val = Prop() def __init__(self): self._value = 1 self._multiplier = {Obj.val: 0} 

Esto tiene ventajas obvias sobre las otras dos opciones sugeridas:

  1. las clases por instancia rompen la orientación de los objetos y aumentan el uso de la memoria;
  2. anular __getattribute__ es ineficiente (ya que todos los atributos de acceso deben pasar por el método especial anulado) y son frágiles.

Como alternativa, podrías usar una propiedad proxy:

 class PerInstancePropertyProxy(object): def __init__(self, prop): self.prop = prop def __get__(self, instance, owner): if instance is None: return self return instance.__dict__[self.prop].__get__(instance, owner) def __set__(self, instance, value): instance.__dict__[self.prop].__set__(instance, value) class Prop(object): def __init__(self, value, multiplier): self.value = value self.multiplier = multiplier def __get__(self, instance, owner): if instance is None: return self return self.value * self.multiplier def __set__(self, instance, value): self.value = value class Obj(object): val = PerInstancePropertyProxy('val') def __init__(self): self.__dict__['val'] = Prop(1.0, 10.0) def prop(self, attr_name): return self.__dict__[attr_name] 

Esta pregunta exacta se planteó en Python-list a principios de este año. Solo voy a citar la respuesta de Ian G. Kelly :

El comportamiento es por diseño. Primero, mantener el comportamiento de los objetos en la definición de clase simplifica la implementación y también hace que las verificaciones de instancia sean más significativas. Para tomar prestado su ejemplo de Registro, si el descriptor “M” está definido por algunas instancias en lugar de por la clase, entonces saber que el objeto “reg” es una instancia de Registro no me dice nada sobre si “reg.M” es una atributo válido o un error. Como resultado, tendré que proteger prácticamente todos los accesos de “reg.M” con una construcción try-except en caso de que “reg” sea el tipo de registro incorrecto.

En segundo lugar, la separación de la clase de la instancia también ayuda a mantener el comportamiento del objeto separado de los datos del objeto. Considere la siguiente clase:

 class ObjectHolder(object): def __init__(self, obj): self.obj = obj 

No te preocupes por lo que esta clase podría ser útil. Solo sepa que está destinado a mantener y proporcionar acceso sin restricciones a objetos de Python arbitrarios:

 >>> holder = ObjectHolder(42) >>> print(holder.obj) 42 >>> holder.obj = range(5) >>> print(holder.obj) [0, 1, 2, 3, 4] 

Dado que la clase debe contener objetos arbitrarios, incluso es válido que alguien quiera almacenar un objeto descriptor allí:

 >>> holder.obj = property(lambda x: x.foo) >>> print(holder.obj)  

Supongamos ahora que Python invocó el protocolo del descriptor para los descriptores almacenados en atributos de instancia:

 >>> holder = ObjectHolder(None) >>> holder.obj = property(lambda x: x.foo) >>> print(holder.obj) Traceback (most recent call last): File "", line 1, in  AttributeError: 'ObjectHolder' object has no attribute 'foo' 

En este caso, el ObjectHolder no mantendría simplemente el objeto de propiedad como datos. El mero hecho de asignar el objeto de propiedad, un descriptor, a un atributo de instancia cambiaría el comportamiento del ObjectHolder. En lugar de tratar “holder.obj” como un atributo de datos simple, comenzaría a invocar el protocolo descriptor en los accesos a “holder.obj” y, en última instancia, los redireccionaría al atributo “holder.foo” inexistente y sin sentido, que ciertamente es No es lo que pretendía el autor de la clase.

Si desea poder admitir varias instancias de un descriptor, simplemente haga que el constructor de ese descriptor tome un argumento de nombre (prefijo) y prefija los atributos agregados con ese nombre. Incluso podría crear un objeto de espacio de nombres (diccionario) dentro de la instancia de clase para contener todas las nuevas instancias de propiedades.

En Python 3.6 esto se puede hacer con bastante facilidad. Tal vez no sea lo que se pretende pero hey, si funciona, ¿verdad? 😉

Python 3.6 agrega el método __set_name__ :

object.__set_name__(self, owner, name)

Llamado en el momento en que se crea el propietario de la clase propietaria. El descriptor ha sido asignado a nombre.

Nuevo en la versión 3.6.

El uso de este nombre para almacenar el valor interno en el dictado de la instancia parece funcionar bien.

 >>> class Prop: ... def __set_name__(self, owner, name): ... self.name = name ... def __get__(self, instance, owner): ... print('get') ... return instance.__dict__.setdefault(self.name, None) ... def __set__(self, instance, value): ... print('set') ... instance.__dict__[self.name] = value ... >>> class A: ... prop = Prop() ... >>> a = A() >>> a.prop = 'spam' set >>> a.prop get 'spam' 

Tenga en cuenta que esto no es una implementación del descriptor completo y, por supuesto, si decide utilizarla es bajo su propio riesgo.