Autor: Luis Fernando Apáez Álvarez
-Curso PyM-
Proyecto 1 (parte III)
Fecha: 01 de diciembre del 2022
Lo que haremos en esta parte del proyecto será modificar el código de la clase graficar() encapsulando algunos de sus atributos o métodos. No obstante, antes abordaremos un poco más a detalle sobre los decoradores en Python.
A las funciones o métodos con los cuales hemos estado trabajando a lo largo de todo el curso se les denomina invocables, esto es, podemos invocar una función o un método. Así, un decorador no es más que un invocable que devolverá otro invocable, es decir, un decorador tomará, por ejemplo, una función y le agregará algunas otras características para posteriormente devolver dicha función.
Para aclalar un poco este concepto veamos el siguiente ejemplo:
# Creamos una función para decorar, que recibe como parámetro otra función
def decorador(fun):
# Mensaje que agregaremos a la funcion a decorar
def mensaje_dec():
print(f'La función {fun} ha sido decorada')
# Invocamos la funcion del parámetro
fun()
# el decorador regresará lo hecho en la función mensaje_dec
return mensaje_dec
notemos que es necesario invocar la función del parámetro fun()
para obtener todas las características propias de dicha función, además, lo que esta haciendo este decorador es sólo agregar un mensaje adicional a las funcionalidades de la función que pasaremos en el parámetro. Veamos algunas pruebas:
# Creamos una función ordinaria
def fun_ordinaria():
print("Soy una función ordinaria")
fun_ordinaria()
Soy una función ordinaria
Ahora, decoraremos la función ``fun_ordinaria()`` como sigue
# Decoramos la función fun_ordinaria a la llamamos fun_decorada
fun_decorada = decorador(fun_ordinaria)
# Invocamos la función decorada
fun_decorada()
La función <function fun_ordinaria at 0x000001E6FC1F91F8> ha sido decorada Soy una función ordinaria
Es decir, la función fun_ordinaria
se ha efectuado de manera normal, pero al pasarla como parámetro de la función decorador()
, se ha agregado adicionalmente un mensaje a la ejecución, en este caso puede decirse que la función fun_ordinaria
fue decorada. Así, la función decorador()
es en realidad un decorador.
Existe otra alternativa para escribir lo anterior de una manera más simple:
@decorador
def fun_ordinaria():
print("Soy una función ordinaria")
donde en vez de realizar la sintaxis que empleamos anteriormente, sólo declaramos de manera normal la función fun_ordinaria
y al escribir arriba @decorador
estamos indicando que fun_ordinaria
tendrá las características adicionales del decorador (que en este caso no es más que un mensaje adicional). Comprobamos
fun_ordinaria()
La función <function fun_ordinaria at 0x000001E6FC1F9798> ha sido decorada Soy una función ordinaria
Así, en vez de escribir
def fun_ordinaria():
print("Soy una función ordinaria")
fun_decorada = decorador(fun_ordinaria)
fun_decorada()
bastará con que escribamos
@decorador
def fun_ordinaria():
print("Soy una función ordinaria")
El ejemplo anterior fue con funciones que no recibían parámetros, ahora veamos un ejemplo de un decorador sobre funciones que reciben parámetros. Para ello consideraremos una función para dividir dos números y el decorador se encargará de arrojar un mensaje en caso de que el denominador sea cero. Para ello
# Creamos el decorador
def decorador_div(fun):
# Característica que se agregará a las funciones decoradas
def inner(a,b):
print("Esta función ha sido decorada")
if b == 0:
print("No se puede dividir entre cero!")
return
# Regresamos la función con sus respectivos parámetros
return fun(a,b)
return inner
donde, si b==0
se imprimirá el mensaje marcado, además, al escribir return
después de dicho mensaje lo que estamos haciendo es que no se regrese el mensaje que Python nos arroja cuando dividimos entre cero, en su lugar no se arrojará nada. Ahora definimos la función para dividir y probamos
@decorador_div
def division(a,b):
print(a / b)
division(2,0)
Esta función ha sido decorada No se puede dividir entre cero!
Cabe resaltar que podemos generalizar los decoradores sobre funciones (o invocables) que reciben como parámetros listas, tuplas o diccionarios, además, podemos decorar más de una vez una función.
Existe una función incorporada en Python que devuelve un método de clase (classmethod()
) para una función dada, es decir, classmethod()
es un decorador.
Un método estático dentro de una clase es un método el cual no requiere de parámetros especificos y no puede acceder o modificar una clase, además, éstos son métodos de tipo herramienta que toman (o no) algunos parámetros y funcionan sobre ellos. En cambio, los métodos de clase tienen como primer parámetro cls
(es decir que los métodos de clase deben tener a la clase como primer parámetro, y esto lo representamos colocando cls
), pueden modificar o acceder al estado de una clase, además, dichos métodos pueden ser llamados tanto por la clase como por los objetos.
Veamos un ejemplo sencillo de métodos de clase y estáticos:
# Definimos una clase nueva
class persona:
def __init__(self, nombre, edad):
self.nombre = nombre
self.edad = edad
# Definimos el método de clase
@classmethod
def mensaje_de_clase(cls, nombre, edad):
# Asignamos nombre y edad a los atributos de la clase
# self.nombre y self.edad utilizando cls()
return cls(nombre, edad)
# En otras palabras está ocurriendo que
# self.nombre = nombre
# self.edad = edad
# Definimos un método estático
@staticmethod
def mensaje_estatico(edad):
return edad > 18
# Accedemos de forma directa al método mediante la propia clase
persona1 = persona.mensaje_de_clase("Luis", 21)
# Vemos que el método de clase ha modificado el atributo self.nombre
# de la clase persona
print(persona1.nombre)
Luis
Por otro lado, podemos invocar el método estático que definimos
print(persona.mensaje_estatico(21))
True
donde notamos que dicho método también puede ser invocado directamente desde la clase. Además, este método actúa sólo sobre el parámetro que ingresamos y no tiene alguna interacción con la clase misma.
En resumen, un método de clase:
Los temas que se han abordado en esta parte del proyecto sirven para entender de mejor manera el funcionamiento y concepto de los métodos get y set, pues éstos en realidad son métodos de clase dado que, el método get accede a los atributos de la clase y el método set modifica los atributos de la clase, teniendo éstos interacción directa con la clase; también, estos temas nos dan un mejor panorama sobre los decoradores, pues recordemos que antes ya los habíamos abordado en la clase 5 de esta sección cuando vimos el decorador @property
.
Nos concentraremos ahora en crear un método estático para la clase graficar()
, el cual servirá para hallar el límite de una función. Para este método nos basaremos integramente en el siguiente código que permite calcular límites. Este código es:
# Importamos los módulos necesarios
from IPython.display import display, Math, Markdown
import sympy as sym
from sympy.parsing.sympy_parser import parse_expr
# Definimos a x como un símbolo
x = sym.symbols("x")
# mostramos un menú para pedir al usuario la función y hacía
# que valor tiende x (controlamos posibles errores)
try:
print("Ingresa la función que depende de x: ")
fun = input(": ")
print("hacía qué valor tiende x: ")
lim = float(input(": "))
# interpretamos la función
y = parse_expr(fun)
# mostramos el resultado
print(f'El límite de la función {y} cuando x tiende a {lim} es:')
print(sym.limit(y,x,lim))
except:
print("Error al ingresar la información, fin del programa")
Ingresa la función que depende de x: : x**2 hacía qué valor tiende x: : 0 El límite de la función x**2 cuando x tiende a 0.0 es: 0
Así, adaptaremos el código anterior a un método estático para agregar a la clase graficar()
. Primero escribiremos el código anterior como una función:
# Función que recibirá dos parámetros (funcion y valor al cual tiende)
def calc_lim(func, lim):
# Definimos a x como un símbolo
x = sym.symbols("x")
# Controlamos posibles errores
try:
# interpretamos la función
y = parse_expr(func)
# mostramos el resultado
print(f'El límite de la función {y} cuando x tiende a {lim} es:')
print(sym.limit(y,x,lim))
except:
print("Error al ingresar la información, fin del programa")
# Probamos la función
calc_lim("x**2", 4)
El límite de la función x**2 cuando x tiende a 4 es: 16
Continuando, adaptamos lo anterior a un método estático en la clase graficar()
:
# Recordemos la clase menu
class menu:
def __init__(self):
self.fun = ""
self.rango = ""
# Atributo para control de flujo
self.control_f = False
def mensaje_inicial(self):
self.cadena1 = "De que tipo es tu función:"
self.cadena2 = "_" * 30
self.cadena3 = "1: Algebraicas"
self.cadena4 = "2: Trigonométricas"
self.cadena5 = "3: Exponencial"
self.cadena6 = "4: Logarítmica"
self.cadena7 = "_" * 30
# Cambiamos valor lógico de control_f
self.control_f = True
return self.cadena1 + "\n" + self.cadena2 + "\n" + self.cadena3 \
+ "\n" + self.cadena4 + "\n" + self.cadena5 + "\n" + self.cadena6 \
+ "\n" + self.cadena7
def info_usuario(self):
# Implementamos el if para el control del flujo
if self.control_f:
self.n_aux = input("Coloca un número del 1 al 4: ")
print("_" * 30)
if int(self.n_aux) not in (1,2,3,4):
print("Error, debes ingresar un número del 1 al 4")
print("Fin del proceso")
else:
self.fun = input("Ingresa tu función: ")
self.rango = input("Ingresa el rango de graficación: ")
self.rango_inf = self.rango[self.rango.find("(") + 1: self.rango.find(",")]
self.rango_sup = self.rango[self.rango.find(",") + 1: self.rango.find(")")]
if int(self.n_aux) != 1:
self.fun = "np." + self.fun
else:
self.fun = self.fun
else:
print("Debe invocarse primero el método mensaje inicial")
class graficar(menu):
def __init__(self):
super().__init__()
def __str__(self):
self.control_f = False
return f'Rango:({self.rango_inf},{self.rango_sup})\nf(x) = {self.fun}'
def Graficador(self):
if not self.control_f:
x = np.arange(eval(self.rango_inf), eval(self.rango_sup), 0.01)
y = self.fun
mplot.plot(x, eval(y))
mplot.grid()
mplot.show()
else:
print("Debe ejecutarse primero el método str!")
# Definimos calc_lim como un método estático
@staticmethod
def calc_lim(func, lim):
# Definimos a x como un símbolo
x = sym.symbols("x")
# Controlamos posibles errores
try:
# interpretamos la función
y = parse_expr(func)
# agregaremos una variable para almacenar el valor del límite
limite = sym.limit(y,x,lim)
# mostramos el resultado
print(f'El límite de la función {y} cuando x tiende a {lim} es:')
print(limite)
except:
print("Error al ingresar la información, fin del programa")
# Probamos el método estático
graficar.calc_lim("x**2", 5)
El límite de la función x**2 cuando x tiende a 5 es: 25
Para finalizar esta parte del proyecto haremos que la salida del método estático calc_lim()
sea más "estética". Para ello definineros primero la siguiente función, la cual permite que la salida de ejecución tenga formato Markdown
def printmd(cadena):
return display(Markdown(cadena))
# Probamos la función
printmd("**Texto en negritas**")
# Con f-string no podemos escribir código látex que comience con \
printmd(f'Ecuación matemática en látex:$$x^{2}=4$$')
# Con r-string podemos escribir comandos con \
printmd(r'Más de látex: $\frac{1}{2}$')
Texto en negritas
Ecuación matemática en látex:$$x^2=4$$
Más de látex: $\frac{1}{2}$
Con lo anterior podremos reescribir la función calc_lim()
como sigue
def calc_lim(func, lim):
# (1)
cad_lim = '\lim\limits_{x\\rightarrow 0}'
# (2)
cad_lim2 = cad_lim.replace('0', lim)
y = parse_expr(func)
limite = sym.limit(y,x,lim)
# (3)
return printmd(f'Cálculo: ${cad_lim2}{sym.latex(y)}={limite}$')
# Probamos la función
calc_lim("x**2", "5")
Cálculo: $\lim\limits_{x\rightarrow 5}x^{2}=25$
donde:
$(1)$ Esta cadena representa a la expresión $\lim\limits_{x\rightarrow 0}$
$(2)$ Reemplazaremos el valor de cero en $x\rightarrow 0$ por cualquier otro valor que sea de nuestro interés
$(3)$ Imprimimos la expresión $\lim\limits_{x\rightarrow 0}$ ({cad_lim2}
) junto con la función a la cual le estamos calculando el límite ({sym.latex(y)}
) y finalmente igualamos al valor obtenido del límite (={limite}
)
Los tres puntos en conjunto nos arrojarán algo como $\lim\limits_{x\rightarrow 5}x^{2}=25$.
Para finalizar agregaremos a la clase graficar
el método calc_lim()
como lo escribimos anteriormente y el método printmd()
el cual también es un método estático. De tal manera, el código final de la clase graficar()
queda como
class graficar(menu):
def __init__(self):
super().__init__()
def __str__(self):
self.control_f = False
return f'Rango:({self.rango_inf},{self.rango_sup})\nf(x) = {self.fun}'
def Graficador(self):
if not self.control_f:
x = np.arange(eval(self.rango_inf), eval(self.rango_sup), 0.01)
y = self.fun
mplot.plot(x, eval(y))
mplot.grid()
mplot.show()
else:
print("Debe ejecutarse primero el método str!")
# Método estático para imprimir en Markdown
@staticmethod
def printmd(cadena):
return display(Markdown(cadena))
# Definimos calc_lim como un método estático
@staticmethod
def calc_lim(func, lim):
try:
cad_lim = '\lim\limits_{x\\rightarrow 0}'
cad_lim2 = cad_lim.replace('0', lim)
y = parse_expr(func)
limite = sym.limit(y,x,lim)
return printmd(f'Cálculo: ${cad_lim2}{sym.latex(y)}={limite}$')
except:
return "Error al ingresar la información, fin del programa"
# Probamos los métodos estáticos calc_lim y printmd:
graficar.calc_lim("x**2", "2")
graficar.printmd("## Esto es un subtítulo")
graficar.printmd("### Me hago más pequeño")
graficar.printmd("#### Más todavía")
graficar.printmd("##### Y más")
graficar.printmd("###### Y más")
En la siguiente parte del proyecto abordaremos los temas de encapsulamiento y los métodos getter y setter, donde ocuparemos los conceptos de método de clase y decoradores visto en esta parte del proyecto.