Entendiendo los descriptores__get__ y__set__ y Python
(5)
Estoy tratando de entender qué son los descriptores de Python y para qué pueden ser útiles. Sin embargo, estoy fallando en eso. Entiendo cómo funcionan, pero aquí están mis dudas. Considere el siguiente código:
class Celsius(object):
def __init__(self, value=0.0):
self.value = float(value)
def __get__(self, instance, owner):
return self.value
def __set__(self, instance, value):
self.value = float(value)
class Temperature(object):
celsius = Celsius()
¿Por qué necesito la clase de descriptores? Por favor explique usando este ejemplo o el que crea que es mejor.
¿Qué es la
instance
y elowner
aquí? (en__get__
). Entonces mi pregunta es, ¿cuál es el propósito del tercer parámetro aquí?¿Cómo llamo / uso este ejemplo?
Estoy tratando de entender qué son los descriptores de Python y para qué pueden ser útiles.
Los descriptores son atributos de clase (como propiedades o métodos) con cualquiera de los siguientes métodos especiales:
-
__get__
(método sin descriptor de datos, por ejemplo en un método / función) -
__set__
(método descriptor de datos, por ejemplo, en una instancia de propiedad) -
__delete__
(método descriptor de datos)
Estos objetos descriptores se pueden usar como atributos en otras definiciones de clase de objeto. (Es decir, viven en el __dict__
del objeto de clase).
Los objetos descriptores se pueden usar para administrar mediante programación los resultados de una búsqueda punteada (por ejemplo, foo.descriptor
) en una expresión normal, una asignación e incluso una eliminación.
Las funciones / métodos, los métodos enlazados, la property
, el classmethod
y el staticmethod
utilizan todos estos métodos especiales para controlar cómo se accede a ellos a través de la búsqueda por puntos.
Un descriptor de datos , como property
, puede permitir una evaluación perezosa de los atributos en función de un estado más simple del objeto, permitiendo que las instancias usen menos memoria que si hubiera calculado previamente cada atributo posible.
Otro descriptor de datos, un member_descriptor
, creado por __slots__
, permite ahorrar memoria al permitir que la clase almacene datos en una estructura de datos similar a una tupla mutable en lugar de la __dict__ más flexible pero que consume más __dict__
.
Los descriptores que no son de datos, por lo general los métodos de instancia, clase y estáticos, obtienen sus primeros argumentos implícitos (normalmente denominados cls
y self
, respectivamente) de su método de descriptor sin datos, __get__
.
La mayoría de los usuarios de Python necesitan aprender solo el uso simple, y no tienen necesidad de aprender o entender la implementación de descriptores más.
En profundidad: ¿Qué son los descriptores?
Un descriptor es un objeto con cualquiera de los siguientes métodos ( __get__
, __set__
o __delete__
), destinado a ser usado a través de una búsqueda por puntos como si fuera un atributo típico de una instancia. Para un objeto propietario, obj_instance
, con un objeto descriptor
:
obj_instance.descriptor
invoca
descriptor.__get__(self, obj_instance, owner_class)
devolviendo unvalue
Así es como funcionan todos los métodos y la forma deget
una propiedad.obj_instance.descriptor = value
invoca
descriptor.__set__(self, obj_instance, value)
devolviendoNone
Así es como funciona elsetter
en una propiedad.del obj_instance.descriptor
invoca
descriptor.__delete__(self, obj_instance)
devolviendoNone
Así es como funciona eldeleter
en una propiedad.
obj_instance
es la instancia cuya clase contiene la instancia del objeto descriptor. self
es la instancia del descriptor (probablemente solo una para la clase de obj_instance
)
Para definir esto con código, un objeto es un descriptor si el conjunto de sus atributos se cruza con cualquiera de los atributos requeridos:
def has_descriptor_attrs(obj):
return set([''__get__'', ''__set__'', ''__delete__'']).intersection(dir(obj))
def is_descriptor(obj):
"""obj can be instance of descriptor or the descriptor class"""
return bool(has_descriptor_attrs(obj))
Un descriptor de datos tiene un __set__
y / o __delete__
.
Un no descriptor de datos no tiene ni __set__
ni __delete__
.
def has_data_descriptor_attrs(obj):
return set([''__set__'', ''__delete__'']) & set(dir(obj))
def is_data_descriptor(obj):
return bool(has_data_descriptor_attrs(obj))
Ejemplos de objetos descriptores incorporados:
-
classmethod
-
staticmethod
-
property
- funciones en general
Descriptores que no son de datos
Podemos ver que el classmethod
y el staticmethod
no son staticmethod
de datos:
>>> is_descriptor(classmethod), is_data_descriptor(classmethod)
(True, False)
>>> is_descriptor(staticmethod), is_data_descriptor(staticmethod)
(True, False)
Ambos solo tienen el método __get__
:
>>> has_descriptor_attrs(classmethod), has_descriptor_attrs(staticmethod)
(set([''__get__'']), set([''__get__'']))
Tenga en cuenta que todas las funciones son también no descriptores de datos:
>>> def foo(): pass
...
>>> is_descriptor(foo), is_data_descriptor(foo)
(True, False)
Descriptor de datos, property
Sin embargo, la property
es un descriptor de datos:
>>> is_data_descriptor(property)
True
>>> has_descriptor_attrs(property)
set([''__set__'', ''__get__'', ''__delete__''])
Orden de búsqueda punteada
Estas son distinciones importantes, ya que afectan el orden de búsqueda para una búsqueda de puntos.
obj_instance.attribute
- Primero, lo anterior se ve para ver si el atributo es un descriptor de datos en la clase de la instancia,
- Si no, mira para ver si el atributo está en el
obj_instance
de__dict__
, entonces - finalmente recae en un descriptor no de datos.
La consecuencia de este orden de búsqueda es que los Descriptores de datos no-como las funciones / métodos pueden ser anulados por instancias .
Resumen y próximos pasos
Hemos aprendido que los descriptores son objetos con cualquiera de __get__
, __set__
o __delete__
. Estos objetos descriptores se pueden usar como atributos en otras definiciones de clase de objeto. Ahora veremos cómo se usan, usando su código como ejemplo.
Análisis de código a partir de la pregunta.
Aquí está su código, seguido de sus preguntas y respuestas a cada uno:
class Celsius(object):
def __init__(self, value=0.0):
self.value = float(value)
def __get__(self, instance, owner):
return self.value
def __set__(self, instance, value):
self.value = float(value)
class Temperature(object):
celsius = Celsius()
- ¿Por qué necesito la clase de descriptores? Por favor explique usando este ejemplo o el que crea que es mejor.
Su descriptor garantiza que siempre tenga un valor flotante para este atributo de clase de Temperature
y que no puede usar del
para eliminar el atributo:
>>> t1 = Temperature()
>>> del t1.celsius
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: __delete__
De lo contrario, sus descriptores ignoran la clase de propietario y las instancias del propietario, en su lugar, almacenan el estado en el descriptor. Puede compartir el estado de todas las instancias con la misma facilidad con un simple atributo de clase (siempre y cuando siempre lo establezca como flotante para la clase y nunca lo elimine, o se sienta cómodo con los usuarios de su código al hacerlo):
class Temperature(object):
celsius = 0.0
Esto le da exactamente el mismo comportamiento que su ejemplo (consulte la respuesta a la pregunta 3 a continuación), pero utiliza un builtin de Pythons ( property
), y se consideraría más idiomático:
class Temperature(object):
_celsius = 0.0
@property
def celsius(self):
return type(self)._celsius
@celsius.setter
def celsius(self, value):
type(self)._celsius = float(value)
- ¿Qué es la
instance
y elowner
aquí? (en__get__
). Entonces mi pregunta es, ¿cuál es el propósito del tercer parámetro aquí?
instance
es la instancia del propietario que está llamando al descriptor. El propietario es la clase en la que se utiliza el objeto descriptor para administrar el acceso al punto de datos. Consulte las descripciones de los métodos especiales que definen los descriptores junto al primer párrafo de esta respuesta para obtener nombres de variables más descriptivos.
- ¿Cómo llamo / uso este ejemplo?
Aquí hay una demostración:
>>> t1 = Temperature()
>>> t1.celsius
0.0
>>> t1.celsius = 1
>>>
>>> t1.celsius
1.0
>>> t2 = Temperature()
>>> t2.celsius
1.0
No puedes borrar el atributo:
>>> del t2.celsius
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: __delete__
Y no puedes asignar una variable que no se pueda convertir en un flotante:
>>> t1.celsius = ''0x02''
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 7, in __set__
ValueError: invalid literal for float(): 0x02
De lo contrario, lo que tiene aquí es un estado global para todas las instancias, que se administra mediante la asignación a cualquier instancia.
La forma esperada en que la mayoría de los programadores de Python lograrían este resultado sería utilizar el decorador de property
, que utiliza los mismos descriptores bajo el capó, pero lleva el comportamiento a la implementación de la clase de propietario (de nuevo, como se definió anteriormente):
class Temperature(object):
_celsius = 0.0
@property
def celsius(self):
return type(self)._celsius
@celsius.setter
def celsius(self, value):
type(self)._celsius = float(value)
Que tiene exactamente el mismo comportamiento esperado del código original:
>>> t1 = Temperature()
>>> t2 = Temperature()
>>> t1.celsius
0.0
>>> t1.celsius = 1.0
>>> t2.celsius
1.0
>>> del t1.celsius
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: can''t delete attribute
>>> t1.celsius = ''0x02''
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 8, in celsius
ValueError: invalid literal for float(): 0x02
Conclusión
Hemos cubierto los atributos que definen los descriptores, la diferencia entre los descriptores de datos y los que no lo son, los objetos integrados que los utilizan y las preguntas específicas sobre el uso.
Entonces, de nuevo, ¿cómo usarías el ejemplo de la pregunta? Espero que no lo hagas. Espero que comience con mi primera sugerencia (un atributo de clase simple) y continúe con la segunda sugerencia (el decorador de la propiedad) si lo considera necesario.
¿Por qué necesito la clase de descriptores? Por favor explique usando este ejemplo o el que crea que es mejor.
Inspirado en Fluent Python por Buciano Ramalho
Imaging tienes una clase como esta
class LineItem:
price = 10.9
weight = 2.1
def __init__(self, name, price, weight):
self.name = name
self.price = price
self.weight = weight
item = LineItem("apple", 2.9, 2.1)
item.price = -0.9 # it''s price is negative, you need to refund to your customer even you delivered the apple :(
item.weight = -0.8 # negative weight, it doesn''t make sense
Deberíamos validar el peso y el precio para evitar asignarles un número negativo, podemos escribir menos código si usamos el descriptor como un proxy como este
class Quantity(object):
__index = 0
def __init__(self):
self.__index = self.__class__.__index
self._storage_name = "quantity#{}".format(self.__index)
self.__class__.__index += 1
def __set__(self, instance, value):
if value > 0:
setattr(instance, self._storage_name, value)
else:
raise ValueError(''value should >0'')
def __get__(self, instance, owner):
return getattr(instance, self._storage_name)
luego define la clase LineItem así:
class LineItem(object):
weight = Quantity()
price = Quantity()
def __init__(self, name, weight, price):
self.name = name
self.weight = weight
self.price = price
y podemos extender la clase Cantidad para hacer una validación más común
¿Por qué necesito la clase de descriptores? Por favor explique usando este ejemplo o el que crea que es mejor.
te da control adicional sobre cómo funcionan los atributos. por ejemplo, si estás acostumbrado a obtener y establecer en java, es la forma en que Python hace eso. una ventaja es que se ve a los usuarios como un atributo (no hay cambios en la sintaxis). para que pueda comenzar con un atributo ordinario y luego, cuando necesite hacer algo sofisticado, cambie a un descriptor.
un atributo es solo un valor mutable un descriptor le permite ejecutar código arbitrario al leer o configurar (o eliminar) un valor. por lo que podría imaginar usarlo para asignar un atributo a un campo en una base de datos, por ejemplo, una especie de ORM.
otro uso podría ser negarse a aceptar un nuevo valor lanzando una excepción en __set__
- haciendo que el "atributo" sea de solo lectura.
¿Qué es la instancia y el propietario aquí? (en
__get__
). Entonces mi pregunta es, ¿cuál es el propósito del tercer parámetro aquí?
esto es bastante sutil (y la razón por la que escribo una nueva respuesta aquí: encontré esta pregunta mientras me preguntaba lo mismo y no encontraba la respuesta existente tan genial).
un descriptor se define en una clase, pero normalmente se llama desde una instancia. cuando se llama desde una instancia, tanto la instance
como el owner
están configurados (y usted puede calcular el owner
desde la instance
por lo que parece un poco inútil). pero cuando se llama desde una clase, solo se establece el owner
, por lo que está allí.
esto solo es necesario para __get__
porque es el único al que se puede llamar en una clase. Si establece el valor de la clase, establece el descriptor en sí. de manera similar para la eliminación. por lo que el owner
no es necesario allí.
¿Cómo llamo / uso este ejemplo?
Bueno, aquí hay un truco genial usando clases similares:
class Celsius:
def __get__(self, instance, owner):
return 5 * (instance.fahrenheit - 32) / 9
def __set__(self, instance, value):
instance.fahrenheit = 32 + 9 * value / 5
class Temperature:
celsius = Celsius()
def __init__(self, initial_f):
self.fahrenheit = initial_f
t = Temperature(212)
print(t.celsius)
t.celsius = 0
print(t.fahrenheit)
(Estoy usando python 3; para python 2 necesitas asegurarte de que esas divisiones sean / 5.0
y / 9.0
). eso da:
100.0
32.0
ahora hay otras formas, posiblemente mejores para lograr el mismo efecto en python (por ejemplo, si centígrados fueran una propiedad, que es el mismo mecanismo básico pero coloca toda la fuente dentro de la clase de Temperatura), pero eso demuestra lo que se puede hacer ...
El descriptor es cómo se implementa el tipo de property
de Python. Un descriptor simplemente implementa __get__
, __set__
, etc. y luego se agrega a otra clase en su definición (como lo hizo anteriormente con la clase de Temperatura). Por ejemplo:
temp=Temperature()
temp.celsius #calls celsius.__get__
Acceder a la propiedad a la que asignó el descriptor (en el ejemplo anterior) se llama al método del descriptor apropiado.
instance
en __get__
es la instancia de la clase (por lo tanto, __get__
recibiría temp
, mientras que owner
es la clase con el descriptor (por lo tanto, sería Temperature
).
Debe usar una clase de descriptores para encapsular la lógica que la alimenta. De esa manera, si el descriptor se utiliza para almacenar en caché una operación costosa (por ejemplo), podría almacenar el valor en sí mismo y no en su clase.
Un artículo sobre descriptores se puede encontrar here .
EDITAR: Como jchl señaló en los comentarios, si simplemente prueba Temperature.celsius
, la instance
será None
.
Intenté (con pequeños cambios como se sugiere) el código de la respuesta de Andrew Cooke. (Estoy corriendo python 2.7).
El código:
#!/usr/bin/env python
class Celsius:
def __get__(self, instance, owner): return 9 * (instance.fahrenheit + 32) / 5.0
def __set__(self, instance, value): instance.fahrenheit = 32 + 5 * value / 9.0
class Temperature:
def __init__(self, initial_f): self.fahrenheit = initial_f
celsius = Celsius()
if __name__ == "__main__":
t = Temperature(212)
print(t.celsius)
t.celsius = 0
print(t.fahrenheit)
El resultado:
C:/Users/gkuhn/Desktop>python test2.py
<__main__.Celsius instance at 0x02E95A80>
212
Con Python anterior a 3, asegúrate de subclasificar desde un objeto que haga que el descriptor funcione correctamente, ya que el método get magic no funciona para clases de estilo antiguo.