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 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 , comoproperty
, 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
y self
, 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 propietario obj_instance
, con un descriptor
objeto:
obj_instance.descriptor
invoca el
descriptor.__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 el
descriptor.__set__(self, obj_instance, value)
retorno None
Así es como funciona el setter
en una propiedad.
del obj_instance.descriptor
invoca el
descriptor.__delete__(self, obj_instance)
retorno None
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:
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 descriptor sin datos no tiene __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 sin datos
Podemos ver eso classmethod
y staticmethod
son descriptores sin datos:
>>> is_descriptor(classmethod), is_data_descriptor(classmethod)
(True, False)
>>> is_descriptor(staticmethod), is_data_descriptor(staticmethod)
(True, False)
Ambos solo tienen el __get__
método:
>>> has_descriptor_attrs(classmethod), has_descriptor_attrs(staticmethod)
(set(['__get__']), set(['__get__']))
Tenga en cuenta que todas las funciones también son descriptores sin datos:
>>> def foo(): pass
...
>>> is_descriptor(foo), is_data_descriptor(foo)
(True, False)
Descriptor de datos, property
Sin embargo, 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 punteada.
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, busca ver si el atributo está en el
obj_instance
's __dict__
, entonces
- finalmente recurre a un Descriptor sin datos.
La 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:
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?
Su descriptor garantiza que siempre tenga un flotante para este atributo de clase 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. 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):
class Temperature(object):
celsius = 0.0
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:
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 instancia y propietario aquí? (en get ). ¿Cuál es el propósito de estos parámetros?
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.
- ¿Cómo llamaría / usaría 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 eliminar el atributo:
>>> del t2.celsius
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: __delete__
Y no puede asignar una variable que no se pueda convertir a 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 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):
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 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.
self
yinstance
?