Descriptores en Python



@eduardo_gpg

Número de visitas 911

Tiempo de lectura 4 min

31 Mayo 2023

En entregas anteriores ya hemos hablado sobre la programación orientada a objetos utilizando Python. Te comparto el último post en relación a ello. Pues bien, continuando con el tema, el día de hoy me gustaría hablar acerca de descriptores en Python, unos de los features que considero fundamental si deseamos dar nuestros primeros pasos en la meta programación y llevar nuestros conocimientos de POO al siguiente nivel.

Será un post sumamente interesante, el cual catalogó como de nivel avanzado, así que sin duda te invito a que te quedes.

Bien, una vez dicho todo esto, comencemos con el post.

Descriptores. 🐍

Comencemos con la pregunta obligada ¿Exactamente qué son los descriptores? Bueno, en palabras simples los descriptores son las forma en la cual podemos tener un control total sobre cada uno de los atributos de nuestras clase.

Lograremos esto implementado el descriptor protocol, que no son más que 4 métodos (uno opcional) que definen el comportamiento a seguir para cada acción de nuestros atributos. Los métodos son los siguientes.

  • __get__(self, obj, type=None) -> object
  • __set__(self, obj, value) -> None
  • __delete__(self, obj) -> None
  • __set_name__(self, owner, name) (Opcional)

Estos métodos, respectivamente, permiten realizar tareas cuando: Se obtiene el valor de un atributo, se asigna uno nuevo valor, se elimina un atributo y se obtiene el nombre del atributo gestionado. 🥳

Se que todo esto puede sonar confuso, porque de hecho lo es, 🥴 así que veamos un par de ejemplos.

Imaginemos que tenemos nuestra clase User.

class User:
    def __init__(self, username, email):
        self.username = username
        self.email = email


    def __str__(self):
        return f'{self.username} - {self.email}'

pywombat = User('pywombat', 'contacto@pywombat.com')
pywombat.username = 'pywombat v1'

print(pywombat)

Si ejecutamos este pequeño script obtendremos en consola los valores de mi objeto, donde se ha actualizado el valor para username pasando de pywombat a pywombat v1. 🤠

Salida:

pywombat v1 - contacto@pywombat.com

Hasta aquí nada nuevo. Sin embargo, ¿Qué pasa si queremos llevar un control total sobre cada cambio que afecte al atributo username? Bueno, para ello debemos implementar un descriptor.

Lo primero que debemos hacer es crear una nueva clase que implemente los métodos anteriormente mencionados.

Nuestra clase puede quedar de la siguiente manera (Recomiendo hacer uso del sufijo Descriptor).

class UsernameDescriptor:

    def __get__(self, instance, owner):
        print(">>> Accediendo al atributo")
        return instance._value


    def __set__(self, instance, value):
        print(">>> Asignando el atributo")
        instance._value = value


    def __delete__(self, instance):
        print(">>> Eliminando el atributo")
        del instance._value

Para esta clase el método get se encarga de retornar el valor del atributo, el método set se encarga de establecer un nuevo valor al atributo y el método delete se encarga de eliminar el atributo.

Lograremos todo esto a partir del parámetro instance con su atributo _value que no sería más que nuestro atributo a monitorear. 🐢

Una vez definida la clase, he implementado el descriptor protocol, debemos instanciar el descriptor para nuestro atributo a monitorear, esto lo haremos en nuestra clase principal, en mi caso en la clase User.

class User:
    username = UsernameDescriptor()


    def __init__(self, username, email):
        self.username = username
        self.email = email

Listo, una vez el descriptor ha sido definido y configurado, cualquier cambio que se realice sobre el atributo username deberá pasar primero por los métodos __get__ __set__ o __delete__.

Por ejemplo, si volvemos a ejecutar mi script, obtendremos 3 nuevos mensajes en consola.

pywombat = User('pywombat', 'contacto@pywombat.com')
pywombat.username = 'pywombat v1'

print(pywombat)

Salida:

>>> Asignando el atributo
>>> Asignando el atributo
>>> Accediendo al atributo
pywombat v1 - contacto@pywombat.com

Con esto confirmamos que el descriptor para username está funcionando correctamente. Tenemos 2 llamadas de asignación y una llamada de consulta. 🐧

Notemos cómo las llamadas a los métodos pueden ocurrir en cualquier parte de nuestro código, ya sea dentro de la clase (en un método) o fuera de ella.

¿Bástate cool no lo crees? 😎

Validaciones

Ahora quizás te estás preguntado¿ En qué escenarios de la vida real puedo implementar descriptores? 🤔 Bueno, desde mi experiencia creo que el escenario más común es el de validar que un atributo no pueda ser eliminado o simplemente no se le permita asignar ciertos valores.

Veamos un ejemplo.

class UsernameError(Exception):
    def __init__(self, message):
        self.message = message


class UsernameDescriptor:
    def __get__(self, instance, owner):
        return instance._value


    def __set__(self, instance, value):
        if value == 'admin':
            raise UsernameError(f'No es posible asignar el valor para el atributo' {self.name})

        instance._value = value


    def __delete__(self, instance):
        raise UsernameError(f'No es posible eliminar el atributo' {self.name})

En este caso he creado una nueva clase Error para mi Atributo username; hago uso de este error al momento de intentar eliminar o asignar el valor 'admin' para el atributo username.

Si intentamos asignar 'admin' o intentamos eliminarlo el atributo obtendremos como resultado un error. 😦

>>> pywombat.username = 'admin'
UsernameError: No es posible asignar el valor para el atributo.

o

>>> del pywombat.username
UsernameError: No es posible eliminar el atributo.

Perfecto, ahora ya tenemos mayor seguridad para nuestras clases y por supuesto para nuestro atributo.

Descriptores vs Properties 🍃

Si eres un Dev Python seguramente al leer todo esto te ha venido a la mente el uso de properties, que si no las conoces te dejo un link a un post donde hablamos acerca de ellos.

Y tienes razón, mucho de lo que se puede hacer con descriptores se puede lograr con properties, sin embargo hay que mencionar que las properties están ligadas 100% a una clase, por lo tanto no es posible reutilizar properties para diferentes clases, algo que sí ocurre si usamos descriptores, ya que al trabajar con una clase esta puede ser instancias la N cantidad de veces que deseemos sobre los N atributos que necesitemos. Eso hace que las clases descriptoras sean mucho más flexibles que los propiedades, esto sin añadir que estas clases pueden heredar de otras y/ o tener sus propios métodos y atributos. 🐼

Para cosas sencillas, llamémoslo así, recomiendo ampliamente el uso de propiedades, sin embargo, para temas complejos, donde la integridad de nuestro datos sea crucial y tengamos un proyecto grande recomiendo hacer uso de descriptores. 🥂


¿El contenido te resulto de ayuda?

Para poder dejar tu opinión es necesario ser un usuario autenticado. Login

Más Tips y Ejercicios 🐍

Adquiere una subscripción PyWombat por tan solo $3 USD. al mes.

Conoce los beneficios de ser usuario premium:
Niveles desbloqueados: Ten accesos a todos los niveles de ejercicios. 🔓
Nuevo límite: Incrementa tu límite de ejercicios por semana. 🚀
Contenido único: Recibe semanalmente recursos exclusivos de Python (Videos, Artículos y Capitulos del libro PyWombat, comienza como desarrollador Python. 🐍