En este artículo, va a construir una aplicación de tablas de multiplicar, utilizando el poder de la Programación Orientada a Objetos (POO) en Python.

Practicará los principales conceptos de P.O.O ., y cómo utilizarlos en una aplicación totalmente funcional.

Python es un lenguaje de programación multiparadigma, lo que significa que nosotros como desarrolladores podemos elegir la mejor opción para cada situación y problema. Cuando hablamos de Programación Orientada a Objetos, nos referimos a uno de los paradigmas más utilizados para construir aplicaciones escalables, en las últimas décadas.

Los fundamentos de la POO

Vamos a echar un vistazo rápido al concepto más importante de la P.O.O. en Python, las clases.

Una clase es una plantilla en la que definimos la estructura y el comportamiento, de los objetos. Esa plantilla nos permite crear Instancias, que no son más que objetos individuales hechos siguiendo la composición de la clase.

Una simple clase libro, con los atributos de título y color, se definiría de la siguiente manera.

clase Libro:
    def __init__(self, title, color):
        self.title = título
        self.color = color

Si queremos crear instancias de la clase libro, debemos llamar a la clase y pasarle argumentos.

# Objetos instancia de la clase Libro
libro_azul = Libro("El niño azul", "Azul")
libro_verde = Libro("La historia de la rana", "Verde")

Una buena representación de nuestro programa actual sería

Class.png

Lo impresionante es que cuando comprobamos el tipo de las instancias blue_book y green_book , obtenemos «Libro».

# Imprimiendo el tipo de los libros

print(tipo(libro_azul))
# <clase '__main__.Libro'>
print(tipo(libro_verde))
# <clase '__main__.Libro'>

Después de tener claros estos conceptos, podemos empezar a construir el proyecto 😃.

Declaración del proyecto

Mientras trabajamos como desarrolladores/programadores, la mayor parte del tiempo no lo pasamos escribiendo código, según thenewstack sólo pasamos un tercio de nuestro tiempo escribiendo o refactorizando código.

Los otros dos tercios los pasamos leyendo el código de otros y analizando el problema en el que estamos trabajando.

Así que para este proyecto, generaré un enunciado del problema y analizaremos cómo crear nuestra aplicación a partir de él. Como resultado, estamos realizando el proceso completo, desde pensar en la solución hasta aplicarla con código.

Un profesor de primaria quiere un juego para poner a prueba las habilidades de multiplicación de los alumnos de 8 a 10 años.

El juego debe tener un sistema de vidas y otro de puntos, en el que el alumno comience con 3 vidas y deba alcanzar una determinada cantidad de puntos para ganar. El programa debe mostrar un mensaje de «perder» si el alumno agota todas sus vidas.

El juego debe tener dos modos, multiplicaciones aleatorias y multiplicaciones de tablas.

El primero debe dar al alumno una multiplicación aleatoria del 1 al 10, y éste debe responder correctamente para ganar un punto. Si no lo hace, el alumno pierde un vivo y el juego continúa. El alumno sólo gana cuando alcanza 5 puntos.

El segundo modo debe mostrar una tabla de multiplicar del 1 al 10, donde el alumno debe introducir el resultado de la multiplicación correspondiente. Si el alumno falla 3 veces pierde, pero si completa dos tablas, el juego termina.

Sé que los requisitos pueden ser un poco mayores, pero le prometo que los resolveremos en este artículo 😁.

Divide y vencerás

La habilidad más importante en programación es la resolución de problemas. Esto se debe a que es necesario tener un plan antes de empezar a hackear el código.

Siempre sugiero tomar el problema más grande y dividirlo en otros más pequeños que puedan ser a la vez, fáciles y eficientemente resueltos.

Así que si necesita crear un juego, empiece por dividirlo en las partes más importantes. Estos subproblemas serán mucho más fáciles de resolver.

Sólo entonces podrá tener claro cómo ejecutar e integrar todo con código.

Así que hagamos un gráfico de cómo sería el juego.

divide and conquer.png

Este gráfico establece las relaciones entre los objetos de nuestra aplicación. Como puede ver, los dos objetos principales son Multiplicación aleatoria y Multiplicación por tablas. Y lo único que comparten son los atributos Puntos y Vidas.

Teniendo en cuenta toda esta información, entremos en el código.

Creación de la clase de juego Padre

Cuando trabajamos con programación orientada a objetos, buscamos la forma más limpia de evitar la repetición de código. A esto se le llama DRY (don’t repeat yourself, no te repitas).

Nota: Este objetivo no está relacionado con escribir el menor número de líneas de código (la calidad del código no debe medirse por ese aspecto) sino con abstraer la lógica más utilizada.

De acuerdo con la idea anterior, la clase padre de nuestra aplicación debe establecer la estructura y el comportamiento deseado de las otras dos clases.

Veamos cómo se haría.

clase BaseGame:

    # Lenght que se centra el mensaje
    longitud_mensaje = 60
    
    descripción = ""  
        
    def __init__(self, puntos_a_ganar, n_vidas=3):
        """Clase base del juego

        Args
            points_to_win (int): los puntos que necesitará el juego para terminar 
            n_vidas (int): El número de vidas que tiene el alumno. Por defecto es 3.
        """
        self.puntos_para_ganar = puntos_para_ganar

        auto.puntos = 0
        
        auto.vidas = n_vidas

    def obtener_entrada_numérica(self, mensaje=""):

        while True:
            # Obtener la entrada del usuario
            user_input = input(mensaje) 
            
            # Si la entrada es numérica, devuélvala
            # Si no lo es, imprime un mensaje y repite
            if user_input.isnumeric():
                return int(user_input)
            si no
                print("La entrada debe ser un número")
                continuar     
             
    def imprimir_mensaje_de_bienvenida(self):
        print("JUEGO DE MULTIPLICACIÓN DE PITONES".center(self.longitud_mensaje))

    def print_lose_message(self):
        print("LAMENTO QUE HAYA PERDIDO TODAS SUS VIDAS".center(self.mensaje_largo))

    def print_win_message(self):
        print(f "ENHORABUENA HAS ALCANZADO {self.points}".center(self.message_lenght))
        
    def print_current_lives(self):
        print(f "Actualmente tiene {self.vidas}")

    def print_current_score(self):
        print(f "Su puntuación es {self.puntos}")

    def print_description(self):
        print("\n\n" self.description.center(self.message_lenght) "\n")

    # Método básico de ejecución
    def ejecutar(self):
        self.print_welcome_message()
        
        self.print_description()

Vaya, parece una clase enorme. Permítame explicarla en profundidad.

En primer lugar, entendamos los atributos de la clase y el constructor.

Base-game-constructor.png

Básicamente, los atributos de clase, son variables creadas dentro de la clase, pero fuera del constructor o de cualquier método.

Mientras que los atributos de instancia son variables creadas sólo dentro del constructor.

La principal diferencia entre, estos dos es el alcance. es decir, los atributos de clase son accesibles tanto, desde un objeto instancia como desde la clase. Por otro lado, los atributos de instancia sólo son accesibles desde un objeto instancia.

juego = BaseJuego(5)

# Acceso a la longitud del mensaje de la clase attr from class
print(game.longitud_mensaje) # 60

# Acceso a la longitud del mensaje de la clase attr from class
print(JuegoBase.longitud_mensaje) # 60

# Acceso a la instancia de puntos attr desde instancia
print(juego.puntos) # 0

# Accediendo al atributo de la instancia de puntos desde la clase
print(JuegoBase.puntos) # Error de atributo

Otro artículo puede profundizar en este tema. Manténgase en contacto para leerlo.

La funciónget_numeric_input se utiliza para evitar que el usuario proporcione cualquier entrada que no sea numérica. Como puede observar, este método está diseñado para preguntar al usuario hasta obtener una entrada numérica. Lo utilizaremos más adelante en las clases del niño.

Base-game-input.png

Los métodos de impresión nos permiten ahorrarnos la repetición de imprimir lo mismo cada vez que se produce un evento en el juego.

Por último, pero no menos importante, el método run es sólo una envoltura que las clases Random multiplication y Table multiplication utilizarán para interactuar con el usuario y hacer que todo sea funcional.

Base-game-run.png

Creación de las clases del niño

Una vez que hemos creado esa clase padre, que establece la estructura y parte de la funcionalidad de nuestra aplicación, es hora de construir las clases del modo de juego propiamente dicho, utilizando el poder de la herencia.

Clase de multiplicación aleatoria

Esta clase ejecutará el «primer modo» de nuestro juego. Va a utilizar el módulo aleatorio , por supuesto, que nos dará la capacidad de pedir al usuario operaciones aleatorias del 1 al 10. Aquí tiene un excelente artículo sobre el módulo random (y otros módulos importantes) 😉.

import random # Módulo para operaciones aleatorias
clase MultiplicaciónAleatoria(JuegoBase):

    description = "En este juego debe responder correctamente a la multiplicación aleatoria\NGana si alcanza 5 puntos, o pierde si pierde todas sus vidas"

    def __init__(self):
        # Los puntos necesarios para ganar son 5
        # Pase el argumento 5 "puntos_para_ganar
        super().__init__(5)

    def obtener_numeros_aleatorios(self):

        primer_número = random.randint(1, 10)
        segundo_número = random.randint(1, 10)

        return primer_número, segundo_número
        
    def ejecutar(self):
        
        # Llama a la clase superior para imprimir los mensajes de bienvenida
        super().run()
        

        while self.vidas > 0 and self.puntos_para_ganar > self.puntos:
            # Obtiene dos números aleatorios
            número1, número2 = self.get_random_numbers()

            operación = f"{número1} x {número2}: "

            # Pide al usuario que responda a esa operación 
            # Evitar errores de valor
            respuesta_usuario = self.get_numeric_input(mensaje=operación)

            if usuario_respuesta == número1 * número2:
                print("\nSu respuesta es correcta\n")
                
                # Añade un punto
                self.puntos = 1
            si no
                print("\nLo siento, su respuesta es incorrecta\n")

                # Sustrae una vida
                self.vidas -= 1
            
            self.print_current_score()
            self.print_current_lives()
            
        # Sólo se ejecuta cuando el juego ha terminado
        # y ninguna de las condiciones es cierta
        si no
            # Imprime el mensaje final
            
            if self.puntos >= self.puntos_para_ganar:
                self.print_win_message()
            si no
                self.print_lose_message()

He aquí otra clase masiva 😅. Pero como he dicho antes, no es el número de líneas que ocupa, es lo legible y eficiente que es. Y lo mejor de Python es que permite a los desarrolladores hacer código limpio y legible como si estuvieran hablando inglés normal.

Esta clase tiene una cosa que puede confundirle, pero se lo explicaré de la forma más sencilla posible.

   # Clase padre
    def __init__(self, puntos_a_ganar, n_vidas=3):
        "...
    # Clase hija
    def __init__(self):
        # El número de puntos necesarios para ganar es 5
        # Pase el argumento 5 "puntos_para_ganar
        super().__init__(5)

El constructor de la clase hijo está llamando a la función super que, al mismo tiempo hace referencia a la clase padre (BaseGame). Básicamente le está diciendo a Python

¡Rellene el atributo «puntos_para_ganar» de la clase padre con 5!

No es necesario poner self, dentro de la parte super().__init__() sólo porque estamos llamando a super dentro del constructor, y resultaría redundante.

También estamos usando la función super en el método run, y veremos lo que ocurre en ese trozo de código.

   # Método básico de ejecución
    # Método padre
    def ejecutar(self):
        self.print_welcome_message()
        
        self.print_description()
    def ejecutar(self):
        
        # Llama a la clase superior para imprimir los mensajes de bienvenida
        super().run()
        
        .....

Como puede observar el método run de la clase padre, imprime el mensaje de bienvenida y de descripción. Pero es una buena idea mantener esa funcionalidad y además añadir otras adicionales en las clases hijas. De acuerdo con eso, usamos super para ejecutar todo el código del método padre antes de ejecutar el siguiente trozo.

La otra parte de la función ejecutar es bastante sencilla. Pide al usuario un número con el mensaje de la operación que debe responder. Luego se compara el resultado con la multiplicación real y si son iguales, se añade un punto, si no lo son se quita 1 vida.

Vale la pena decir que estamos utilizando bucles while-else. Esto excede el alcance de este artículo pero publicaré uno sobre ello en unos días.

Por último, get_random_numbers, utiliza la función random.randint, que devuelve un número entero aleatorio dentro del rango especificado. Luego devuelve una tupla de dos enteros aleatorios.

Clase de multiplicación aleatoria

El «segundo modo», debe mostrar el juego en formato de tabla de multiplicar, y asegurarse de que el usuario responde correctamente al menos a 2 tablas.

Para ello, utilizaremos de nuevo el poder de super y modificaremos el atributo de la clase padre points_to_win a 2.

clase TablaMultiplicación(JuegoBase):

    description = "En este juego debe resolver correctamente la tabla de multiplicar completa\nGana si resuelve 2 tablas"
    
    def __init__(self):
        # Necesita completar 2 tablas para ganar
        super().__init__(2)

    def ejecutar(self):

        # Imprime mensajes de bienvenida
        super().run()

        while self.vidas > 0 and self.puntos_para_ganar > self.puntos:
            # Obtiene dos números aleatorios
            número = random.randint(1, 10)   

            for i in range(1, 11):
                
                if self.vidas <= 0:
                    # Asegúrese de que el juego no puede continuar 
                    # si el usuario agota las vidas

                    auto.puntos = 0
                    break 
                
                operación = f"{número} x {i}: "

                usuario_respuesta = self.get_numeric_input(mensaje=operación)

                if usuario_respuesta == número * i:
                    print("¡Genial! Su respuesta es correcta")
                si no
                    print("Lo sentimos, su respuesta no es correcta") 

                    self.vidas -= 1

            auto.puntos = 1
            
        # Sólo se ejecuta cuando el juego ha terminado
        # y ninguna de las condiciones es cierta
        else:
            # Imprime el mensaje final
            
            if self.puntos >= self.puntos_para_ganar:
                self.print_win_message()
            si no
                self.print_lose_message()

Como puede darse cuenta sólo estamos modificando el método de ejecución de esta clase. Esa es la magia de la herencia, escribimos una vez la lógica que usamos en varios sitios, y nos olvidamos de ella 😅.

En el método de ejecución, estamos utilizando un bucle for para obtener los números del 1 al 10 y construimos la operación que se muestra al usuario.

Una vez más, si se agotan las vidas o se alcanzan los puntos necesarios para ganar, el bucle while se romperá y se mostrará el mensaje de victoria o derrota.

SÍ, hemos creado los dos modos del juego, pero hasta ahora si ejecutamos el programa no ocurrirá nada.

Así que finalicemos el programa implementando la elección del modo, e instanciando las clases dependiendo de esa elección.

Implementación de la elección

El usuario podrá elegir qué modo quiere jugar. Así que veamos cómo implementarlo.

if __name__ == "__main__":

    print("Seleccionar modo de juego")

    elección = input("[1],[2]: ")

    si elección == "1
        juego = RandomMultiplication()
    elif elección == "2":
        juego = TablaMultiplicación()
    si no
        print("Por favor, seleccione un modo de juego válido")
        exit()

    game.run()

En primer lugar, pedimos al usuario que elija entre los modos 1 ó 2. Si la entrada no es válida, el script deja de ejecutarse. Si el usuario selecciona el primer modo, el programa ejecutará el modo de juego Multiplicación aleatoria , y si selecciona el segundo, se ejecutará el modo Multiplicación por tablas .

Este es el aspecto que tendría

game.png

Conclusión

Enhorabuena, acaba de construir una aplicación Python con Programación Orientada a Objetos.

Todo el código está disponible en el repositorio de Github.

En este artículo usted aprendió a :

  • Usar constructores de clases Python
  • Crear una aplicación funcional con POO
  • Utilizar la función super en las clases Python
  • Aplicar los conceptos básicos de herencia
  • Implemente atributos de clase e instancia

Feliz codificación 👨‍💻

A continuación, explore algunos de los mejores IDE de Python para mejorar la productividad.