Creați o aplicație Python pentru tabel de înmulțire cu POO

În acest material, vom dezvolta împreună o aplicație pentru exersarea tabelelor de înmulțire, folosind principiile programării orientate pe obiecte (POO) în Python.

Vom aprofunda conceptele fundamentale ale POO și vom învăța cum să le aplicăm practic într-o aplicație complet funcțională.

Python este un limbaj de programare flexibil, care ne permite, ca dezvoltatori, să alegem cea mai potrivită abordare pentru fiecare problemă specifică. Programarea Orientată pe Obiect este una dintre cele mai utilizate metode pentru crearea de aplicații scalabile în ultimele decenii.

Fundamentele Programării Orientate pe Obiect

Vom analiza rapid elementul central al POO în Python: clasele.

O clasă este un model care definește structura și comportamentul obiectelor. Acest model ne permite să creăm instanțe, care sunt, în esență, obiecte individuale derivate din structura clasei.

O clasă simplă pentru o carte, cu proprietăți precum titlul și culoarea, ar fi definită astfel:

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

Pentru a crea instanțe ale clasei Book, trebuie să apelăm clasa și să îi furnizăm argumente:

# Crearea instanțelor clasei Book
blue_book = Book("Copilul albastru", "Albastru")
green_book = Book("Povestea broaștei", "Verde")

O reprezentare bună a programului nostru în acest punct ar fi:

Un aspect interesant este că, atunci când verificăm tipul instanțelor blue_book și green_book, obținem „Book”.

# Afișarea tipului cărților

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

Acum, că am înțeles aceste concepte, suntem pregătiți să începem proiectul 😃.

Descrierea Proiectului

Conform thenewsstack, dezvoltatorii petrec doar o mică parte a timpului scriind cod, restul fiind dedicat citirii codului altora și înțelegerii problemelor.

Prin urmare, vom formula o descriere detaliată a problemei și vom analiza cum să construim aplicația noastră pornind de la ea. Astfel, vom parcurge întregul proces, de la conceperea soluției până la implementarea ei în cod.

Un profesor de școală primară dorește un joc care să evalueze cunoștințele de înmulțire ale elevilor cu vârste între 8 și 10 ani.

Jocul ar trebui să aibă un sistem de vieți și un sistem de puncte. Elevul începe cu 3 vieți și trebuie să acumuleze un anumit număr de puncte pentru a câștiga. În caz contrar, se va afișa un mesaj de pierdere.

Jocul va avea două moduri: înmulțiri aleatorii și tabele de înmulțire.

Primul mod va oferi elevului o înmulțire aleatorie cu numere de la 1 la 10. Dacă răspunde corect, primește un punct. Dacă răspunsul este greșit, pierde o viață. Elevul câștigă jocul dacă ajunge la 5 puncte.

Al doilea mod va prezenta tabelul de înmulțire cu numere de la 1 la 10. Elevul va introduce rezultatul fiecărei înmulțiri. Dacă greșește de 3 ori, pierde. Dacă rezolvă două tabele complete, câștigă jocul.

Sunt conștient că cerințele sunt destul de elaborate, dar vă asigur că le vom aborda eficient în acest articol 😁.

Abordarea „Divide et Impera”

Cea mai importantă abilitate în programare este rezolvarea problemelor. Este esențial să avem un plan înainte de a începe să scriem cod.

Sugerez să împărțim problemele mari în subprobleme mai mici, mai ușor de gestionat și rezolvat.

Astfel, pentru a crea un joc, vom începe prin a-l segmenta în părțile sale principale. Aceste subprobleme vor fi mult mai ușor de abordat individual.

Vom obține astfel claritate și vom înțelege cum să executăm și să integrăm toate elementele cu ajutorul codului.

Haideți să creăm o schemă care să ilustreze structura jocului.

Această schemă arată relațiile dintre componentele aplicației. Cele două elemente principale sunt înmulțirea aleatorie și tabelele de înmulțire. Ambele împărtășesc atributele Puncte și Vieți.

Având aceste informații, putem trece la cod.

Crearea Clasei de Joc Părinte

Când folosim programarea orientată pe obiecte, căutăm să evităm pe cât posibil repetarea codului. Acest principiu este cunoscut sub numele de DRY (Nu te repeta).

Rețineți: Nu urmărim să scriem cât mai puține linii de cod (calitatea codului nu trebuie evaluată în funcție de acest criteriu), ci să abstractizăm logica care este utilizată frecvent.

Conform acestui principiu, clasa părinte a aplicației noastre trebuie să definească structura și comportamentul dorit al celorlalte două clase.

Să vedem cum facem asta:

class BaseGame:

    # Lungimea mesajului pentru centrare
    message_lenght = 60
    
    description = ""    
        
    def __init__(self, points_to_win, n_lives=3):
        """Clasa de bază pentru joc

        Args:
            points_to_win (int): numărul de puncte necesare pentru a termina jocul
            n_lives (int): numărul de vieți ale studentului. Implicit 3.
        """
        self.points_to_win = points_to_win

        self.points = 0
        
        self.lives = n_lives

    def get_numeric_input(self, message=""):

        while True:
            # Obține datele introduse de utilizator
            user_input = input(message) 
            
            # Dacă inputul este numeric, îl returnează
            # Altfel, afișează un mesaj și repetă
            if user_input.isnumeric():
                return int(user_input)
            else:
                print("Inputul trebuie să fie un număr")
                continue     
             
    def print_welcome_message(self):
        print("JOCUL ÎNMULȚIRILOR CU PYTHON".center(self.message_lenght))

    def print_lose_message(self):
        print("ÎMI PARE RĂU, AI PIERDUT TOATE VIEȚILE".center(self.message_lenght))

    def print_win_message(self):
        print(f"FELICITĂRI, AI AJUNS LA {self.points} PUNCTE".center(self.message_lenght))
        
    def print_current_lives(self):
        print(f"În prezent ai {self.lives} vieți\n")

    def print_current_score(self):
        print(f"\nScorul tău este {self.points}")

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

    # Metoda de bază pentru rulare
    def run(self):
        self.print_welcome_message()
        
        self.print_description()

Această clasă pare destul de complexă. Permiteți-mi să vă explic în detaliu.

În primul rând, să analizăm atributele clasei și constructorul.

Atributele clasei sunt variabile definite în interiorul clasei, dar în afara constructorului sau a oricărei metode.

Atributele instanței sunt variabile create doar în interiorul constructorului.

Diferența principală dintre cele două este raza de acțiune. Atributele clasei sunt accesibile atât din interiorul obiectului instanță, cât și din clasă. Pe de altă parte, atributele instanței sunt accesibile doar dintr-un obiect instanță.

game = BaseGame(5)

# Accesarea atributului de clasă message_lenght din interiorul clasei
print(game.message_lenght) # 60

# Accesarea atributului de clasă message_lenght din interiorul clasei
print(BaseGame.message_lenght)  # 60

# Accesarea atributului de instanță points din interiorul instanței
print(game.points) # 0

# Accesarea atributului de instanță points din interiorul clasei
print(BaseGame.points) # Eroare de atribut

Un alt material ar putea aprofunda acest subiect. Urmăriți-ne pentru a-l citi.

Funcția get_numeric_input previne introducerea de date non-numerice de către utilizator. Această metodă solicită input utilizatorului până când acesta oferă o intrare numerică. O vom folosi ulterior în clasele derivate.

Metodele de afișare ne ajută să evităm repetarea codului atunci când trebuie să afișăm mesaje la anumite evenimente din joc.

Metoda run este o interfață pe care clasele de înmulțiri aleatorii și tabele de înmulțire o vor folosi pentru a interacționa cu utilizatorul și pentru a face totul funcțional.

Crearea Claselor Copil

Acum, că am creat clasa părinte, care definește structura și o parte din funcționalitatea aplicației noastre, este timpul să construim clasele care implementează modurile de joc, folosind moștenirea.

Clasa pentru Înmulțiri Aleatorii

Această clasă va implementa primul mod al jocului. Va utiliza modulul random pentru a genera întrebări aleatorii cu numere de la 1 la 10. Iată un articol util despre modulele random (și alte module importante) 😉.

import random # Modul pentru operații aleatorii
class RandomMultiplication(BaseGame):

    description = "În acest joc trebuie să răspunzi corect la înmulțirile aleatorii.\nCâștigi dacă ajungi la 5 puncte sau pierzi dacă îți pierzi toate viețile"

    def __init__(self):
        # Numărul de puncte necesare pentru a câștiga este 5
        # Transmite argumentul 5 la "points_to_win"
        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):
        
        # Apeleză clasa de bază pentru a afișa mesajele de bun venit
        super().run()
        

        while self.lives > 0 and self.points_to_win > self.points:
            # Obține două numere aleatorii
            number1, number2 = self.get_random_numbers()

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

            # Solicită utilizatorului să răspundă la operație
            # Previne erori de valoare
            user_answer = self.get_numeric_input(message=operation)

            if user_answer == number1 * number2:
                print("\nRăspunsul tău este corect!\n")
                
                # Adaugă un punct
                self.points += 1
            else:
                print("\nÎmi pare rău, răspunsul tău este incorect!\n")

                # Scade o viață
                self.lives -= 1
            
            self.print_current_score()
            self.print_current_lives()
            
        # Se execută doar când jocul s-a terminat
        # Și niciuna dintre condiții nu este adevărată
        else:
            # Afișează mesajul final
            
            if self.points >= self.points_to_win:
                self.print_win_message()
            else:
                self.print_lose_message()

Aceasta este o altă clasă extinsă 😅. Însă, după cum am menționat, nu contează numărul de linii de cod, ci cât de lizibil și eficient este acesta. Python permite dezvoltatorilor să creeze cod curat și ușor de înțeles, ca și cum ar vorbi o limbă naturală.

Această clasă are un aspect care ar putea crea confuzie, dar îl voi explica cât mai simplu posibil.

    # Clasa părinte
    def __init__(self, points_to_win, n_lives=3):
        "...
    # Clasa copil
    def __init__(self):
        # Numărul de puncte necesare pentru a câștiga este 5
        # Transmite argumentul 5 la "points_to_win"
        super().__init__(5)

Constructorul clasei copil apelează funcția super, care, la rândul ei, se referă la clasa părinte (BaseGame). Practic, îi spune lui Python:

Completează atributul „points_to_win” al clasei părinte cu valoarea 5!

Nu este necesar să folosim self în interiorul funcției super().__init__(), deoarece apelăm super din constructor și ar fi redundant.

De asemenea, folosim funcția super în metoda run, și vom vedea ce se întâmplă acolo.

    # Metoda de bază pentru rulare
    # Metoda părinte
    def run(self):
        self.print_welcome_message()
        
        self.print_description()
    def run(self):
        
        # Apeleză clasa de bază pentru a afișa mesajele de bun venit
        super().run()
        
        .....

După cum vedeți, metoda run din clasa părinte afișează mesajul de bun venit și descrierea. Este o practică bună să păstrăm această funcționalitate și să adăugăm funcții suplimentare în clasele derivate. De aceea, folosim super pentru a rula codul metodei părinte înainte de a executa codul clasei copil.

Restul funcției run este destul de simplu. Solicităm utilizatorului un număr ca răspuns la operația afișată. Comparăm răspunsul cu rezultatul corect al înmulțirii. Dacă sunt egale, adăugăm un punct, altfel, scădem o viață.

Folosim bucle while-else. Acest lucru depășește subiectul acestui articol, dar voi publica un material despre acest subiect în câteva zile.

În final, metoda get_random_numbers folosește funcția random.randint, care returnează un număr întreg aleatoriu dintr-un interval specificat. Apoi, returnează un tuplu cu două numere întregi aleatorii.

Clasa pentru Tabele de Înmulțire

Al doilea mod al jocului va afișa tabelele de înmulțire și se va asigura că utilizatorul rezolvă corect cel puțin 2 tabele.

Vom utiliza din nou funcția super și vom modifica atributul points_to_win al clasei părinte la 2.

class TableMultiplication(BaseGame):

    description = "În acest joc trebuie să rezolvi corect tabelul de înmulțire complet.\nCâștigi dacă rezolvi corect 2 tabele"
    
    def __init__(self):
        # Trebuie să completeze 2 tabele pentru a câștiga
        super().__init__(2)

    def run(self):

        # Afișează mesajele de bun venit
        super().run()

        while self.lives > 0 and self.points_to_win > self.points:
            # Obține un număr aleatoriu
            number = random.randint(1, 10)            

            for i in range(1, 11):
                
                if self.lives <= 0:
                    # Se asigură că jocul nu mai continuă
                    # dacă utilizatorul a pierdut toate viețile

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

                user_answer = self.get_numeric_input(message=operation)

                if user_answer == number * i:
                    print("Excelent! Răspunsul tău este corect")
                else:
                    print("Îmi pare rău, răspunsul tău nu este corect") 

                    self.lives -= 1

            self.points += 1
            
        # Se execută doar când jocul s-a terminat
        # Și niciuna dintre condiții nu este adevărată
        else:
            # Afișează mesajul final
            
            if self.points >= self.points_to_win:
                self.print_win_message()
            else:
                self.print_lose_message()

După cum ați observat, modificăm doar metoda run în această clasă. Aceasta este magia moștenirii. Scriem o singură dată o logică pe care o folosim în mai multe locuri 😅.

În metoda run, folosim o buclă for pentru a obține numerele de la 1 la 10 și pentru a construi operația care va fi afișată utilizatorului.

Din nou, dacă viețile sunt epuizate sau dacă punctele necesare pentru a câștiga sunt atinse, bucla while se va opri, iar mesajul de victorie sau înfrângere va fi afișat.

Am creat ambele moduri ale jocului, dar până acum, dacă am rula programul, nu s-ar întâmpla nimic.

Deci, vom finaliza programul prin implementarea alegerii modului și instanțierea claselor în funcție de alegere.

Implementarea Sistemului de Alegere

Utilizatorul va putea alege ce mod dorește să joace. Vom implementa acest sistem astfel:

if __name__ == "__main__":

    print("Selectează modul de joc")

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

    if choice == "1":
        game = RandomMultiplication()
    elif choice == "2":
        game = TableMultiplication()
    else:
        print("Selectează un mod de joc valid")
        exit()

    game.run()

Mai întâi, solicităm utilizatorului să aleagă între modul 1 sau 2. Dacă inputul nu este valid, scriptul se oprește. Dacă utilizatorul selectează primul mod, programul va rula modul de joc Random Multiplication. Dacă selectează al doilea mod, va fi rulat modul Table Multiplication.

Iată cum va arăta:

Concluzie

Felicitări! Tocmai ați dezvoltat o aplicație Python folosind principiile programării orientate pe obiecte.

Întregul cod este disponibil în depozitul Github.

În acest articol ați învățat cum să:

  • Utilizați constructorii de clasă Python.
  • Creați o aplicație funcțională folosind POO.
  • Utilizați funcția super în clasele Python.
  • Aplicați conceptele de bază ale moștenirii.
  • Implementați atribute de clasă și instanță.

Spor la codat 👨‍💻

De asemenea, puteți explora cele mai bune IDE-uri Python pentru o productivitate mai bună.