Curso de introducción a la programación con Python¶

    Autor: Luis Fernando Apáez Álvarez
    -Curso PyM-
    Proyecto 1 (parte IV)
    Fecha: 01 de diciembre del 2022


Contenido¶

  • Más sobre métodos de clase
  • Ejercicios

Dentro de Python, para la POO podemos diferenciar tres tipos de métodos:

  1. Método de instancia: Lo utilizamos para acceder o modificar el estado de un objeto. Dichos métodos deben tener como primer parámetro la palabra self.

  2. Método de clase: Lo utilizamos para acceder o modificar el estado de una clase. Por ejemplo, si utilizamos un método dentro de una clase que sólo ocupa variables propias de la clase, entonces dicho método es un método de clase. El primer parámetro de los métodos de clase es, por convención, cls el cual se refiere a la clase.

  3. Método estático: Es un método de utilidad general que realiza una tarea de forma aislada. Dentro de este método, no usamos instancia o variable de clase porque este método estático no toma ningún parámetro como self y cls.

Más sobre métodos de clase ¶

Los métodos de clase solo están vinculados con la propia clase y no con los objetos, de modo que éste solo puede acceder a las variables de clase. Este tipo de métodos puede modificar un estado de la clase cambiando así el valor de una variable de clase, lo cual se vería reflejado en todos los objetos de la clase.

Veamos un ejemplo.

In [2]:
# Definimos la clase estudiante
class estudiante:
    # nombre de la escuela
    escuela = 'UNAM'
    # constructor
    def __init__(self, nombre, edad):
        # atributos
        self.nombre = nombre
        self.edad = edad
    # crearemos un metodo de clase para cambiar el 
    # nombre de la escuela
    @classmethod
    def cambio_escuela(cls, nueva_escuela):
        cls.escuela = nueva_escuela
        # o alternativamente
        # cls.escuela = nueva_escuela

Notemos la gran diferencia existente entre el método de clase en comparativa con los métodos de instancia. En los métodos de instancia estamos acostumbrados a colocar el self. con el cual nos referimos a los objetos en cuestión, luego, para el método de clase tendremos en su lugar cls. la cual hace referencia a la clase con la cual estemos trabajando, la cual es en este caso la clase estudiante. Además, tenemos que cls.escuela es totalmente equivalente a colocar de manera directa estudiante.escuela.

Pongamos en acción el método de clase cambio_escuela:

In [4]:
# instanciamos
estud1 = estudiante('Luis', 25)
# accedamos al nombre de la escuela
estud1.escuela
Out[4]:
'UNAM'
In [5]:
# creamos otro objeto
estud2 = estudiante('Luisa', 25)
# accedamos al nombre de la escuela
estud2.escuela
Out[5]:
'UNAM'
In [6]:
# accedemos al nombre de la escuela de nuestra clase
# estudiante
estudiante.escuela
Out[6]:
'UNAM'
In [7]:
# empleamos el metodo de clase
estudiante.cambio_escuela('IPN')
estudiante.escuela
Out[7]:
'IPN'

Vemos que los el cambio hecho previamente se ve reflejado en los dos objetos que creamos antes:

In [8]:
print(estud1.escuela)
print(estud2.escuela)
IPN
IPN

Veamos otro ejemplo de método de clase

In [9]:
# Definimos la clase estudiante
class estudiante:
    # constructor
    def __init__(self, nombre, promedio):
        # atributos
        self.nombre = nombre
        self.promedio = promedio
    # metodo str
    def __str__(self):
        return f'Nombre: {self.nombre}. Promedio: {self.promedio}'
    
    # crearemos un metodo de clase para ingresar el promedio
    # pero que este en formato de nota
    @classmethod
    def promedio_nota(cls, nombre, promedio_n):
        return cls(nombre, promedio_n)
        # o alternativamente
        # return estudiante(nombre, promedio_n)

donde lo que está ocurriendo en la última línea es que el método de clase servirá para crear un objeto. Notemos que el método de clase está utilizando a la clase misma para la creación de un objeto. Además, podemos ver que al escribir cls(nombre, promedio_n) tenemos que nombre será pasado al atributo self.nombre del objeto y análogamente promedio_n.

In [10]:
# instanciamos un objeto
estudiante1 = estudiante('Luis', 8)
print(estudiante1)
Nombre: Luis. Promedio: 8
In [11]:
# creamos el objeto auxiliandonos para ello
# del metodo de clase promedio_nota
estudiante2 = estudiante.promedio_nota('Luis', 'C')
print(estudiante2)
Nombre: Luis. Promedio: C

Así, en este caso estamos utilizando el método de clase de manera indirecta para modificar los atributos de la clase a la hora de crear un objeto.

Finalmente, si definimos una clase padre y dentro de ella tenemos métodos de clase, para todas las clases hijas éstas heredaran dichos métodos de clase.

Por otro lado, consideremos el código de la clase menu y efectuemos algunos cambios necesarios

In [42]:
# Recordemos la clase menu
class menu:
    # en realidad las cadenas cadena(1-6) pueden ser atributos de clase
    cadena1 = "De que tipo es tu función:"
    cadena2 = "_" * 30 + '\n'
    cadena3 = "1: Algebraicas"
    cadena4 = "2: Trigonométricas"
    cadena5 = "3: Exponencial"
    cadena6 = "4: Logarítmica"
    # constructor
    def __init__(self):
        self.fun = ""
        self.rango = ""
        # Atributo para control de flujo
        self.control_f = False

    def mensaje_inicial(self):
        # Cambiamos valor lógico de control_f
        self.control_f = True
        print(menu.cadena1)
        print(menu.cadena2)
        print(menu.cadena3)
        print(menu.cadena4)
        print(menu.cadena5)
        print(menu.cadena6)
        print(menu.cadena2)
    
    def info_usuario(self):
        # Implementamos el if para el control del flujo
        if self.control_f:
            n_aux = int(input("Coloca un número del 1 al 4: "))
            print("_" * 30)
            if 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 n_aux != 1:
                self.fun = "np." + self.fun
            else:
                self.fun = self.fun
        else:
            print("Debe invocarse primero el método mensaje_inicial")

Ejercicios ¶

Ejercicio 1¶

Lo que deberás hacer es convertir el método mensaje_inicial() a un método de clase realizando las modificaciones pertienentes. Nota que deberás hacer que el constructor reciba un parámetro el cual debe ser del tipo booleano, lo cual te permitirá realizar correctamente la definición del método de clase.

Ejercicio 2¶

De la clase menu, haz que el atributo self.control_f sea privado. De tal manera, lo que deberás hacer también será implementar los correspondientes métodos get y set. Nota: para la implementación de los get y set deberás utilizar decoradores.


Consideraremos ahora la clase graficar, pero la haremos independiente de la clase menu y realizaremos algunas modificaciones:

In [111]:
# Importamos los módulos necesarios
from IPython.display import display, Math, Markdown
import sympy as sym
from sympy.parsing.sympy_parser import parse_expr
import matplotlib.pyplot as plt
import numpy as np

class graficar:
    # constructor
    def __init__(self, booleano):
        self.__control_f = booleano
    # metodo str
    def __str__(self):
        # requeriremos que primero se ejecute el metodo info_usuario(),
        # lo cual controlamos con el siguiente try-except
        try:
            return f'Rango:({self.rango_inf},{self.rango_sup})\nf(x) = {self.fun}'
        except AttributeError:
            return 'Primero debe ejecutarse el método info_usuario()!!'
    # utilizaremos este metodo de la clase menu:    
    def info_usuario(self):
        # Implementamos el if para el control del flujo
        if self.__control_f:
            n_aux = int(input("Coloca un número del 1 al 4: "))
            print("_" * 30)
            if 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 n_aux != 1:
                self.fun = "np." + self.fun
            else:
                self.fun = self.fun
        else:
            print("Debe configurarse un valor inicial booleano de True")
            
    def Graficador(self):
        if self.__control_f:
            x = np.arange(eval(self.rango_inf), eval(self.rango_sup), 0.01)
            y = self.fun
            plt.plot(x, eval(y))
            plt.grid()
            plt.show()
        else:
            print("Debe configurarse un valor inicial booleano de True")
    # 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):
        x = sym.symbols('x')
        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 graficar.printmd(f'Cálculo: ${cad_lim2}{sym.latex(y)}={limite}$')

donde notamos que en el return del método calc_lim() para poder acceder al método estático printmd() fue necesario colocar primero el nombre de la clase. Cabe mencionar que en la parte pasada del proyecto no fue necesario colocar graficar.printmd pues en alguna celda de código anterio se había definido y ejecutado la función

def printmd(cadena):
    return display(Markdown(cadena))

Ejercicio 3¶

Implementa un método estático a la clase graficar para poder calcular derivadas. Dicho método debe ser similar al método calc_lim().