Comprender los descriptores __get__ y __set__ y Python

310

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()
  1. ¿Por qué necesito la clase de descriptor?

  2. ¿Qué es instancey owneraquí? (en __get__). ¿Cuál es el propósito de estos parámetros?

  3. ¿Cómo llamaría / usaría este ejemplo?

Matt Bronson
fuente

Respuestas:

147

El descriptor es cómo propertyse implementa el tipo 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 Temperatura). Por ejemplo:

temp=Temperature()
temp.celsius #calls celsius.__get__

El acceso a la propiedad a la que asignó el descriptor ( celsiusen el ejemplo anterior) llama al método de descriptor apropiado.

instancein __get__es la instancia de la clase (así arriba, __get__recibiría temp, mientras que owneres la clase con el descriptor (así sería Temperature).

Debe usar una clase de descriptor para encapsular la lógica que la alimenta. De esa manera, si el descriptor se usa para almacenar en caché alguna operación costosa (por ejemplo), podría almacenar el valor en sí mismo y no en su clase.

Puede encontrar un artículo sobre descriptores aquí. .

EDITAR: Como jchl señaló en los comentarios, si simplemente lo intentas Temperature.celsius, instancelo será None.

li.davidm
fuente
66
¿Cuál es la diferencia entre selfy instance?
Lemma Prism
2
'instancia' puede ser instancia de cualquier clase, self será instancia de la misma clase.
TheBeginner
3
@LemmaPrism selfes la instancia del descriptor, instancees la instancia de la clase (si se instancia) el descriptor está en ( instance.__class__ is owner).
Tcll
Temperature.celsiusda el valor de 0.0acuerdo con el código celsius = Celsius(). Se llama al descriptor Celsius, por lo que su instancia tiene el valor init 0.0asignado al atributo de clase Temperatura, celsius.
Angel Salazar
109

¿Por qué necesito la clase de descriptor?

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.

¿Qué es instancey owneraquí? (en __get__). ¿Cuál es el propósito de estos parámetros?

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 instancey ownerestán configurados (y puede trabajar ownerdesde instanceentonces, parece un poco inútil). Pero cuando se llama desde una clase, solo ownerse 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 eso ownerque no se necesita allí.

¿Cómo llamaría / usaría 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 necesita asegurarse de que esas divisiones sean / 5.0y / 9.0). Eso da:

100.0
32.0

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

Andrew Cooke
fuente
2
Las conversiones son incorrectas: deberían ser C = 5 (F − 32) / 9, F = 32 + 9C / 5.
musiphil
1
Asegúrese de tener un objeto de temperatura. Hacer lo siguiente arruina las cosas. t1 = Temperatura (190) imprimir t1.celsius t1.celsius = 100 imprimir t1.fahrenheit Ahora, cuando verifica t.celcius y t.fahrenheit, también se modifican. t.celcius es 115 y t.fahrenheit es 32. lo cual es claramente incorrecto. @Eric
Ishan Bhatt
1
@IshanBhatt: Creo que es por el error señalado por musiphil arriba. Además, esta no es mi respuesta
Eric
69

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 clsy 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 descriptorobjeto:

  • obj_instance.descriptorinvoca el
    descriptor.__get__(self, obj_instance, owner_class)retorno a. value
    Así es como funcionan todos los métodos y geten una propiedad.

  • obj_instance.descriptor = valueinvoca el
    descriptor.__set__(self, obj_instance, value)retorno None
    Así es como funciona el setteren una propiedad.

  • del obj_instance.descriptorinvoca el
    descriptor.__delete__(self, obj_instance)retorno None
    Así es como funciona el deleteren una propiedad.

obj_instancees la instancia cuya clase contiene la instancia del objeto descriptor. selfes 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 classmethody staticmethodson 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, propertyes 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
  1. Primero, lo anterior se ve para ver si el atributo es un Descriptor de datos en la clase de la instancia,
  2. Si no, busca ver si el atributo está en el obj_instance's __dict__, entonces
  3. 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()
  1. ¿Por qué necesito la clase de descriptor?

Su descriptor garantiza que siempre tenga un flotante para este atributo de clase Temperaturey que no puede usar delpara 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)
  1. ¿Qué es instancia y propietario aquí? (en get ). ¿Cuál es el propósito de estos parámetros?

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

  1. ¿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 propertydecorador, 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.

Aaron Hall
fuente
1
Bien, aprendí más de esta respuesta (ciertamente también aprendí de otros). Una pregunta sobre esta afirmación "La forma esperada que los programadores de Python más experimentados lograrían este resultado ..." La clase de temperatura que define antes y después de la declaración es idéntica. ¿Extrañé lo que estás haciendo aquí?
Yolo Voe
1
@YoloVoe no, es cierto, agregué algunas palabras entre paréntesis para enfatizar que es una repetición de lo anterior.
Aaron Hall
1
Esta es una respuesta INCREÍBLE. Tendré que leerlo varias veces más, pero siento que mi comprensión de Python aumentó algunas muescas
Lucas Young
20

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 foobaren un instancede Class:

ingrese la descripción de la imagen aquí

Aquí hay dos condiciones importantes:

  • Si la clase de instancetiene una entrada para el nombre del atributo y tiene __get__y __set__.
  • Si noinstance tiene entrada para el nombre del atributo pero la clase tiene una y la tiene .__get__

Ahí es donde entran los descriptores:

  • Descriptores de datos que tienen ambos __get__y __set__.
  • Descriptores sin datos que solo tienen __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:

¿Por qué necesito la clase de descriptor?

¡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 selfque se pasan implícitamente como primer argumento.

def test_function(self):
    return self

class TestClass(object):
    def test_method(self):
        ...

Si busca test_methoduna instancia, obtendrá un "método vinculado":

>>> instance = TestClass()
>>> instance.test_method
<bound method TestClass.test_method of <__main__.TestClass object at ...>>

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):

>>> test_function.__get__(instance, TestClass)
<bound method test_function of <__main__.TestClass object at ...>>

Incluso puede llamar a este "método independiente":

>>> test_function.__get__(instance, TestClass)()
<__main__.TestClass at ...>

¡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. Descuidando getter, settery deleterel propertydescriptor es (del Descriptor Guía práctica "Propiedades" ):

class Property(object):
    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        if doc is None and fget is not None:
            doc = fget.__doc__
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable attribute")
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(obj)

Puesto que es un descriptor de datos se invoca cada vez que se mira el "nombre" de la propertyy simplemente delegados a las funciones decorados con @property, @name.settery @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. propertyes una abstracción para acceso variable de instancia, functionproporciona una abstracción para métodos, staticmethodproporciona 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:

class TypedProperty(object):
    __slots__ = ('_name', '_type')
    def __init__(self, typ):
        self._type = typ

    def __get__(self, instance, klass=None):
        if instance is None:
            return self
        return instance.__dict__[self._name]

    def __set__(self, instance, value):
        if not isinstance(value, self._type):
            raise TypeError(f"Expected class {self._type}, got {type(value)}")
        instance.__dict__[self._name] = value

    def __delete__(self, instance):
        del instance.__dict__[self._name]

    def __set_name__(self, klass, name):
        self._name = name

Entonces puedes usar el descriptor en una clase:

class Test(object):
    int_prop = TypedProperty(int)

Y jugando un poco con eso:

>>> t = Test()
>>> t.int_prop = 10
>>> t.int_prop
10

>>> t.int_prop = 20.0
TypeError: Expected class <class 'int'>, got <class 'float'>

O una "propiedad perezosa":

class LazyProperty(object):
    __slots__ = ('_fget', '_name')
    def __init__(self, fget):
        self._fget = fget

    def __get__(self, instance, klass=None):
        if instance is None:
            return self
        try:
            return instance.__dict__[self._name]
        except KeyError:
            value = self._fget(instance)
            instance.__dict__[self._name] = value
            return value

    def __set_name__(self, klass, name):
        self._name = name

class Test(object):
    @LazyProperty
    def lazy(self):
        print('calculating')
        return 10

>>> t = Test()
>>> t.lazy
calculating
10
>>> t.lazy
10

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.

¿Qué es instancey owneraquí? (en __get__). ¿Cuál es el propósito de estos parámetros?

Depende de cómo busques el atributo. Si busca el atributo en una instancia, entonces:

  • el segundo argumento es la instancia en la que buscas el atributo
  • el tercer argumento es la clase de la instancia

En caso de que busque el atributo en la clase (suponiendo que el descriptor esté definido en la clase):

  • el segundo argumento es None
  • el tercer argumento es la clase donde buscas el atributo

Básicamente, el tercer argumento es necesario si desea personalizar el comportamiento cuando realiza una búsqueda a nivel de clase (porque instancees None).

¿Cómo llamaría / usaría este ejemplo?

Su ejemplo es básicamente una propiedad que solo permite valores que se pueden convertir floaty 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 ):

>>> t1 = Temperature()
>>> t2 = Temperature()

>>> t1.celsius = 20   # setting it on one instance
>>> t2.celsius        # looking it up on another instance
20.0

>>> Temperature.celsius  # looking it up on the class
20.0

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.

MSeifert
fuente
No estoy seguro de si el fondo transparente del gráfico que realmente sufre en modo oscuro debe informarse como un error en stackoverflow.
Tshirtman
@Tshirtman Creo que este es un problema con la imagen en sí. No es completamente transparente ... Lo tomé de la publicación del blog y no sé cómo recrearlo con el fondo transparente adecuado. Es una lástima que se vea tan extraño con el fondo oscuro :(
MSeifert
9

¿Por qué necesito la clase de descriptor?

Inspirado en Fluent Python por Buciano Ramalho

Imagine que tiene 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 defina la clase LineItem de esta manera:

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

wllbll
fuente
1
Caso de uso interesante, ya que muestra cómo usar el descriptor para interactuar con múltiples instancias de usuarios. Inicialmente no entendí el punto importante: se debe crear un atributo con un descriptor en el espacio de nombres de la clase (p weight = Quantity(). Ej. , Pero los valores se deben establecer en el espacio de nombres de la instancia solo con self(p self.weight = 4. Ej. ), De lo contrario, el atributo se volvería al nuevo valor y el descriptor sería descartado. ¡Genial!
minutos
No puedo entender una cosa. Está definiendo weight = Quantity()como variable de clase y su __get__y __set__está trabajando en la variable de instancia. ¿Cómo?
Technocrat
0

Intenté (con cambios menores como se sugirió) el código de la respuesta de Andrew Cooke. (Estoy ejecutando 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 el objeto, lo que hará que el descriptor funcione correctamente, ya que get magic no funciona para las clases de estilo antiguo.

Gregory Kuhn
fuente
1
Los descriptores solo funcionan con nuevas clases de estilo. Para python 2.x, esto significa derivar su clase de "objeto", que es el valor predeterminado en Python 3.
Ivo van der Wijk
0

Vería https://docs.python.org/3/howto/descriptor.html#properties

class Property(object):
    "Emulate PyProperty_Type() in Objects/descrobject.c"

    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        if doc is None and fget is not None:
            doc = fget.__doc__
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable attribute")
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(obj)

    def getter(self, fget):
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        return type(self)(self.fget, self.fset, fdel, self.__doc__)
Yonks Somarl
fuente
1
Esto no responde la pregunta ni proporciona información útil.
Sebastian Nielsen