Posts | Tags | Categories | Archive

Descriptores - Parte 1

Cuando accedemos a los atributos de un objeto en python, a veces existen unos intermediarios casi imperceptibles llamados “descriptores” que son los responsables últimos del funcionamiento de la programación orientada a objetos. Están detrás de propiedades, métodos, métodos estáticos, métodos de clase y del mecanismo super() responsable de la herencia múltiple. Su labor es imprescindible y, sin embargo, son los grandes desconocidos del lenguaje.

Protocolo “descriptor”

Por protocolo “descriptor” se entiende la sustitución de un atributo por un objeto que intermedia en los accesos a ese atributo. Tal vez, las propiedades (property) puedan ser el ejemplo más visible de los descriptores, pero veremos que los descriptores están más presentes de lo podemos pensar.

Como descripción formal del protocolo descriptor, podemos decir que un descriptor es todo objeto que tenga definido al menos uno de estos tres métodos:

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

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

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

Respectivamente, serían los métodos para obtener, asignar y borrar un atributo del objeto obj.

Podemos distinguir dos tipos de descriptores:

  • Descriptor de datos (data descriptor): cuando tiene definidos los métodos __get__ y __set__. Es el que usaremos para acceder y cambiar el valor de un atributo.
  • Descriptor de no-datos (non-data descriptor): cuando sólo tiene definido el método __get__. Su uso será casi exclusivo para acceso a los métodos de un objeto.

Como veremos más adelante, distinguir entre estos dos tipos de descriptores es muy importante, ya que cada uno tiene distinto orden de preferencia cuando se buscan atributos en una jerarquía de clases.

Implementación de los “Descriptores de Datos”

Empecemos por un ejemplo:

class Desc(object):
    def __init__(self, mul):
        self.mul=mul
    def __get__(self, obj, cls=None):
        return obj.value*self.mul

class C(object):
    a12=Desc(12)
    a200=Desc(200)

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

c=C(2)
print c.value, c.a12, c.a200  #--> 2 24 400

Los atributos a12 y a200 están definidos por instancias del descriptor Desc(). Cuando accedemos a estos atributos, en lugar de devolvernos el descriptor, nos devuelve el valor resultante del método __get__ del descriptor.

De modo más explícito, sería:

c.a12 --> c.a12.__get__(c)

Al no estar definido el método __set__, se pueden reasignar estos atributos sin mayor problema, aunque dejarían así de estar controlado por el descriptor:

c.a12=12

Para completar el protocolo de descriptor de datos basta añadir un método __set__:

class Descrip(object):
    def __init__(self, mul):
        self.mul=mul
    def __get__(self, obj, cls=None):
        return obj.value*self.mul
    def __set__(self, obj, value):
        obj.value=value

La asignación anterior, se nos convertiría en:

c.a12=12 --> c.a12.__set__(c, 12)

Como se intuye, el descriptor tiene aquí total control sobre el valor final que se guardará como atributo. Como posible utilización, se pueden crear atributos de sólo lectura, para lo que bastaría con que el método __set__ genere un error AttributeError si se intenta modificar el atributo:

class Descrip(object):
    def __init__(self, mul):
        self.mul=mul
    def __get__(self, obj, cls=None):
        return obj.value*self.mul
    def __set__(self, obj, value):
        raise AttributeError

Tan sólo falta añadir el método __delete__ para completar el protocolo. No hay que olvidarse de este método si queremos que un atributo de sólo lectura aún pueda ser modificado mediante un borrado previo a su reasignación:

class Descrip(object):
    def __init__(self, mul):
        self.mul=mul
    def __get__(self, obj, cls=None):
        return obj.value*self.mul
    def __set__(self, obj, value):
        raise AttributeError
    def __delete__(self, obj):
        del self

c=C(2)

print c.a12 #--> 24

c.a12=100 #ERROR: AttributeError

del C.a12
c.a12=100

print c.a12  #--> 100 (no descriptor)

Saltarse al descriptor

Llegados aquí, se nos plantea una pregunta: ¿hay algún modo de acceder a los atributos sin pasar por su descriptor?

Y no es para nada una pregunta caprichosa. El descriptor necesita algún modo de acceder a los atributos que está gestionando sin tener que pasar por sí mismo. Tal vez, se podría hacer a través del diccionario del objeto, accesible como __dict__:

c.__dict__["a12"]=100  #equivalente a c.a12=100

Si lo pruebas, verás que no funciona. Cuando se busca un atributo, primero se busca entre los atributos de la clase antes de mirar en el diccionario de la instancia. Este orden de prioridades lo veremos en el próximo artículo cuando veamos el funcionamiento interno de un descriptor.

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