Atributos con tipado estático en Python (usando un descriptor)

Python

En esta receta voy a contar cómo utilizar los «descriptores» de Python para poder crear atributos (variables de instancia) que no puedan cambiar de tipo durante la vida del objeto. Por supuesto, también es una excusa para aprender algo sobre los descriptores en sí.

Requisitos

  • Python >= 2.2 (se necesitan clases del «nuevo estilo», las que heredan de object).
  • Conocimientos precisos sobre los mecanismos de introspección de Python.

Descriptores

Los descriptores de Python son clases que implementan un «protocolo» concreto, es decir, una serie de métodos. A saber:

__get__(self, obj, type=None)
__set__(self, obj, value)
__delete__(self, obj)

Aunque esto parezca bastante extraño, lo cierto es que los descriptores se usan en la mayoría de componentes del lenguaje: funciones, métodos estáticos y de clase, properties y muchas otras cosas. La Descriptor HowTo Guide define el descriptor como «un atributo de objeto con comportamiento asociado», y eso es porque es posible añadir código que se ejecutar cuando se pida (get), se asigne (set) o se destruya un atributo creado con un descriptor.

Se distingue entre «data descriptors» si definen __get__ y __set__ y «non-data descriptors» si solo definen el __get__. Esta diferencia afecta al comportamiento del descriptor en lo referente al método __get__ aunque ambos lo tienen. No voy a entrar más en detalle sobre esto porque aquí vamos a hacer un «data descriptor».

Tipado estático

Nuestro descriptor nos va a permitir definir el tipo de un atributo al declarar la clase. No será posible cambiar el tipo del atributo operando con las instancias de la clase, aunque sí el valor. El descriptor sería algo como:

class TypedAttr(object):
    def __init__(self, name, cls):
        self.name = name
        self.cls = cls
 
    def __get__(self, obj, objtype):
        if (obj is None):
            raise AttributeError
 
        try:
            return obj.__dict__[self.name]
        except KeyError:
            raise AttributeError
 
    def __set__(self, obj, val):
        if not isinstance(val, self.cls):
            raise TypeError("'%s' given but '%s' expected" % (val.__class__.__name__, self.cls.__name__))
 
        obj.__dict__[self.name] = val

Y se usa de este modo:

>>> class A(object):
…     a = TypedAttr('__a', str)
>>>
>>> a1 = A()
>>> a1.a = 'hi'
>>> 
>>> a1.a = 3
>>> TypeError: 'int' given but 'str' expected

Puedes encontrar este descriptor en la última versión del paquete python-pyarco

Referencias

Comentarios

Opciones de visualización de comentarios

Seleccione la forma que prefiera para mostrar los comentarios y haga clic en «Guardar las opciones» para activar los cambios.
Imagen de shakaran

Uso de format

Muy interesante y completo el artículo. Pero tengo algunas observaciones/dudas.

Como requisitos se exige python 2.2 mínimo, pero creo que es incorrecto, ya que se usan funciones como format() que solo aparecen a partir de 2.6. Luego en realidad sólo podría utilizarse a partir de 2.6.

No se tampoco la razón de usar format(), ya que su propósito es hacer sustituciones complejas, pero en esta porción de código:

     raise TypeError("'{0}' given but '{1}' expected".format(
                    val.class.name, self.cls.name))

No se hace una sustitución muy elaborada, que podría ser reemplazada con:

     raise TypeError("'%s' given but '%s' expected" %(
                    val.class.name, self.cls.name))

Y con esto posiblmente si sería compatible hasta Python 2.2

Today is a good day. Would you be tomorrow?

Imagen de david.villa

Ciertamente

Se supone que el operador % estará obsoleto en futuras versiones, aunque posiblemente podamos seguir usándolo algunos años. Personalmente no utilizo % cuando escribo código nuevo, aunque estoy de acuerdo que, para este ejemplo, la obligación de usar python2.6 solo por algo tan accesorio no merece la pena. Lo cambio.

No soy portavoz de ningún colectivo, grupo o facción. Mi opinión es personal e intransferible.

Imagen de magmax

Artículo y comentarios

Jawdropping!
Me he quedado flipado, tanto por el artículo como por la desaparición de % (aunque la verdad es que me parece bastante acertado).

Un saludo.

Miguel Ángel García
http://magmax.org

Imagen de shakaran

Anonadado

Oh vaya, desconocía que % a partir de 3.0 pasara a ser deprecated. Es bastante curioso que se elimine, siendo un operador tan útil y tan utilizado, pero supongo que tendrá sus motivos (quizás adoptar una forma más pythonica o eliminar alguna ambigüedad). Gracias por la info David, todos los días se aprende algo nuevo.

Today is a good day. Would you be tomorrow?