Saltar al contenido

Manejo de atributos mediante propiedades.

En Python, cuando creamos una clase, definimos una serie de atributos (que indicarán su estado) y de métodos (que marcarán su comportamiento). El acceso a ambos se realiza de manera sencilla mediante el operador punto. Un ejemplo de ello (como nota decir que todo el código mostrado en esta entrada está escrito en Python 3 sobre el IDE PyScripter)  es el siguiente:

La salida sería:

Para ejemplos sencillos podría ser suficiente actuar de esta manera, pero en otros momentos necesitaremos más versatilidad. Imaginemos que queremos validar los atributos con una determinada condición. En nuestro caso podría ser que los valores de dato estuviesen entre 0 y 10. A los valores menores que 0 se les asignaría el 0 y a los mayores de 10, el 10. Todo ello se lograría fácilmente creando un método que manejase esa condición y ejecutándolo también en __init__():

Con salida:

En el método set_dato() hacemos la comprobación que el dato es entero o real. De lo contrario lanzamos una excepción. Con ello nos aseguraríamos que el dato cumple las condiciones que queremos.
De la misma forma que hemos creado un método para manejar la escritura del atributo dato podríamos hacerlo para su lectura. Son los denominados, de forma genérica, métodos getter y setter, que nos permiten, respectivamente, obtener o modificar los atributos. El concepto de encapsulación de datos incluye, además del uso de estos métodos, el que los atributos sean privados (no accesibles desde fuera de la propia clase), algo que en Python se logra añadiendo un doble guión bajo al nombre del atributo. Tendríamos lo siguiente:

La salida es:

En Python es habitual que los atributos que en un principio son simples datos estáticos con el tiempo se conviertan en expresiones a calcular dinámicamente o, como ya hemos visto, haya que validarlos de alguna manera. Si actuamos con métodos getter y setter desde el principio podremos hacer esa nueva implementación sin cambiar la forma en la que accedemos a los datos, pero si lo hemos hecho simplemente mediante el operador punto, estaríamos en una situación delicada. Es para superar este escollo, o conseguir una forma mas pythónica que los métodos getter y setter, para lo que se usan las propiedades.
¿Qué son exactamente las propiedades?
Las propiedades son atributos que manejamos mediante métodos getter, setter y deleter, por lo que podríamos llamarlos “atributos manejados”. Podemos considerarlos unos atributos “especiales”. En realidad en Python los datos, métodos y propiedades de una clase son todos atributos. Los métodos serían atributos “llamables” y las propiedades atributos “personalizables”. La única diferencia entre una propiedad y un atributo estándar es que las primeras pueden invocar código personalizado al ser obtenidas, modificadas o eliminadas. Las propiedades se asocian a los atributos de la clase y, como cualquiera de ellos, son heredados en subclases e instancias. Una propiedad maneja un solo y específico atributo, y siempre harán referencia a los métodos de la clase en la que estén definidas.
Por lo tanto, las propiedades parecen atributos estándar, pero al acceder a ellos se lanzan los métodos getter, setter o deleter correspondientes. Como éstos métodos reciben el argumento self, podremos acceder a todos los elementos de la clase o la instancia (sus atributos y métodos) y hacer uso de ellos.
¿Cuándo usar entonces las propiedades?
Respecto al uso desde el principio de simples métodos getter , setter y deleter, las propiedades nos aportan un código mas pythónico, siendo mas fácil la lectura y escritura de los atributos. La interfaz es mas homogénea ya que a todo se accede mediante el operador punto.
Respecto al uso de atributos estándar, las propiedades nos dan la posibilidad de incorporar código que se ejecute de forma dinámica cuando intentamos acceder a ellos (para obtenerlos, modificarlos o borrarlos). Imaginemos el caso comentado con anterioridad: tenemos hecho un programa donde accedemos a atributos estándar públicos. Posteriormente necesitamos que esos atributos estándar sean validados con unas determinadas condiciones. Mediante las propiedades lograremos nuestro objetivo sin modificar el formato del código ya escrito, algo que no lograríamos con los métodos.
¿Cómo crear las propiedades?
La función property(), integrada en el intérprete, nos permite canalizar la lectura o escritura de los atributos (además de interceptar el momento en el que son borrados o proporcionar documentación sobre ellos) mediante funciones o métodos. Su formato es el siguiente:

Ninguno de los parámetros es obligatorio. Si no los pasamos su valor por defecto es None. La función fget() se encargará de interceptar la lectura del atributo, la fset() de hacerlo cuando se escriba, la fdel() a la hora de borrarlo y el argumento doc recibirá una cadena para documentar el atributo (si no lo recibe, se copia el docstring de fget() que, por defecto, tiene valor None). Si alguna operación no está permitida (por ejemplo, si intentamos borrar un atributo, algo no demasiado habitual, y no tenemos indicada fdel()) se lanzará una excepción. La función fget() devolverá el valor procesado del atributo y tanto fset() como fdel() devolverán None. La función property() devuelve un objeto de tipo propiedad que se asigna al nombre del atributo. Un primer ejemplo del uso de las propiedades podría ser el siguiente:

Obtenemos la siguiente salida:

En él se ha creado una clase Persona con un atributo privado nombre y tres métodos de tipo getter, setter y deleter. Posteriormente se crea, mediante la función property(), la propiedad nombre vinculada a ellos, que reemplaza al atributo del mismo nombre. Se añade además una información de ayuda. Más adelante creamos una instancia de Persona a la que posteriormente cambiamos el atributo nombre, tras lo cual lo borramos. Al intentar  borrarlo de nuevo, generamos un error que manejamos. También se lanza un error manejado al intentar leer el atributo borrado. Es interesante ejecutar el código paso a paso para ver su funcionamiento exacto. En este ejemplo, por simplicidad, no hemos manejado posibles excepciones en el método setter, pero sería conveniente hacerlo en un código completo.
Si no quisiésemos contravenir uno de los principios del Zen de Python (“Debería haber una, y preferiblemente solo una, forma obvia de hacer las cosas”) los métodos get_nombre(), set_nombre() y del_nombre() deberían ser privados (algo que nuevamente logramos colocando un doble guión bajo antes de su nombre).
Pero tampoco es ésta la forma mas pythónica para tratar con las propiedades, algo que se consigue mediante el uso de decoradores. Recordemos que un decorador es básicamente una función que “envuelve” a otra dotándola (al añadir código) de alguna funcionalidad extra. El formato es el siguiente:

También podríamos ponerlo así:

Podemos ahora usar la función property() como decorador para que se ejecute cuando queramos acceder a uno de los atributos.

O también colocarlo de la siguiente manera:

De esta manera lograríamos pasar mi_atributo como el primer argumento de la función property(), que es el que se usa cuando intentamos leer. El objeto propiedad tiene métodos getter, setter y deleter que asignan los métodos de acceso de la propiedad y que devuelven una copia de la propia propiedad. Los podemos, a su vez, usar para decorar métodos con el mismo nombre mi_atributo que usaremos en el momento en que intentamos modificar o borrar el atributo. Es un poco lioso, por lo que el código nos puede aclarar un poco las cosas:

Salida:

En el código definimos en  __init__() el atributo nombre como público. Eso nos permitirá que se ejecute, al crear la instancia de Persona, el método setter asociado a su propiedad y por tanto añadir el código que creamos conveniente. Nuevamente, por simplicidad, no se ha añadido manejo de excepciones ni condiciones de validación en él. Mediante la función property() decoramos el método getter que tiene el mismo nombre que nuestro atributo, es decir, nombre. Una vez hecho ésto, podemos usar los métodos setter y deleter del objeto propiedad para decorar los métodos correspondientes, de nombre nombre. Esta si es la forma mas pythónica de tratar las propiedades.
Los métodos getter de las propiedades pueden ser muy útiles si necesitamos calcular atributos sobre la marcha teniendo como base otros atributos. O también si queremos almacenar el resultado de un cálculo complejo para que sucesivas peticiones de ese cálculo no lo realicen de nuevo, sino que recuperen el resultado previo. Un ejemplo heterodoxo del uso de todo ello  es el siguiente: imaginemos que queremos crear una clase que almacene uno o una serie de valores. Podemos tener  o un número (entero o real) o una lista de números (reales y/o enteros). Como condición de validación tendremos que el número o números esté/n entre 0 y 10 (inclusive). De no ser así asignaremos al número el valor 0. Hay en la clase una propiedad llamada que simula (mediante el uso del módulo time) una operación pesada computacionalmente aplicada a los datos. Simulamos que nos cuesta tres segundos calcularla y el resultado es 123.79382.  Lo almacenamos en transformada  por si hay una posterior petición de su cálculo. Otra propiedad llamada media calcula el promedio de los datos ya filtrados mediante la validación. Se comprueba que los datos de entrada son correctos y si no se lanza una excepción de tipo ValueError.
El código y su correspondiente salida serían algo así, aconsejando una ejecución paso a paso para observar su funcionamiento detalladamente:

12 comentarios en «Manejo de atributos mediante propiedades.»

  1. Un artículo bastante compresible. Una pequeñísima corrección: las propiedades, al igual que los setters/getters, tienen sentido si son la única forma de acceder a los valores de los atributos que protegen. Al calcular la media, debes emplear la propiedad self.valor y no el atributo self.__valor .

  2. Muchas gracias por esta entrada. Precisamente estaba estudiando este tema de la POO. Pero siempre soy reacio a usar los metodos privados (aunque no lo sean del todo en Python). A veces, creo, que hacen la clase que creamos con excesivo codigo. Y si no son extrictamente necesarios, mejor crear los atributos y metodos publicos. De todas formas me leere con mucho gusto toda la explicacion que has expuesto. Tampoco podemos olvidar el name mangling. Ya que por este metodo se accede con facilidad a los valores que hemos encapsulado. Supongo, que el que programa por mero hobby o entretenimiento, pocas veces usara la ocultacion de datos.
    Por cierto, muy buen blog sobre Python. Alguna entrada sobre estructuras de datos tampoco estaria mal. Sobre arboles y grafos. Saludos!

    1. Muchas gracias por tu comentario Juan. Hace que tenga la sensación de que la entrada es útil para alguien, y eso es muy motivante.
      El uso de atributos privados es mas para evitar que un uso erróneo (no intencionado) pueda sobrescribirlos que para protegerlos completamente de un acceso externo,ya que en Python no es posible crear atributos privados en sentido estricto, cosa que por ejemplo sí es posible en Java. Por ello, aunque tengamos un atributo privado podemos acceder a él desde fuera de la clase mediante “name mangling”, como bien comentas. Muchos programadores de Python no están cómodos con ello y simplemente “marcan” como convención con un solo guión bajo los atributos que desean que sean privados (ya que no tiene un significado especial para el intérprete cuando lo usamos en nombres de atributos), convención que debe ser posteriormente respetada, claro está.

    2. Sobre Pybonacci decir que yo solo soy un autor invitado. Como lector llevo disfrutando muchos años de él y me parece una referencia extraordinaria dento del mundo Python en castellano, al margen de la gran labor divulgativa que han hecho en todos estos años. Así que te animo a leer los artículos ya que son todos de una enorme calidad.
      ¡Saludos!

  3. Gracias amigo, con este artículo ya pude comprender el @property, ya que no le entendía. Básicamente en lugar de poner
    “nombre = propety(get, set, del, doc)”, simplemente colocabas @property y ya lo demás te lo hacía sin colocar eso.

  4. Hola, tremenda explicación. Muchas gracias. Tengo una consulta. Por qué no se crea el método media de forma “normal”??. Así podría ser usado como media() (alguien podría confundirlo con un atributo y colocar media = 3.4). Vengo de programar en C++ y quiero entender como programar pythónicamente.

    Muchas gracias por el aporte (y)

    1. Hola. Si no hay un “setter” no se podría hacer lo que dices y daría error.

      Hay muchas formas de hacer las cosas y la que dices, usando un método, puedo tener todo el sentido del mundo. En este caso, los datos los encapsulas en la instancia y el “getter” media solo es un atajo para calcular la media, para hacer una declaración de intenciones de que la media de la instancia es única (y lo usas como si fuera un atributo),…

      Pero vamos, yo no soy ingeniero informático y algunas cosas me salen como me salen dependiendo del momento y solo necesito que lo que haga sea correcto. Esto es un medio para un fin. Las cosas no están escritas en hierro y hay mucha libertad para hacer lo que necesites de la forma que mejor se ajuste a la necesidad.

      Si te dedicas a la ingeniería informática puedo entender que busques más formalidad 🙂

      Saludos.

Responder a Anónimo Cancelar la respuesta

Tu dirección de correo electrónico no será publicada.

5 + four =

Pybonacci