Posts | Tags | Categories | Archive

Descriptores - Parte 3

Hasta ahora hemos visto cómo funcionan los descriptores para acceder a atributos de datos que funcionan como “propiedades” (property). Pero al iniciar esta serie de artículos dije que los descriptores son también “los responsables del funcionamiento de métodos, métodos estáticos, métodos de clase y del mecanismo super() responsable de la herencia múltiple”. Es el momento de ver cómo lo hacen:

Métodos vistos como funciones

Es común pensar que los métodos y las funciones comparten muchas similitudes. Considerando que en python las funciones son objetos de primera clase lo primero que podemos probar es a asignar directamente funciones a atributos de una clase para crear métodos dinámicamente:

class C(object):
    pass

def func(obj):
    print "obj is %s " % obj

C.method = func

#prueba del nuevo método
c = C()

c.method()    # cualquiera de...
C.method(c)   # ...estas invocaciones...
func(c)       # ...dan el mismo resultado

Esta dualidad entre funciones y métodos va más allá si observamos que, en realidad, las funciones son “descriptores”, tal como podemos comprobar mirando su diccionario:

>>> hasattr(func, "__get__")
True
>>> hasattr(func,"__set__")
False

Concretamente, las funciones son “descriptores de no-datos” y como tales se aplicarán las reglas comentadas en artículos previos. En concreto, se buscarán antes los métodos en el diccionario del objeto que entre los atributos de su clase1, lo que nos va a permitir suplantar métodos en tiempo de ejecución.

Con añadir funciones a los atributos de clase será suficiente para la mayoría de casos que nos podamos enfrentar. El resto de este artículo va orientado para algunos casos de “técnicas dinámicas” que requieren diferenciar el comportamiento de un objeto respecto al resto de las instancias de la misma clase.

Invocación de descriptores y sus enlaces

Hasta ahora no nos habíamos preocupado por el segundo argumento que se pasa al método __get__ en el interface “descriptor”, al que se denomina “propietario” (“owner”) y que siempre coincide con la clase de la instancia. A través de la instancia o del propietario, __get__ devolverá el atributo enlazado con la instancia y/o clase según sea el comportamiento buscado.

Veamos cómo funciona en detalle: supongamos que tenemos una instancia obj de una clase Cls y accedemos a través del descriptor desc. Tendremos las siguientes formas de establecer el enlace:

  • Llamada directa: __get__(obj) invocación explícita a partir del descriptor. Es la más simple, aunque infrecuente. (pe: desc.__get__(obj))

  • Enlace con la Instancia: __get__(obj, Cls) Se usa en el acceso al atributo obj.desc, donde se efectúa la llamada implícita Cls.__dict__['desc'].__get__(obj, Cls)

  • Enlace con la clase: __get__(None, Cls) Se usa en el acceso al atributo Cls.desc, donde se efectúa la llamada implícita Cls.__dict__['desc'].__get__(None, Cls)

  • Enlace con super: se da con instancias de la clase super utilizadas en la herencia múltiple. El acceso al atributo super(Cls, obj).desc inicia una búsqueda en obj.__class__.__mro__ para encontrar la clase base inmediatamente precedente a la clase Cls (=SuperCls) e invoca el descriptor con la llamada SuperCls.__dict__['desc'].__get__(obj, obj.__class__) con lo que obtenemos el atributo enlazado con una de las clase padre según el algoritmo MRO.

Como se puede observar, el método __get__ del descriptor recibe diferentes argumentos según el enlace que se vaya a usar, lo que nos permitirá programar el descriptor según el uso que deseemos darle.

Técnicas dinámicas

Para realizar nuestros experimentos, supongamos que tenemos el siguiente descriptor:

def desc(*args, **kwargs):
    print args, kwargs

Es una simple función que imprime los argumentos que recibe con el fin de poder analizarlos. Con una clase y una instancia intentaremos ver cómo añadirles métodos dinámicos:

class Cls(object):
    pass

obj=Cls()

El caso trivial es añadir el descriptor como atributo de la clase:

>>> Cls.meth=desc
>>> obj.meth
<bound method Cls.desc of <__main__.Cls object at 0x8ffd3ac>>
>>> obj.meth()
(<__main__.Cls object at 0x8ffd3ac>,) {}

Encaja con el funcionamiento estándar de los descriptores, que pasa por establecer primero un enlace del descriptor con la instancia o con la clase para obtener después el método ejecutable.

Pero a veces necesitamos añadir métodos sobre la instancia y no sobre la clase. Ésto puede ser debido a:

  1. Sólo queremos modificar una instancia sin que afecte al resto
  2. Queremos “decorar” el método de clase a través de un método de la instancia

Técnicamente, son los llamados “métodos singleton” que lenguajes como ruby incluyen en su sintaxis, pero que en python se implementan hackeando los descriptores.

Si añadiésemos un descriptor a una instancia sin establer ningún enlace:

>>> obj.meth=desc
>>> obj.meth()
() {}
>>> obj.meth
<function desc at 0xb76776bc>

Vemos que el funcionamiento es similar a si hubiéramos ejecutado directamente la función. En realidad, actúa como “métodos estáticos”, descriptores que no están enlazados con nada.

Para conseguir que el descriptor funcione como un método normal, necesitamos enlazarlo con la instancia:

>>> obj.meth=desc.__get__(obj, Cls)
>>> obj.meth()
(<__main__.Cls object at 0xb767adec>,) {}
>>> obj.meth
<bound method Cls.desc of <__main__.Cls object at 0xb767adec>>

Aquí ya vemos que el método se identifica como un “método normal” más de la clase Cls.

De forma parecida, podríamos enlazar el descriptor con la clase, pero vista como instancia, no como clase, con lo que obtenemos un “método de clase”:

>>> obj.meth=desc.__get__(Cls, type(Cls))
>>> obj.meth()
(<class '__main__.Cls'>,) {}

Hemos visto las opciones posibles para realizar diversas técnicas dinámicas. No es habitual verlas en el código que usamos normalmente. Casi puedo asegurar que si necesitas alguna de estas técnicas, es que te has pasado por alto alguna otra forma más sencilla de hacer lo mismo.

Pequeño truco

Todo lo anteriormente dicho funciona siempre que estemos trabajando con “descriptores de no-datos”. Si deseamos que un método de la clase no sea suplantado por un método en la instancia basta con crearlo como “descriptor de datos”. Lo más sencillo es usar el decorador @property:

>>> class Cls(object):
...  @property
...  def meth(self):
...    print "Desde clase"
... 
>>> obj=Cls()
>>> obj.meth=desc.__get__(obj,Cls)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute

Resto de artículos de la serie

  1. Optimizaciones con los Métodos Especiales
  2. Método __getattribute__
  3. Descriptores – Parte 1
  4. Descriptores – Parte 2

Descriptor Howto

Como referencias en la documentación oficial:

  1. Descriptor HowTo Guide
  2. Implementing Descriptors

  1. Este orden no se respeta con los “métodos especiales” y cuando estamos trabajando con “descriptores de datos”. Revisar el resto de artículos sobre descriptores. 

© Chema Cortés. Built using Pelican. Theme is subtle by Carey Metcalfe. Based on svbhack by Giulio Fidente.