馃敼 Propiedades en Python

隆Hola! En este art铆culo veremos c贸mo puedes usar el decorador @property en Python y sus ventajas.

Aprender谩s:

  • Las ventajas de usar propiedades en Python.
  • Los fundamentos de los decoradores (qu茅 son y c贸mo est谩n relacionados con @property).
  • C贸mo puedes usar @property para definir getters, setters y deleters.

馃敼 Ventajas de las propiedades en Python

Comencemos con un poco de contexto. 驴Por qu茅 deber铆as usar propiedades en Python?

Las propiedades se pueden considerar como la forma "Pythonica" de trabajar con atributos y programaci贸n orientada a objetos en Python porque:

  • La sintaxis para definir propiedades es muy concisa y f谩cil de leer.
  • Puedes acceder al valor de los atributos de instancias como si fueran atributos p煤blicos y usar la magia de los intermediarios (getters y setters) para validar valores nuevos y para evitar acceder o modificar los valores directamente.
  • Puedes "reutilizar" el nombre de una propiedad para evitar crear nuevos nombres para el getter, setter y deleter.

Estas ventajas hacen que las propiedades sean herramientas geniales en Python para escribir c贸digo m谩s conciso y f谩cil de leer.  

馃敻 Introducci贸n a los decoradores

Una funci贸n decoradora ("decorator function" en ingl茅s) es una funci贸n que agrega funcionalidad a otra funci贸n que recibe como argumento.

Usar una funci贸n decoradora es como a帽adir chispas de chocolate a un helado. Nos permite agregar funcionalidad a una funci贸n existente sin modificarla.

En el siguiente ejemplo puedes ver la estructura general de una funci贸n decoradora en Python:

def decorador(f):
    def funcion_nueva():
        print("Funcionalidad extra")
        f()
    return funcion_nueva

@decorador
def funcion_inicial():
    print("Funcionalidad inicial")

funcion_inicial()

Analicemos estos elementos en m谩s detalle:

  • Primero encontramos la funci贸n decoradora def decorador(f) (las chispas de chocolate 鉁) que toma una funci贸n f como argumento.
def decorador(f):
    def funcion_nueva():
        print("Funcionalidad extra")
        f()
    return funcion_nueva
  • Esta funci贸n decoradora tiene una funci贸n anidada, funcion_nueva. Nota c贸mo f es llamada dentro de funcion_nueva para obtener la misma funcionalidad y agregar funcionalidad nueva antes de la llamada a la funci贸n (tambi茅n podemos a帽adir funcionalidad nueva luego de la llamada a la funci贸n).
  • La funci贸n decoradora retorna la funci贸n anidada funcion_nueva.
  • Luego (abajo), encontramos la funci贸n que ser谩 decorada (el helado) funcion_inicial. Nota la sintaxis muy peculiar (@decorador) antes de la primera l铆nea de la funci贸n.
@decorador
def funcion_inicial():
    print("Funcionalidad inicial")

funcion_inicial()

Si ejecutamos el c贸digo, podemos ver el siguiente resultado:

Funcionalidad extra
Funcionalidad inicial

Nota c贸mo la funci贸n decoradora se ejecuta incluso si solo llamamos a la funci贸n funcion_inicial. Esta es la magia de agregar @decorador en Python.

馃挕 Dato: en general, escribimos @<nombre_de_la_funci贸n_decoradora>, reemplazando el nombre de la funci贸n decoradora luego del s铆mbolo @.

S茅 que debes estar pregunt谩ndote: 驴c贸mo se relaciona este concepto con @property? El decorador @property es un decorador incorporado (built-in) para la funci贸n property() en Python.

Se usa para asignarle una funcionalidad "especial" a ciertos m茅todos para que act煤en como getters, setters o deleters al momento de definir y usar propiedades en una clase.

Ahora que ya conoces m谩s sobre los decoradores, veamos una situaci贸n real en la cual usar铆as @property.

馃敻 Escenario real: @property

Digamos que la siguiente clase es parte de tu programa. Est谩s modelando una casa con una clase Casa (en este momento, la clase solo tiene un atributo de instancia precio):

class Casa:

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

Este atributo de instancia es p煤blico porque su nombre no tiene un gui贸n bajo al inicio.

Como el atributo es p煤blico, es muy probable que t煤 y otros desarrolladores en tu equipo usen y modifiquen el atributo directamente en otras partes del programa usando notaci贸n de punto.

De esta forma:

# Acceder al valor
obj.precio

# Modificar el valor
obj.precio = 40000

馃挕 Dato: obj representa una variable que contiene una referencia a una instancia de Casa.

Hasta ahora todo est谩 funcionando correctamente, 驴cierto?

Pero digamos que te piden que hagas ciertos cambios en el c贸digo, espec铆ficamente debes hacer que este atributo no sea p煤blico y que se valide el valor nuevo antes de asignarlo.

Espec铆ficamente, debes verificar si el valor es un n煤mero decimal (float) positivo. 驴C贸mo lo har铆as? Veamos.

Cambiando tu c贸digo

En este momento, si decides agregar getters y setters, t煤 y tu equipo probablemente entrar铆an en p谩nico.

Esto ocurrir铆a porque cada l铆nea de c贸digo que usa o modifica el valor del atributo tendr谩 que ser modificada para llamar al getter y al setter. Si no hacemos estos cambios, el c贸digo ya no funcionar谩. 鈿狅笍

# Antes: obj.precio
obj.get_precio()

# Antes: obj.precio = 40000
obj.set_precio(40000)

Pero... 隆las propiedades vienen al rescate! Con @property, t煤 y tu equipo no necesitar谩n modificar ninguna de estas l铆neas porque podr谩s agregar getters y setters "detr谩s de escenas" sin afectar la sintaxis que usabas para usar y modificar los atributos cuando eran p煤blicos.

Genial, 驴no?

馃敼 @property: sintaxis y l贸gica

Si decides usar @property, tu clase se ver谩 como el siguiente ejemplo:

class Casa:

	def __init__(self, precio):
		self._precio = precio

	@property
	def precio(self):
		return self._precio
	
	@precio.setter
	def precio(self, precio_nuevo):
		if precio_nuevo > 0 and isinstance(precio_nuevo, float):
			self._precio = precio_nuevo
		else:
			print("Por favor ingrese un precio valido.")

	@precio.deleter
	def precio(self):
		del self._precio

Espec铆ficamente, puedes definir tres m茅todos para una propiedad:

  • Un getter - para acceder al valor del atributo.
  • Un setter - para actualizar el valor del atributo.
  • Un deleter - para eliminar el atributo de la instancia.

Precio ahora est谩 "protegido"
El atributo precio ahora se considera "protegido" o no p煤blico porque agregamos un gui贸n bajo al inicio de su nombre en self._precio:

self._precio = precio

En Python, por convenio, cuando agregas un gui贸n bajo a un nombre, le dices a otros desarrolladores que no debe ser usado o modificado directamente fuera de la clase. Solo debe ser usado a trav茅s de los intermediarios (getters y setters) si est谩n definidos.

馃敻 Getter

Aqu铆 tenemos el m茅todo getter:

@property
def precio(self):
	return self._precio

Veamos la sintaxis:

  • @property - Se usa para indicar que se va a definir una propiedad. Nota c贸mo inmediatamente esto logra que el c贸digo sea m谩s f谩cil de leer porque podemos saber el prop贸sito de este m茅todo.
  • def precio(self) - La primera l铆nea de la definici贸n. Nota c贸mo el getter tiene el mismo nombre de la propiedad que vamos a definir: precio. Este es el nombre que usaremos para acceder y modificar el atributo fuera de la clase. El m茅todo solo toma un par谩metro formal, self, una referencia a la instancia.
  • return self._precio - esta l铆nea es exactamente lo que esperar铆as en un getter. Se retorna el valor del atributo.

En este ejemplo podemos ver c贸mo se usa el getter detr谩s de escenas con notaci贸n de punto:

>>> casa = Casa(50000.0)   # Crear instancia
>>> casa.precio            # Acceder al valor
50000.0

Nota c贸mo accedemos al atributo precio como si fuera un atributo p煤blico. No cambiamos la sintaxis pero usamos el getter como intermediario para evitar acceder al valor directamente.

馃敼 Setter

Ahora tenemos el m茅todo setter:

@precio.setter
def precio(self, precio_nuevo):
	if precio_nuevo > 0 and isinstance(precio_nuevo, float):
		self._precio = precio_nuevo
	else:
		print("Por favor ingrese un valor valido")

Veamos la sintaxis:

  • @precio.setter - se usa para indicar que este es el m茅todo setter para la propiedad precio. Nota que no estamos usando @property.setter sino @precio.setter. El nombre de la propiedad se incluye antes de .setter.
  • def precio(self, precio_nuevo): - La primera l铆nea de la definici贸n del m茅todo y la lista de par谩metros. Nota c贸mo el nombre de la propiedad se usa como el nombre del setter. Tambi茅n tenemos un segundo par谩metro formal (precio_nuevo), el cual es el nuevo valor que ser谩 asignado al atributo precio (si es v谩lido).
  • Finalmente, tenemos el cuerpo del setter en el cual validamos el argumento para verificar si es un float positivo y luego, si el argumento es v谩lido, actualizamos el valor del atributo. Si el valor no es v谩lido, se muestra un mensaje. Puedes escoger c贸mo manejar valores inv谩lidos de acuerdo a las necesidad de tu programa.

Este es un ejemplo del uso del m茅todo setter con @property:

>>> casa = Casa(50000.0)    # Crear instancia
>>> casa.precio = 45000.0   # Actualizar valor
>>> casa.precio             # Acceder al valor
45000.0

Nota c贸mo no estamos cambiando la sintaxis pero ahora usamos un intermediario (el setter) para validar el argumento antes de asignarlo. El valor nuevo (45000.0) se pasa como argumento para el setter:

casa.precio = 45000.0

Si intentamos asignar un valor inv谩lido, podemos ver el mensaje. Tambi茅n podemos verificar que el valor del atributo no se actualiz贸.

>>> casa = Casa(50000.0)
>>> casa.precio = -50
Por favor ingrese un valor valido
>>> casa.precio
50000.0

馃挕 Dato: esto prueba que el m茅todo setter s铆 est谩 actuando como intermediario. Se llama "detr谩s de escenas" cuando intentamos actualizar el valor y el mensaje se muestra cuando el valor no es v谩lido.

馃敻 Deleter

Finalmente, tenemos el m茅todo deleter:

@precio.deleter
def precio(self):
	del self._precio

Veamos la sintaxis:

  • @precio.deleter - se usa para indicar que este es el m茅todo deleter para la propiedad precio. Nota que esta l铆nea es muy parecida a @precio.setter pero ahora estamos definiendo un m茅todo deleter, as铆 que escribimos @precio.deleter.
  • def precio(self): - La primera l铆nea de la definici贸n. Este m茅todo solo tiene un par谩metro formal, self.
  • del self._precio - El cuerpo, en el cual eliminamos el atributo de instancia.

馃挕 Dato: nota que el nombre de la propiedad se "reusa" para los tres m茅todos.

Este es un ejemplo del uso de un m茅todo deleter con @property:

# Crear instancia
>>> casa = Casa(50000.0)

# El atributo de instancia existe
>>> casa.precio
50000.0

# Eliminar el atributo de instancia
>>> del casa.precio

# El atributo de instancia ya no existe
>>> casa.precio
Traceback (most recent call last):
  File "<pyshell#35>", line 1, in <module>
    casa.precio
  File "<pyshell#20>", line 8, in precio
    return self._precio
AttributeError: 'Casa' object has no attribute '_precio'

El atributo de instancia fue eliminado exitosamente. Cuando intentamos usarlo nuevamente, se genera un error porque el atributo ya no existe.

馃敼 Tips 煤tiles

No necesariamente tienes que definir los tres m茅todos para cada propiedad. Puedes definir propiedades que solo pueden ser usadas pero no modificadas (read-only) si solo defines un m茅todo getter.

Tambi茅n puedes definir un getter y un setter sin un deleter.

Si crees que un atributo solo debe ser inicializado cuando la instancia es creada o solo debe modificarse internamente dentro de la clase, puedes omitir el setter.

Puedes escoger los m茅todos que deseas incluir en base al contexto en el cual est谩s trabajando.

馃敻 En resumen

  • Puedes definir propiedades con el decorador @property. Esta opci贸n es m谩s concisa y f谩cil de leer.
  • @property puede ser considerado como la forma "Pythonica" de definir getters, setters y deleters.
  • Al definir propiedades, puedes cambiar la implementaci贸n interna de una clase sin afectar el programa. As铆 que puedes agregar getters, setters y deleters que act煤an como intermediarios "detr谩s de escenas" para evitar acceder a los valores o modificarlos directamente.

Espero que te haya gustado mi tutorial y que te haya sido de utilidad. Te invito a seguirme en Twitter (@EstefaniaCassN). 猸