Cree una aplicación de tabla de multiplicación de Python con programación orientada a objetos

En este artículo, creará una aplicación de tablas de multiplicar utilizando el poder de la programación orientada a objetos (POO) en Python.

Practicará los conceptos principales de OOP y cómo usarlos en una aplicación completamente funcional.

Python es un lenguaje de programación multiparadigma, lo que significa que los 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 programación orientada a objetos

Vamos a echar un vistazo rápido al concepto más importante de OOP 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 creados siguiendo la composición de la clase.

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

class Book:
    def __init__(self, title, color):
        self.title = title
        self.color = color

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

# Instance objects of Book class
blue_book = Book("The blue kid", "Blue")
green_book = Book("The frog story", "Green")

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

Lo increíble es que cuando verificamos el tipo de las instancias blue_book y green_book, obtenemos «Book».

# Printing the type of the books

print(type(blue_book))
# <class '__main__.Book'>
print(type(green_book))
# <class '__main__.Book'>

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

Declaración de proyecto

Al trabajar como desarrolladores/programadores, la mayor parte del tiempo no se dedica a escribir código, según la pila de noticias solo dedicamos un tercio de nuestro tiempo a escribir o refactorizar código.

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

Entonces, para este proyecto, generaré una declaración del problema y analizaremos cómo crear nuestra aplicación a partir de ella. Como resultado, estamos haciendo el proceso completo, desde pensar en la solución hasta aplicarla con código.

Una maestra 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 puntos, donde el alumno comienza con 3 vidas y debe llegar a cierta cantidad de puntos para ganar. El programa debe mostrar un mensaje de “perder” si el estudiante agota toda su vida.

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

El primero debe darle al alumno una multiplicación aleatoria del 1 al 10, y debe responder correctamente para ganar un punto. Si eso no ocurre, el estudiante pierde una vida y el juego continúa. El alumno sólo gana cuando alcanza los 5 puntos.

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

Sé que los requisitos tal vez sean un poco más grandes, pero te prometo que los resolveremos en este artículo 😁.

Divide y conquistaras

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

Siempre sugiero tomar el problema más grande y dividirlo en otros más pequeños que puedan resolverse de manera fácil y eficiente.

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

En ese momento puede tener la claridad de cómo ejecutar e integrar todo con código.

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

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

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

Creando la clase de juego Parent

Cuando trabajamos con programación orientada a objetos, buscamos la forma más limpia de evitar la repetición de código. Se llama SECO (no te repitas).

Nota: Este objetivo no está relacionado con escribir la menor cantidad 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.

Vamos a ver cómo se haría.

class BaseGame:

    # Lenght which the message is centered
    message_lenght = 60
    
    description = ""    
        
    def __init__(self, points_to_win, n_lives=3):
        """Base game class

        Args:
            points_to_win (int): the points the game will need to be finished 
            n_lives (int): The number of lives the student have. Defaults to 3.
        """
        self.points_to_win = points_to_win

        self.points = 0
        
        self.lives = n_lives

    def get_numeric_input(self, message=""):

        while True:
            # Get the user input
            user_input = input(message) 
            
            # If the input is numeric, return it
            # If it isn't, print a message and repeat
            if user_input.isnumeric():
                return int(user_input)
            else:
                print("The input must be a number")
                continue     
             
    def print_welcome_message(self):
        print("PYTHON MULTIPLICATION GAME".center(self.message_lenght))

    def print_lose_message(self):
        print("SORRY YOU LOST ALL OF YOUR LIVES".center(self.message_lenght))

    def print_win_message(self):
        print(f"CONGRATULATION YOU REACHED {self.points}".center(self.message_lenght))
        
    def print_current_lives(self):
        print(f"Currently you have {self.lives} livesn")

    def print_current_score(self):
        print(f"nYour score is {self.points}")

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

    # Basic run method
    def run(self):
        self.print_welcome_message()
        
        self.print_description()

Wow, esto parece una clase bastante grande. Déjame explicarlo en profundidad.

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

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

Mientras que los atributos de instancia son variables creadas solo dentro del constructor.

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

game = BaseGame(5)

# Accessing game message lenght class attr from class
print(game.message_lenght) # 60

# Accessing the message_lenght class attr from class
print(BaseGame.message_lenght)  # 60

# Accessing the points instance attr from instance
print(game.points) # 0

# Accesing the points instance attribute from class
print(BaseGame.points) # Attribute error

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

La función get_numeric_input se usa para evitar que el usuario proporcione cualquier entrada que no sea numérica. Como puede notar, este método está diseñado para preguntarle al usuario hasta que obtenga una entrada numérica. Lo usaremos más adelante en las clases del niño.

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

Por último, pero no menos importante, el método de ejecución es solo un contenedor que las clases de multiplicación aleatoria y multiplicación de tablas usarán para interactuar con el usuario y hacer que todo funcione.

Creando las clases del niño

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

Clase de multiplicación aleatoria

Esta clase ejecutará el «primer modo» de nuestro juego. Por supuesto, utilizará el módulo aleatorio, que nos dará la posibilidad de pedirle al usuario operaciones aleatorias del 1 al 10. Aquí hay un excelente artículo sobre el azar (y otros módulos importantes) 😉.

import random # Module for random operations
class RandomMultiplication(BaseGame):

    description = "In this game you must answer the random multiplication correctlynYou win if you reach 5 points, or lose if you lose all your lives"

    def __init__(self):
        # The numbers of points needed to win are 5
        # Pass 5 "points_to_win" argument
        super().__init__(5)

    def get_random_numbers(self):

        first_number = random.randint(1, 10)
        second_number = random.randint(1, 10)

        return first_number, second_number
        
    def run(self):
        
        # Call the upper class to print the welcome messages
        super().run()
        

        while self.lives > 0 and self.points_to_win > self.points:
            # Gets two random numbers
            number1, number2 = self.get_random_numbers()

            operation = f"{number1} x {number2}: "

            # Asks the user to answer that operation 
            # Prevent value errors
            user_answer = self.get_numeric_input(message=operation)

            if user_answer == number1 * number2:
                print("nYour answer is correctn")
                
                # Adds a point
                self.points += 1
            else:
                print("nSorry, your answer is incorrectn")

                # Substracts a live
                self.lives -= 1
            
            self.print_current_score()
            self.print_current_lives()
            
        # Only get executed when the game is finished
        # And none of the conditions are true
        else:
            # Prints the final message
            
            if self.points >= self.points_to_win:
                self.print_win_message()
            else:
                self.print_lose_message()

Aquí hay otra clase masiva 😅. Pero como dije antes, no es la cantidad de líneas que toma, sino cuán legible y eficiente es. Y lo mejor de Python es que permite a los desarrolladores crear código limpio y legible como si estuvieran hablando inglés normal.

Esta clase tiene una cosa que puede confundirte, pero te la explicaré de la manera más simple posible.

    # Parent class
    def __init__(self, points_to_win, n_lives=3):
        "...
    # Child class
    def __init__(self):
        # The numbers of points needed to win are 5
        # Pass 5 "points_to_win" argument
        super().__init__(5)

El constructor de la clase secundaria llama a la superfunción que, al mismo tiempo, se refiere a la clase principal (BaseGame). Básicamente le está diciendo a Python:

¡Complete el atributo «points_to_win» de la clase principal con 5!

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

También estamos usando la super función en el método de ejecución, y veremos qué sucede en ese fragmento de código.

    # Basic run method
    # Parent method
    def run(self):
        self.print_welcome_message()
        
        self.print_description()
    def run(self):
        
        # Call the upper class to print the welcome messages
        super().run()
        
        .....

Como puede notar el método de ejecución en la clase principal, imprima el mensaje de bienvenida y descripción. Pero es una buena idea mantener esa funcionalidad y también agregar otras adicionales en las clases secundarias. De acuerdo con eso, usamos super para ejecutar todo el código del método padre antes de ejecutar la siguiente pieza.

La otra parte de la función de ejecución es bastante sencilla. Solicita 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 suma un punto, si no quitan 1 vida.

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

Finalmente, 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 tablas de multiplicar, y asegurarse de que el usuario responda correctamente al menos 2 tablas.

Para ese propósito, usaremos nuevamente el poder de super y modificaremos el atributo de la clase padre points_to_win a 2.

class TableMultiplication(BaseGame):

    description = "In this game you must resolve the complete multiplication table correctlynYou win if you solve 2 tables"
    
    def __init__(self):
        # Needs to complete 2 tables to win
        super().__init__(2)

    def run(self):

        # Print welcome messages
        super().run()

        while self.lives > 0 and self.points_to_win > self.points:
            # Gets two random numbers
            number = random.randint(1, 10)            

            for i in range(1, 11):
                
                if self.lives <= 0:
                    # Ensure that the game can't continue 
                    # if the user depletes the lives

                    self.points = 0
                    break 
                
                operation = f"{number} x {i}: "

                user_answer = self.get_numeric_input(message=operation)

                if user_answer == number * i:
                    print("Great! Your answer is correct")
                else:
                    print("Sorry your answer isn't correct") 

                    self.lives -= 1

            self.points += 1
            
        # Only get executed when the game is finished
        # And none of the conditions are true
        else:
            # Prints the final message
            
            if self.points >= self.points_to_win:
                self.print_win_message()
            else:
                self.print_lose_message()

Como puede darse cuenta, solo 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 múltiples lugares, y nos olvidamos 😅.

En el método de ejecución, usamos 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 ciclo while se romperá y se mostrará el mensaje de ganar o perder.

SÍ, creamos los dos modos del juego, pero hasta ahora si ejecutamos el programa no pasará nada.

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

Implementación de elección

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

if __name__ == "__main__":

    print("Select Game mode")

    choice = input("[1],[2]: ")

    if choice == "1":
        game = RandomMultiplication()
    elif choice == "2":
        game = TableMultiplication()
    else:
        print("Please, select a valid game mode")
        exit()

    game.run()

Primero, le pedimos al usuario que elija entre los modos 1 o 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 de Multiplicación Aleatoria, y si selecciona el segundo, se ejecutará el modo de multiplicación de Tablas.

Así es como se vería.

Conclusión

Felicitaciones, solo crear una aplicación de Python con Programación Orientada a Objetos.

Todo el código está disponible en el Repositorio Github.

En este artículo aprendiste a:

  • Usar constructores de clases de Python
  • Crear una aplicación funcional con programación orientada a objetos
  • Usa la superfunción en las clases de Python
  • Aplicar los conceptos básicos de la herencia.
  • Implementar atributos de clase e instancia

Codificación feliz 👨‍💻

A continuación, explore algunos de los mejores IDE de Python para una mejor productividad.