# Guía práctica de uso de los descriptores

Autor

  Raymond Hettinger

Contacto

  <python at rcn dot com>

Contenido

- Guía práctica de uso de los descriptores

  - Resumen

  - Definición e introducción

  - Protocolo descriptor

  - Invocar descriptores

  - Ejemplo de descriptor

  - Propiedades

  - Funciones y métodos

  - Métodos estáticos y métodos de clase

## Resumen

Definir los descriptores, resumir el protocolo y mostrar como los descriptores son llamados. Estudiar un descriptor personalizado y varios descriptores de Python incorporados, incluidas funciones, propiedades, métodos estáticos y métodos de clase. Mostrar como funciona cada uno proporcionando un equivalente puro de Python y un ejemplo de aplicación.

Aprender acerca de los descriptores no solo brinda acceso a un conjunto de herramientas mayor, sino que genera una comprensión más profunda de como funciona Python y una apreciación sobre la elegancia de su diseño.

## Definición e introducción

En general, un descriptor es un atributo de objeto con «comportamiento vinculante», dónde el acceso al atributo ha sido reemplazado por métodos en el protocolo del descriptor. Esos métodos son `__get__()`, `__set__()` y `__delete__()`. Si alguno de esos métodos está definido para un objeto, se dice que es un descriptor.

El comportamiento predeterminado para el acceso a los atributos es obtener, establecer o eliminar el atributo del diccionario de un objeto. Por ejemplo, `a.x` tiene una cadena de búsqueda que comienza con `a.__dict__['x']`, luego `type(a).__dict__['x']` y continúa a través de las clases base de `type(a)` excluyendo metaclases. Si el valor buscado es un objeto que define uno de los métodos del descriptor, entonces Python puede anular el comportamiento predeterminado e invocar el método del descriptor en su lugar. El lugar donde ésto ocurre en la cadena de precedencia depende de qué métodos del descriptor fueron definidos.

Los descriptores son un potente protocolo de propósito general. Son el mecanismo detrás de las propiedades, métodos, métodos estáticos, métodos de clase y `super()`. Se utilizan en todo Python para implementar las clases de nuevo estilo introducidas en la versión 2.2. Los descriptores simplifican el código C subyacente y ofrecen un conjunto flexible de nuevas herramientas para los programas de Python cotidianos.

## Protocolo descriptor

`descr.__get__(self, obj, type=None) -> value`

`descr.__set__(self, obj, value) -> None`

`descr.__delete__(self, obj) -> None`

Eso es todo lo que hay que hacer. Si se define cualquiera de estos métodos, el objeto se considera un descriptor y puede anular el comportamiento predeterminado al ser buscado como un atributo.

Si un objeto define `__set__()` o `__delete__()`, se considera un descriptor de datos. Los descriptores que solo definen `__get__()` se denominan descriptores de no-datos (normalmente se utilizan para métodos, pero son posibles otros usos).

Los descriptores de datos y de no-datos difieren en como se calculan las anulaciones con respecto a las entradas en el diccionario de una instancia. Si el diccionario de una instancia tiene una entrada con el mismo nombre que un descriptor de datos, el descriptor de datos tiene prioridad. Si el diccionario de una instancia tiene una entrada con el mismo nombre que un descriptor de no-datos, la entrada del diccionario tiene prioridad.

Para crear un descriptor de datos de solo lectura, se define tanto `__get__()` como `__set__()` donde `__set__()` lanza un error `AttributeError` cuando es llamado. Definir el método `__set__()` de forma que lance una excepción genérica es suficiente para convertirlo en un descriptor de datos.

## Invocar descriptores

Un descriptor puede ser llamado directamente mediante el nombre de su método. Por ejemplo `d.__get__(obj)`.

Alternativamente, es más común que un descriptor se invoque automáticamente al acceder a un atributo. Por ejemplo, `obj.d` busca `d` en el diccionario de `obj`. Si `d` define el método `__get__()`, entonces se invoca `d.__get__(obj)` de acuerdo con las reglas de precedencia que se enumeran a continuación.

Los detalles de la invocación dependen de si `obj` es un objeto o una clase.

Para los objetos, el mecanismo se encuentra en `object.__getattribute__()` que transforma `b.x` en `type(b).__dict__['x'].__get__(b, type(b))`. La implementación funciona a través de una cadena de precedencia que da a los descriptores de datos prioridad sobre las variables de instancia, a las variables de instancia prioridad sobre los descriptores de no-datos y asigna la prioridad más baja a `__getattr__()` si se proporciona. La implementación completa en C se puede encontrar en `PyObject_GenericGetAttr()` en Objects/object.c.

Para clases, el mecanismo se define en `type.__getattribute__()` que transforma `B.x` en `B.__dict__['x'].__get__(None, B)`. En Python puro, quedaría así:

    def __getattribute__(self, key):
        "Emulate type_getattro() in Objects/typeobject.c"
        v = object.__getattribute__(self, key)
        if hasattr(v, '__get__'):
            return v.__get__(None, self)
        return v

Los puntos importantes a recordar son:

- los descriptores son invocados por el método `__getattribute__()`

- redefinir `__getattribute__()` evita las llamadas automáticas al descriptor

- `object.__getattribute__()` y `type.__getattribute__()` realizan diferentes llamadas a `__get__()`.

- los descriptores de datos siempre anulan los diccionarios de instancia.

- los descriptores de no-datos pueden ser reemplazados por los diccionarios de instancia.

El objeto devuelto por `super()` también tiene un método personalizado `__getattribute__()` para poder invocar descriptores. La búsqueda de atributo `super(B, obj).m` busca `obj.__class__.__mro__` para la clase base `A` inmediatamente después de `B` y luego devuelve `A.__dict__['m'].__get__(obj, B)`. Si no es un descriptor, se devuelve `m` sin cambios. Si no está en el diccionario, `m` revierte a una búsqueda usando `object.__getattribute__()`.

Los detalles de la implementación están en `super_getattro()` en Objects/typeobject.c y un equivalente puro de Python se puede encontrar en el Guido’s Tutorial.

Los detalles anteriores muestran que el mecanismo para los descriptores está incrustado en los métodos `__getattribute__()` para `object`, `type` y `super()`. Las clases heredan este mecanismo cuando derivan de `object` o mediante una metaclase que proporcione funcionalidades similares. Del mismo modo, las clases pueden desactivar la invocación del descriptor redefiniendo `__getattribute__()`.

## Ejemplo de descriptor

El siguiente código crea una clase cuyos objetos son descriptores de datos que imprimen un mensaje para cada lectura o escritura. Redefinir `__getattribute__()` es un enfoque alternativo que podría hacer esto para cada atributo. Sin embargo, este descriptor es útil para monitorizar solo algunos atributos elegidos:

    class RevealAccess(object):
        """A data descriptor that sets and returns values
           normally and prints a message logging their access.
        """

        def __init__(self, initval=None, name='var'):
            self.val = initval
            self.name = name

        def __get__(self, obj, objtype):
            print('Retrieving', self.name)
            return self.val

        def __set__(self, obj, val):
            print('Updating', self.name)
            self.val = val

    >>> class MyClass(object):
    ...     x = RevealAccess(10, 'var "x"')
    ...     y = 5
    ...
    >>> m = MyClass()
    >>> m.x
    Retrieving var "x"
    10
    >>> m.x = 20
    Updating var "x"
    >>> m.x
    Retrieving var "x"
    20
    >>> m.y
    5

El protocolo es simple y ofrece interesantes posibilidades. Varios casos de uso son tan comunes que se han empaquetado en llamadas a funciones individuales. Las propiedades, los métodos vinculados, los métodos estáticos y los métodos de clase se basan en el protocolo descriptor.

## Propiedades

Llamar a `property()` es una forma sucinta de construir un descriptor de datos que desencadena llamadas a funciones al acceder a un atributo. Su firma es:

    property(fget=None, fset=None, fdel=None, doc=None) -> property attribute

La documentación muestra un uso típico para definir un atributo administrado `x`:

    class C(object):
        def getx(self): return self.__x
        def setx(self, value): self.__x = value
        def delx(self): del self.__x
        x = property(getx, setx, delx, "I'm the 'x' property.")

Para ver cómo se implementa `property()` en términos del protocolo descriptor, aquí hay un equivalente puro de Python:

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

La función incorporada `property()` es de ayuda cuando una interfaz de usuario ha otorgado acceso a atributos y luego los cambios posteriores requieren la intervención de un método.

Por ejemplo, una clase de hoja de cálculo puede otorgar acceso al valor de una celda a través de `Cell('b10').value`. Las mejoras posteriores del programa requieren que la celda se vuelva a calcular en cada acceso; sin embargo, el programador no quiere afectar al código de cliente existente que accede al atributo directamente. La solución es envolver el acceso al valor del atributo en un descriptor de datos mediante una propiedad:

    class Cell(object):
        . . .
        def getvalue(self):
            "Recalculate the cell before returning value"
            self.recalc()
            return self._value
        value = property(getvalue)

## Funciones y métodos

Las características orientadas a objetos de Python se basan en un entorno basado en funciones. Usando descriptores de no-datos, ambas se combinan perfectamente.

Los diccionarios de clase almacenan los métodos como funciones. En una definición de clase, los métodos se escriben usando `def` o `lambda`, las herramientas habituales para crear funciones. Los métodos solo difieren de las funciones regulares en que el primer argumento está reservado para la instancia del objeto. Por convención en Python, la referencia de instancia se llama self pero puede llamarse this o cualquier otro nombre de variable.

Para admitir llamadas a métodos, las funciones incluyen el método `__get__()` para vincular métodos durante el acceso a atributos. Esto significa que todas las funciones son descriptores de no-datos que devuelven métodos enlazados cuando se invocan desde un objeto. En Python puro, funciona así:

    class Function(object):
        . . .
        def __get__(self, obj, objtype=None):
            "Simulate func_descr_get() in Objects/funcobject.c"
            if obj is None:
                return self
            return types.MethodType(self, obj)

Ejecutar el intérprete muestra como funciona el descriptor de función en la práctica:

    >>> class D(object):
    ...     def f(self, x):
    ...         return x
    ...
    >>> d = D()

    # Access through the class dictionary does not invoke __get__.
    # It just returns the underlying function object.
    >>> D.__dict__['f']
    <function D.f at 0x00C45070>

    # Dotted access from a class calls __get__() which just returns
    # the underlying function unchanged.
    >>> D.f
    <function D.f at 0x00C45070>

    # The function has a __qualname__ attribute to support introspection
    >>> D.f.__qualname__
    'D.f'

    # Dotted access from an instance calls __get__() which returns the
    # function wrapped in a bound method object
    >>> d.f
    <bound method D.f of <__main__.D object at 0x00B18C90>>

    # Internally, the bound method stores the underlying function and
    # the bound instance.
    >>> d.f.__func__
    <function D.f at 0x1012e5ae8>
    >>> d.f.__self__
    <__main__.D object at 0x1012e1f98>

## Métodos estáticos y métodos de clase

Los descriptores de no-datos proporcionan un mecanismo simple para variaciones de los patrones habituales para vincular funciones en métodos.

En resumen, las funciones tienen un método `__get__()` para que se puedan convertir en un método cuando se accede a ellas como atributos. El descriptor de no-datos transforma una llamada a `obj.f(*args)` en `f(obj, *args)`. Llamar a `klass.f(*args)` se convierte en `f(*args)`.

Este cuadro resume el enlace (binding) y sus dos variantes más útiles:

> Transformación | Llamado desde un objeto | Llamado desde una clase
> --- | --- | ---
> función | f(obj, *args) | f(*args)
> método estático | f(*args) | f(*args)
> método de clase | f(type(obj), *args) | f(klass, *args)

Los métodos estáticos devuelven la función subyacente sin cambios. Llamar a `c.f` o `C.f` es equivalente a una búsqueda directa en `object.__getattribute__(c, "f")` o en `object.__getattribute__(C, "f")`. Como resultado, la función se vuelve idénticamente accesible desde un objeto o una clase.

Buenos candidatos para ser métodos estáticos son los métodos que no hacen referencia a la variable `self`.

Por ejemplo, un paquete de estadística puede incluir una clase contenedora para datos experimentales. La clase proporciona métodos normales para calcular el promedio, la media, la mediana y otras estadísticas descriptivas que dependen de los datos. Sin embargo, puede haber funciones útiles que están relacionadas conceptualmente pero que no dependen de los datos. Por ejemplo, `erf(x)` es una práctica rutinaria de conversión que surge en el trabajo estadístico pero que no depende directamente de un conjunto de datos en particular. Se puede llamar desde un objeto o la clase: `s.erf(1.5) --> .9332` o `Sample.erf(1.5) --> .9332`.

Dado que los métodos estáticos devuelven la función subyacente sin cambios, las llamadas de ejemplo carecen de interés:

    >>> class E(object):
    ...     def f(x):
    ...         print(x)
    ...     f = staticmethod(f)
    ...
    >>> E.f(3)
    3
    >>> E().f(3)
    3

Usando el protocolo descriptor de no-datos, una versión pura de Python de `staticmethod()` se vería así:

    class StaticMethod(object):
        "Emulate PyStaticMethod_Type() in Objects/funcobject.c"

        def __init__(self, f):
            self.f = f

        def __get__(self, obj, objtype=None):
            return self.f

A diferencia de los métodos estáticos, los métodos de clase anteponen la referencia de clase a la lista de argumentos antes de llamar a la función. Este formato es el mismo si quien llama es un objeto o una clase:

    >>> class E(object):
    ...     def f(klass, x):
    ...         return klass.__name__, x
    ...     f = classmethod(f)
    ...
    >>> print(E.f(3))
    ('E', 3)
    >>> print(E().f(3))
    ('E', 3)

Este comportamiento es útil siempre que la función solo necesite tener una referencia de clase y no preocuparse por los datos subyacentes. Un uso de los métodos de clase es crear constructores de clase alternativos. En Python 2.3, el método de clase `dict.fromkeys()` crea un nuevo diccionario a partir de una lista de claves. El equivalente puro de Python es:

    class Dict(object):
        . . .
        def fromkeys(klass, iterable, value=None):
            "Emulate dict_fromkeys() in Objects/dictobject.c"
            d = klass()
            for key in iterable:
                d[key] = value
            return d
        fromkeys = classmethod(fromkeys)

Ahora se puede construir un nuevo diccionario de claves únicas así:

    >>> Dict.fromkeys('abracadabra')
    {'a': None, 'r': None, 'b': None, 'c': None, 'd': None}

Usando el protocolo descriptor de no-datos, una implementación pura en Python de `classmethod()` se vería así:

    class ClassMethod(object):
        "Emulate PyClassMethod_Type() in Objects/funcobject.c"

        def __init__(self, f):
            self.f = f

        def __get__(self, obj, klass=None):
            if klass is None:
                klass = type(obj)
            def newfunc(*args):
                return self.f(klass, *args)
            return newfunc