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       | Llamado desde una  |
   |                   | objeto                 | 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
