Estoy tratando de entender qué son los descriptores de Python y para qué sirven. 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 descriptor?
¿Qué es
instance
yowner
aquí? (en__get__
). ¿Cuál es el propósito de estos parámetros?¿Cómo llamaría / usaría este ejemplo?
fuente
self
yinstance
?self
es la instancia del descriptor,instance
es la instancia de la clase (si se instancia) el descriptor está en (instance.__class__ is owner
).Temperature.celsius
da el valor de0.0
acuerdo con el códigocelsius = Celsius()
. Se llama al descriptor Celsius, por lo que su instancia tiene el valor init0.0
asignado al atributo de clase Temperatura, celsius.Le da control adicional sobre cómo funcionan los atributos. Si estás acostumbrado a obtener y establecer en Java, por ejemplo, entonces es la forma en que Python hace eso. Una ventaja es que se ve a los usuarios como un atributo (no hay cambio en la sintaxis). Entonces puede comenzar con un atributo ordinario y luego, cuando necesite hacer algo elegante, cambiar 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 tanto, puede 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 al lanzar una excepción
__set__
, haciendo que el "atributo" sea de solo lectura.Esto es bastante sutil (y la razón por la que estoy escribiendo una nueva respuesta aquí: encontré esta pregunta mientras me preguntaba lo mismo y no encontré 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 ambos
instance
yowner
están configurados (y puede trabajarowner
desdeinstance
entonces, parece un poco inútil). Pero cuando se llama desde una clase, soloowner
se establece, por lo que está allí.Esto solo es necesario
__get__
porque es el único al que se puede llamar en una clase. Si establece el valor de la clase, establece el descriptor mismo. Del mismo modo para la eliminación. Es por esoowner
que no se necesita allí.Bueno, aquí hay un truco genial usando clases similares:
(Estoy usando Python 3; para python 2 necesita asegurarse de que esas divisiones sean
/ 5.0
y/ 9.0
). Eso da:Ahora hay otras formas, posiblemente mejores, de lograr el mismo efecto en Python (por ejemplo, si Celsius fuera una propiedad, que es el mismo mecanismo básico pero coloca toda la fuente dentro de la clase de Temperatura), pero eso muestra lo que se puede hacer ...
fuente
Los descriptores son atributos de clase (como propiedades o métodos) con cualquiera de los siguientes métodos especiales:
__get__
(método no descriptor de datos, por ejemplo, en un método / función)__set__
(método del 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__
objeto de la clase).Los objetos descriptores se pueden usar para administrar mediante programación los resultados de una búsqueda punteada (p. Ej.
foo.descriptor
) en una expresión normal, una asignación e incluso una eliminación.Funciones / métodos, los métodos consolidados,
property
,classmethod
, ystaticmethod
todo el uso de estos métodos especiales para controlar la forma en que se accede a través las operaciones de búsqueda de 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 calculara previamente cada atributo posible.Otro descriptor de datos, a
member_descriptor
, creado por__slots__
, permite el ahorro de memoria al permitir que la clase almacene datos en una estructura de datos similar a una tupla mutable en lugar de la más flexible pero que consume mucho espacio__dict__
.Descriptores no de datos, por lo general de instancia, clase y métodos estáticos, obtienen sus primeros argumentos implícitos (por lo general el nombre
cls
yself
, respectivamente) a partir de su método descriptor no de datos,__get__
.La mayoría de los usuarios de Python necesitan aprender solo el uso simple, y no tienen necesidad de aprender o comprender más la implementación de descriptores.
En profundidad: ¿qué son los descriptores?
Un descriptor es un objeto con cualquiera de los métodos siguientes (
__get__
,__set__
o__delete__
), destinado a ser utilizado a través de puntos-lookup como si fuera un atributo típico de una instancia. Para un objeto propietarioobj_instance
, con undescriptor
objeto:obj_instance.descriptor
invoca eldescriptor.__get__(self, obj_instance, owner_class)
retorno a.value
Así es como funcionan todos los métodos y
get
en una propiedad.obj_instance.descriptor = value
invoca eldescriptor.__set__(self, obj_instance, value)
retornoNone
Así es como funciona el
setter
en una propiedad.del obj_instance.descriptor
invoca eldescriptor.__delete__(self, obj_instance)
retornoNone
Así es como funciona el
deleter
en una propiedad.obj_instance
es la instancia cuya clase contiene la instancia del objeto descriptor.self
es la instancia del descriptor (probablemente solo uno para la clase deobj_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:
Un descriptor de datos tiene un
__set__
y / o__delete__
.Un descriptor sin datos no tiene
__set__
ni__delete__
.Ejemplos de objetos descriptores incorporados:
classmethod
staticmethod
property
Descriptores sin datos
Podemos ver eso
classmethod
ystaticmethod
son descriptores sin datos:Ambos solo tienen el
__get__
método:Tenga en cuenta que todas las funciones también son descriptores sin datos:
Descriptor de datos,
property
Sin embargo,
property
es un descriptor de datos:Orden de búsqueda punteada
Estas son distinciones importantes , ya que afectan el orden de búsqueda para una búsqueda punteada.
obj_instance
's__dict__
, entoncesLa consecuencia de este orden de búsqueda es que los Descriptores que no son de datos, como 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 de la pregunta
Aquí está su código, seguido de sus preguntas y respuestas a cada uno:
Su descriptor garantiza que siempre tenga un flotante para este atributo de clase
Temperature
y que no puede usardel
para eliminar el atributo:De lo contrario, sus descriptores ignoran la clase de propietario y las instancias del propietario, en su lugar, almacenan el estado en el descriptor. Podría compartir fácilmente el estado en todas las instancias con un atributo de clase simple (siempre y cuando siempre lo configure como flotante para la clase y nunca lo elimine, o se sienta cómodo con que los usuarios de su código lo hagan):
Esto le proporciona exactamente el mismo comportamiento que en su ejemplo (vea la respuesta a la pregunta 3 a continuación), pero usa un pitón incorporado (
property
), y se consideraría más idiomático:instance
es la instancia del propietario que llama al descriptor. El propietario es la clase en la que se usa el objeto descriptor para administrar el acceso al punto de datos. Consulte las descripciones de los métodos especiales que definen los descriptores al lado del primer párrafo de esta respuesta para obtener nombres de variables más descriptivos.Aquí hay una demostración:
No puedes eliminar el atributo:
Y no puede asignar una variable que no se pueda convertir a flotante:
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 que los programadores de Python más experimentados lograrían este resultado sería usar el
property
decorador, que utiliza los mismos descriptores bajo el capó, pero trae el comportamiento a la implementación de la clase de propietario (nuevamente, como se definió anteriormente):Que tiene exactamente el mismo comportamiento esperado del código original:
Conclusión
Hemos cubierto los atributos que definen los descriptores, la diferencia entre los descriptores de datos y no, los objetos incorporados que los usan 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 propiedades) si lo considera necesario.
fuente
Antes de entrar en los detalles de los descriptores, puede ser importante saber cómo funciona la búsqueda de atributos en Python. Esto supone que la clase no tiene metaclase y que usa la implementación predeterminada de
__getattribute__
(ambas pueden usarse para "personalizar" el comportamiento).La mejor ilustración de la búsqueda de atributos (en Python 3.xo para clases de estilo nuevo en Python 2.x) en este caso es de Entender las metaclases de Python (código de registro de ionel) . La imagen se utiliza
:
como sustituto de "búsqueda de atributos no personalizables".Esto representa la búsqueda de un atributo
foobar
en uninstance
deClass
:Aquí hay dos condiciones importantes:
instance
tiene una entrada para el nombre del atributo y tiene__get__
y__set__
.instance
tiene entrada para el nombre del atributo pero la clase tiene una y la tiene .__get__
Ahí es donde entran los descriptores:
__get__
y__set__
.__get__
.En ambos casos, el valor devuelto se
__get__
llama con la instancia como primer argumento y la clase como segundo argumento.La búsqueda es aún más complicada para la búsqueda de atributos de clase (véase, por ejemplo, la búsqueda de atributos de clase (en el blog mencionado anteriormente) ).
Pasemos a sus preguntas específicas:
¡En la mayoría de los casos no necesita escribir clases de descriptor! Sin embargo, probablemente sea un usuario final muy habitual. Por ejemplo funciones. Las funciones son descriptores, así es como las funciones se pueden usar como métodos
self
que se pasan implícitamente como primer argumento.Si busca
test_method
una instancia, obtendrá un "método vinculado":Del mismo modo, también podría vincular una función invocando su
__get__
método manualmente (no se recomienda realmente, solo con fines ilustrativos):Incluso puede llamar a este "método independiente":
¡Tenga en cuenta que no proporcioné ningún argumento y la función sí devolvió la instancia que había vinculado!
¡Las funciones son descriptores sin datos !
Algunos ejemplos incorporados de un descriptor de datos serían
property
. Descuidandogetter
,setter
ydeleter
elproperty
descriptor es (del Descriptor Guía práctica "Propiedades" ):Puesto que es un descriptor de datos se invoca cada vez que se mira el "nombre" de la
property
y simplemente delegados a las funciones decorados con@property
,@name.setter
y@name.deleter
(si está presente).Hay varios otros descriptores en la biblioteca estándar, por ejemplo
staticmethod
,classmethod
.El punto de los descriptores es fácil (aunque rara vez los necesita): Código común abstracto para acceso a atributos.
property
es una abstracción para acceso variable de instancia,function
proporciona una abstracción para métodos,staticmethod
proporciona una abstracción para métodos que no necesitan acceso a instancia yclassmethod
proporciona una abstracción para métodos que necesitan acceso a clases en lugar de acceso a instancia (esto es un poco simplificado).Otro ejemplo sería una propiedad de clase .
Un ejemplo divertido (usando
__set_name__
Python 3.6) también podría ser una propiedad que solo permite un tipo específico:Entonces puedes usar el descriptor en una clase:
Y jugando un poco con eso:
O una "propiedad perezosa":
Estos son casos en los que mover la lógica a un descriptor común podría tener sentido, sin embargo, uno también podría resolverlos (pero tal vez repitiendo algún código) con otros medios.
Depende de cómo busques el atributo. Si busca el atributo en una instancia, entonces:
En caso de que busque el atributo en la clase (suponiendo que el descriptor esté definido en la clase):
None
Básicamente, el tercer argumento es necesario si desea personalizar el comportamiento cuando realiza una búsqueda a nivel de clase (porque
instance
esNone
).Su ejemplo es básicamente una propiedad que solo permite valores que se pueden convertir
float
y que se comparte entre todas las instancias de la clase (y en la clase, aunque uno solo puede usar el acceso de "lectura" en la clase; de lo contrario, reemplazaría la instancia del descriptor ):Es por eso que los descriptores generalmente usan el segundo argumento (
instance
) para almacenar el valor para evitar compartirlo. Sin embargo, en algunos casos puede ser deseable compartir un valor entre instancias (aunque no puedo pensar en un escenario en este momento). Sin embargo, prácticamente no tiene sentido para una propiedad centígrada en una clase de temperatura ... excepto tal vez como un ejercicio puramente académico.fuente
Inspirado en Fluent Python por Buciano Ramalho
Imagine que tiene una clase como esta
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
luego defina la clase LineItem de esta manera:
y podemos extender la clase Cantidad para hacer una validación más común
fuente
weight = Quantity()
. Ej. , Pero los valores se deben establecer en el espacio de nombres de la instancia solo conself
(pself.weight = 4
. Ej. ), De lo contrario, el atributo se volvería al nuevo valor y el descriptor sería descartado. ¡Genial!weight = Quantity()
como variable de clase y su__get__
y__set__
está trabajando en la variable de instancia. ¿Cómo?Intenté (con cambios menores como se sugirió) el código de la respuesta de Andrew Cooke. (Estoy ejecutando python 2.7).
El código:
El resultado:
Con Python anterior a 3, asegúrate de subclasificar el objeto, lo que hará que el descriptor funcione correctamente, ya que get magic no funciona para las clases de estilo antiguo.
fuente
Vería https://docs.python.org/3/howto/descriptor.html#properties
fuente