Descriptores – Parte 2

jun 21 2011 Published by under Python

¿Cómo funciona un descriptor?

Todos los objetos y todas las clases que derivan de object1 adquieren de él un método llamado __getattribute__. Siempre a través de este método se accede a los atributos, y es en este método donde se hace toda la magia de los descriptores, de modo que un acceso al atributo obj.x se transformará en una llamada a type(obj).__dict__['x'].__get__(obj, type(obj)) si el atributo se trate de un descriptor. Una expresión casi ininteligible que va a requerir alguna que otra explicación. Lo importante es saber que al sobrecargar el método __getattribute__ deberemos cuidarnos de invocar al método de la clase padre si queremos que los descriptores sigan funcionando con normalidad.

Atributos de un objeto

De todos los atributos que tiene un objeto python, algunos son “Atributos especiales” que aporta python para su funcionamiento interno como son __class__ o __bases__. Estos atributos son bastante antipáticos de manejar ya que, o bien no son reportados por la función dir(), o bien tienen restricciones para ser modificados.

Por otro lado, están los atributos definidos dinámicamente por el programa que forman lo que se conoce como “diccionario del objeto”. Estos atributos se guandan en el (también) atributo __dict__.

Los “atributos de tipo” son los atributos asociados a un objeto por pertenencia a una clase. Estos atributos pueden estar enmascarados por los atributos del diccionario del objeto, algo muy útil cuando se aplican “técnicas dinámicas” de parcheo.

Hay que tener en cuenta que algunos de los tipos estándar como list,tuple,dict,… no tienen atributo __dict__ con lo que no tienen diccionario donde añadir o suplantar atributos dinámicamente. La única opción pasa por derivar clases a partir de ellos para añadir allí los atributos deseados.

Búsqueda de atributos

Al buscar un atributo obj.attr, se sigue un orden determinado de prioridad según el tipo de atributo que se esté buscando:

  1. Atributos especiales: son los que tienen mayor prioridad.

  2. Descriptores de datos: se buscan en el diccionario de la clase (obj.__class__.__dict__) y en todos los diccionarios de las clases padre. Si se encuentra, se retorna el resultado del descriptor (la expresión tan chula que puse al principio del artículo). Si no es un descriptor de datos, entonces se ignora y se sigue buscando.

  3. Atributos del diccionario del objeto: se busca el atributo en el diccionario del objeto (obj.__dict__). Si obj fuera una clase (==isinstance(obj,type)), entonces también se buscaría en los diccionarios de las clases padre (obj.__bases__) y, de ser un descriptor de datos, se devolverá el resultado del descriptor en su lugar.

  4. Descriptores de no-datos: se repite el paso 2, pero esta vez se buscan descriptores de no-datos.

  5. Método __getattr__: por último, si no ha habido éxito en la búsqueda del atributo, se intenta invocar el método __getattr__, de existir, para delegar en él.

  6. Si todo ha fallado, se termina la búsqueda retornando un error AttributeError.

En resumidas cuentas, se priorizan los descriptores de datos a las variables de instancia, las variables de instancia a los descriptores de no-datos y, con la más baja prioridad, se invocaría el método __getattr__.

Remarcar la diferencia que hay entre un descriptor de datos y uno de no-datos en el orden de búsqueda. Por el simple hecho de añadir un método __get__, un descriptor se pondría por delante de los atributos del diccionario del objeto en el orden de búsqueda. También apuntar que sólo se buscan descriptores entre los atributos de clase, por lo que no tendrá sentido asignar descriptores en otro atributos.

En el caso de la asignación de atributos, se seguirían estos pasos:

  1. Se busca descriptores de datos en el diccionario de la clase (obj.__class__.__dict__) y todos los diccionarios de las clases padre. Si se encuentra un descriptor de datos, entonces se invoca el método __set__ del descriptor.

  2. Se invoca el método __setattr__, si existe, para delegar en él.

  3. Como última prioridad, se inserta el atributo en el diccionario del objeto.

En estos pasos no aparecen los descriptores de no-datos. Si realizamos una asignación sobre un descriptor de no-datos, acabaría siendo reemplazado como cualquier atributo normal.

¿Se puede saltar un descriptor de datos?

La prioridad de los descriptores de datos frente al resto de atributos hace prácticamente imposible saltárselos para acceder directamente a un atributo. Todo acceso al atributo pasa por sus manos, regla que se aplica también con el propio descriptor y que da origen a bastantes recursividades sin fin. Por ello es habitual que el descriptor mantenga un atributo auxiliar “privado”, ya que de otro modo no tendrá otra forma de acceso directo.

Algo que sí podemos hacer es cambiar las prioridades con la definición de un método __getattribute__ propio. Como ejemplo, se podría priorizar los atributos del diccionario frente a los descriptores de esta manera:

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

class C(object):
    a12=Descrip(12)
    a200=Descrip(200)
   
    def __init__(self,value):
        self.value=value
       
    def __getattribute__(self, attr):
        dic=super(C,self).__getattribute__("__dict__")
        if attr in dic:
            return dic[attr]
        else:
            return super(C,self).__getattribute__(attr)

c=C(2)

print c.a12  #--> 24  (valor del descriptor)
c.__dict__["a12"]=100
print c.a12  #--> 100 (valor del diccionario)

  1. En python 2.x, a las clases que derivan de object se las denomina “nuevas clases” por contraste con las clases que había hasta ese momento. En python 3.x, todas las clases derivarán por defecto de object

2 responses so far

Descriptores – Parte 1

jun 19 2011 Published by under Python

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.

One response so far