Crea un’app per la tabella di moltiplicazione Python con OOP

In questo articolo, creerai un’app per le tabelle di moltiplicazione, utilizzando la potenza della programmazione orientata agli oggetti (OOP) in Python.

Eserciterai i concetti principali di OOP e come utilizzarli in un’applicazione completamente funzionale.

Python è un linguaggio di programmazione multiparadigma, il che significa che noi sviluppatori possiamo scegliere l’opzione migliore per ogni situazione e problema. Quando si parla di programmazione orientata agli oggetti, ci si riferisce a uno dei paradigmi più utilizzati per costruire applicazioni scalabili, negli ultimi decenni.

Le basi dell’OOP

Daremo una rapida occhiata al concetto più importante di OOP in Python, le classi.

Una classe è un modello in cui definiamo la struttura e il comportamento degli oggetti. Quel template ci permette di creare delle Istanze, che non sono altro che singoli oggetti realizzati seguendo la composizione della classe.

Una semplice classe di libri, con gli attributi di titolo e colore, sarebbe definita come segue.

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

Se vogliamo creare istanze del libro di classe, dobbiamo chiamare la classe e passarle degli argomenti.

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

Una buona rappresentazione del nostro programma attuale sarebbe:

La cosa fantastica è che quando controlliamo il tipo delle istanze blue_book e green_book, otteniamo “Book”.

# Printing the type of the books

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

Dopo aver chiarito questi concetti, possiamo iniziare a costruire il progetto 😃.

Dichiarazione del progetto

Mentre si lavora come sviluppatori/programmatori, la maggior parte del tempo non viene speso a scrivere codice, secondo thenewstack passiamo solo un terzo del nostro tempo a scrivere o refactoring del codice.

Abbiamo speso gli altri due terzi leggendo il codice di altri e analizzando il problema su cui stiamo lavorando.

Quindi, per questo progetto, genererò una dichiarazione del problema e analizzeremo come creare la nostra app da essa. Di conseguenza, stiamo realizzando il processo completo, dal pensare alla soluzione per applicarla con il codice.

Un insegnante elementare vuole un gioco per testare le capacità di moltiplicazione degli studenti dagli 8 ai 10 anni.

Il gioco deve avere un sistema di vite e punti, in cui lo studente inizia con 3 vite e deve raggiungere un certo numero di punti per vincere. Il programma deve mostrare un messaggio di “perdita” se lo studente esaurisce tutte le sue vite.

Il gioco deve avere due modalità, moltiplicazioni casuali e moltiplicazioni da tavolo.

Il primo dovrebbe dare allo studente una moltiplicazione casuale da 1 a 10, e lui/lei deve rispondere correttamente per vincere un punto. Se ciò non accade, lo studente perde un live e il gioco continua. Lo studente vince solo quando raggiunge 5 punti.

La seconda modalità deve visualizzare una tavola pitagorica da 1 a 10, dove lo studente deve inserire il risultato della rispettiva moltiplicazione. Se lo studente fallisce 3 volte perde, ma se completa due tavoli, il gioco finisce.

So che i requisiti potrebbero essere un po’ più grandi, ma ti prometto che li risolveremo in questo articolo 😁.

Dividere e conquistare

L’abilità più importante nella programmazione è la risoluzione dei problemi. Questo perché devi avere un piano prima di iniziare a hackerare il codice.

Suggerisco sempre di prendere il problema più grande e dividerlo in problemi più piccoli che possono essere risolti sia in modo facile che efficiente.

Quindi, se devi creare un gioco, inizia suddividendolo nelle parti più importanti. Questi sottoproblemi saranno molto più facili da risolvere.

Proprio in quel momento puoi avere la chiarezza di come eseguire e integrare tutto con il codice.

Quindi facciamo un grafico di come sarebbe il gioco.

Questo grafico stabilisce le relazioni tra gli oggetti della nostra app. Come puoi vedere i due oggetti principali sono la moltiplicazione casuale e la moltiplicazione della tabella. E l’unica cosa che condividono sono gli attributi Punti e Vite.

Avendo tutte queste informazioni in mente, entriamo nel codice.

Creazione della classe di gioco Parent

Quando lavoriamo con la programmazione orientata agli oggetti, cerchiamo il modo più pulito per evitare la ripetizione del codice. Questo è chiamato ASCIUTTO (non ripeterti).

Nota: questo obiettivo non è legato alla scrittura del minor numero di righe di codice (la qualità del codice non deve essere misurata da questo aspetto) ma all’astrazione della logica più utilizzata.

Secondo l’idea precedente, la classe genitore della nostra applicazione deve stabilire la struttura e il comportamento desiderato delle altre due classi.

Vediamo come sarebbe fatto.

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, questa sembra una classe piuttosto vasta. Lascia che te lo spieghi in profondità.

Prima di tutto, capiamo gli attributi della classe e il costruttore.

Fondamentalmente, gli attributi di classe sono variabili create all’interno della classe, ma al di fuori del costruttore o di qualsiasi metodo.

Mentre gli attributi di istanza sono variabili create solo all’interno del costruttore.

La differenza principale tra questi due è l’ambito. cioè gli attributi di classe sono accessibili sia da un oggetto istanza che dalla classe. D’altra parte, gli attributi dell’istanza sono accessibili solo da un oggetto istanza.

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

Un altro articolo può approfondire questo argomento. Resta in contatto per leggerlo.

La funzione get_numeric_input viene utilizzata per impedire all’utente di fornire qualsiasi input che non sia numerico. Come puoi notare, questo metodo è progettato per chiedere all’utente fino a quando non riceve un input numerico. Lo useremo più tardi nelle classi del bambino.

I metodi di stampa ci consentono di salvare la ripetizione della stampa della stessa cosa ogni volta che si verifica un evento nel gioco.

Ultimo ma non meno importante, il metodo run è solo un wrapper che le classi Random multiplication e Table multiplication useranno per interagire con l’utente e rendere tutto funzionale.

Creare le classi del bambino

Una volta creata la classe genitore, che stabilisce la struttura e alcune delle funzionalità della nostra app, è il momento di creare le vere e proprie classi della modalità di gioco, utilizzando il potere dell’ereditarietà.

Classe di moltiplicazione casuale

Questa classe eseguirà la “prima modalità” del nostro gioco. Ovviamente utilizzerà il modulo casuale, che ci darà la possibilità di chiedere all’utente operazioni casuali da 1 a 10. Ecco un eccellente articolo sul casuale (e altri moduli importanti) 😉.

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()

Ecco un’altra classe enorme 😅. Ma come ho affermato prima, non è il numero di righe che ci vogliono, è quanto è leggibile ed efficiente. E la cosa migliore di Python è che consente agli sviluppatori di creare codice pulito e leggibile come se stessero parlando un inglese normale.

Questa lezione ha una cosa che potrebbe confonderti, ma te la spiegherò nel modo più semplice possibile.

    # 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)

Il costruttore della classe figlia sta chiamando la funzione super che, allo stesso tempo, fa riferimento alla classe genitore (BaseGame). Fondamentalmente sta dicendo a Python:

Compila l’attributo “points_to_win” della classe genitore con 5!

Non è necessario inserire self all’interno della parte super().__init__() solo perché stiamo chiamando super all’interno del costruttore e risulterebbe ridondante.

Stiamo anche usando la funzione super nel metodo run e vedremo cosa sta succedendo in quel pezzo di codice.

    # 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()
        
        .....

Come puoi notare il metodo run nella classe genitore, prin il messaggio di benvenuto e descrizione. Ma è una buona idea mantenere quella funzionalità e aggiungerne anche di extra nelle classi figlie. In base a ciò, usiamo super per eseguire tutto il codice del metodo genitore prima di eseguire il pezzo successivo.

L’altra parte della funzione run è piuttosto semplice. Chiede all’utente un numero con il messaggio dell’operazione a cui deve rispondere. Poi il risultato viene confrontato con la moltiplicazione reale e se sono uguali aggiunge un punto, se non tolgono 1 punto vita.

Vale la pena dire che stiamo usando i cicli while-else. Questo va oltre lo scopo di questo articolo ma ne pubblicherò uno a riguardo tra qualche giorno.

Infine, get_random_numbers, utilizza la funzione random.randint, che restituisce un numero intero casuale all’interno dell’intervallo specificato. Quindi restituisce una tupla di due numeri interi casuali.

Classe di moltiplicazione casuale

La “seconda modalità”, deve visualizzare il gioco in un formato di tabella di moltiplicazione e assicurarsi che l’utente risponda correttamente ad almeno 2 tabelle.

A tale scopo, utilizzeremo nuovamente il potere di super e modificheremo l’attributo della classe genitore 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()

Come puoi capire, stiamo solo modificando il metodo run di questa classe. Questa è la magia dell’ereditarietà, scriviamo una volta la logica che usiamo in più punti e ce ne dimentichiamo 😅.

Nel metodo run, stiamo usando un ciclo for per ottenere i numeri da 1 a 10 e costruire l’operazione che viene mostrata all’utente.

Ancora una volta se le vite sono esaurite o vengono raggiunti i punti necessari per vincere, il ciclo while si interromperà e verrà visualizzato il messaggio di vittoria o sconfitta.

Sì, abbiamo creato le due modalità di gioco, ma fino ad ora se eseguiamo il programma non succederà nulla.

Quindi finalizziamo il programma implementando la scelta della modalità e istanziando le classi in base a tale scelta.

Implementazione della scelta

L’utente sarà in grado di scegliere quale modalità vuole giocare. Vediamo quindi come 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()

Innanzitutto, chiediamo all’utente di scegliere tra le modalità 1 o 2. Se l’input non è valido, lo script interrompe l’esecuzione. Se l’utente seleziona la prima modalità, il programma eseguirà la modalità di gioco Random Multiplication, e se seleziona la seconda, verrà eseguita la modalità Table multiplication.

Ecco come sarebbe.

Conclusione

Congratulazioni, sei solo creare un’app Python con la programmazione orientata agli oggetti.

Tutto il codice è disponibile in Deposito Github.

In questo articolo hai imparato a:

  • Usa i costruttori di classi Python
  • Crea un’app funzionale con OOP
  • Usa la super funzione nelle classi Python
  • Applicare i concetti di base dell’ereditarietà
  • Implementare attributi di classe e istanza

Buona programmazione 👨‍💻

Successivamente, esplora alcuni dei migliori IDE Python per una migliore produttività.