Zbuduj aplikację tabliczki mnożenia w Pythonie za pomocą OOP

W tym artykule zamierzasz zbudować aplikację tabliczki mnożenia, korzystając z możliwości programowania obiektowego (OOP) w języku Python.

Przećwiczysz główne koncepcje OOP i jak ich używać w w pełni funkcjonalnej aplikacji.

Python to wieloparadygmatyczny język programowania, co oznacza, że ​​jako programiści możemy wybrać najlepszą opcję dla każdej sytuacji i problemu. Kiedy mówimy o programowaniu obiektowym, mamy na myśli jeden z najczęściej używanych paradygmatów do tworzenia skalowalnych aplikacji w ostatnich dziesięcioleciach.

Podstawy OOP

Przyjrzymy się pokrótce najważniejszej koncepcji OOP w Pythonie, czyli klasom.

Klasa to szablon, w którym definiujemy strukturę i zachowanie obiektów. Ten szablon pozwala nam tworzyć Instancje, które są niczym innym jak pojedynczymi obiektami wykonanymi zgodnie z kompozycją klasy.

Prosta klasa książki z atrybutami tytułu i koloru byłaby zdefiniowana w następujący sposób.

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

Jeśli chcemy utworzyć instancje klasy book, musimy wywołać klasę i przekazać jej argumenty.

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

Dobrą reprezentacją naszego obecnego programu byłoby:

Niesamowite jest to, że kiedy sprawdzamy typ instancji blue_book i green_book, otrzymujemy „Book”.

# Printing the type of the books

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

Po krystalicznie czystym zrozumieniu tych koncepcji możemy przystąpić do budowania projektu 😃.

Oświadczenie projektowe

Pracując jako programiści/programiści, większość czasu nie spędza się na pisaniu kodu nowy stos spędzamy tylko jedną trzecią naszego czasu na pisaniu lub refaktoryzacji kodu.

Pozostałe dwie trzecie spędziliśmy na czytaniu kodu innych i analizowaniu problemu, nad którym pracujemy.

Więc dla tego projektu wygeneruję opis problemu i przeanalizujemy, jak stworzyć z niego naszą aplikację. W efekcie realizujemy cały proces, od zastanowienia się nad rozwiązaniem po jego zastosowanie z kodem.

Nauczyciel szkoły podstawowej chce gry sprawdzającej umiejętności mnożenia uczniów w wieku od 8 do 10 lat.

Gra musi mieć system życia i punktów, w którym uczeń zaczyna z 3 życiami i musi osiągnąć określoną liczbę punktów, aby wygrać. Program musi wyświetlać komunikat „przegraj”, jeśli uczeń wyczerpuje całe swoje życie.

Gra musi mieć dwa tryby, losowe mnożenia i mnożenia tabeli.

Pierwszy powinien dać uczniowi losowe pomnożenie od 1 do 10, a on / ona musi odpowiedzieć poprawnie, aby zdobyć punkt. Jeśli tak się nie stanie, uczeń traci życie i gra toczy się dalej. Uczeń wygrywa tylko wtedy, gdy zdobędzie 5 punktów.

Drugi tryb musi wyświetlać tabliczkę mnożenia od 1 do 10, gdzie uczeń musi wprowadzić wynik odpowiedniego mnożenia. Jeśli studentowi nie powiedzie się 3 razy, przegrywa, ale jeśli ukończy dwa stoły, gra się kończy.

Wiem, że wymagania może trochę większe, ale obiecuję, że w tym artykule je rozwiążemy 😁.

Dziel i rządź

Najważniejszą umiejętnością w programowaniu jest rozwiązywanie problemów. Dzieje się tak, ponieważ musisz mieć plan, zanim zaczniesz hakować kod.

Zawsze sugeruję wziąć większy problem i podzielić go na mniejsze, które można łatwo i skutecznie rozwiązać.

Jeśli więc chcesz stworzyć grę, zacznij od podzielenia jej na najważniejsze części. Te podproblemy będą znacznie łatwiejsze do rozwiązania.

Właśnie wtedy możesz mieć jasność, jak wykonać i zintegrować wszystko z kodem.

Zróbmy więc wykres, jak wyglądałaby gra.

Ta grafika przedstawia relacje między obiektami naszej aplikacji. Jak widać, dwa główne obiekty to mnożenie losowe i mnożenie tabeli. A jedyne, co je łączy, to atrybuty Punkty i Życia.

Mając wszystkie te informacje na uwadze, przejdźmy do kodu.

Tworzenie klasy gry Parent

Kiedy pracujemy z programowaniem zorientowanym obiektowo, szukamy najczystszego sposobu na uniknięcie powtarzania kodu. To się nazywa SUCHY (nie powtarzaj się).

Uwaga: ten cel nie jest związany z pisaniem mniejszej liczby linii kodu (jakość kodu nie może być mierzona tym aspektem), ale z abstrakcją najczęściej używanej logiki.

Zgodnie z poprzednim pomysłem, klasa nadrzędna naszej aplikacji musi ustalić strukturę i pożądane zachowanie pozostałych dwóch klas.

Zobaczmy, jak by to było zrobione.

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, wydaje się, że to całkiem duża klasa. Pozwól, że wyjaśnię to dogłębnie.

Przede wszystkim zrozummy atrybuty klasy i konstruktora.

Zasadniczo atrybuty klasy to zmienne utworzone wewnątrz klasy, ale poza konstruktorem lub jakąkolwiek metodą.

Podczas gdy atrybuty instancji są zmiennymi tworzonymi tylko wewnątrz konstruktora.

Główną różnicą między tymi dwoma jest zakres. tj. atrybuty klasy są dostępne zarówno z obiektu instancji, jak iz klasy. Z drugiej strony atrybuty instancji są dostępne tylko z obiektu instancji.

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

Inny artykuł może zagłębić się w ten temat. Pozostań w kontakcie, aby go przeczytać.

Funkcja get_numeric_input służy do uniemożliwienia użytkownikowi wprowadzania jakichkolwiek danych, które nie są numeryczne. Jak możesz zauważyć, ta metoda jest zaprojektowana tak, aby pytać użytkownika, dopóki nie otrzyma danych liczbowych. Wykorzystamy go później na zajęciach dziecka.

Metody drukowania pozwalają nam zaoszczędzić powtarzania drukowania tego samego za każdym razem, gdy wystąpi zdarzenie w grze.

Wreszcie, metoda run jest tylko opakowaniem, którego klasy Random mnożenie i mnożenie tabeli będą używać do interakcji z użytkownikiem i sprawienia, by wszystko działało.

Tworzenie klas dziecka

Po utworzeniu tej klasy nadrzędnej, która określa strukturę i niektóre funkcje naszej aplikacji, nadszedł czas na zbudowanie rzeczywistych klas trybu gry, korzystając z mocy dziedziczenia.

Losowa klasa mnożenia

Ta klasa uruchomi „pierwszy tryb” naszej gry. Będzie oczywiście korzystał z modułu losowego, co da nam możliwość zadania użytkownikowi losowych operacji od 1 do 10. Oto doskonały artykuł o losowych (i innych ważnych modułach) 😉.

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

Oto kolejna masowa klasa 😅. Ale jak powiedziałem wcześniej, nie chodzi o liczbę linii, które zajmuje, ale o to, jak bardzo jest czytelny i wydajny. A najlepsze w Pythonie jest to, że pozwala programistom tworzyć czysty i czytelny kod, tak jakby mówili normalnym angielskim.

Te zajęcia mają jedną rzecz, która może cię zmylić, ale wyjaśnię to tak prosto, jak to tylko możliwe.

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

Konstruktor klasy potomnej wywołuje superfunkcję, która jednocześnie odwołuje się do klasy nadrzędnej (BaseGame). Zasadniczo mówi Pythonowi:

Wypełnij atrybut „points_to_win” klasy nadrzędnej wartością 5!

Nie jest konieczne umieszczanie self w części super().__init__() tylko dlatego, że wywołujemy super wewnątrz konstruktora, co spowodowałoby nadmiarowość.

Używamy również funkcji super w metodzie run i zobaczymy, co dzieje się w tym fragmencie kodu.

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

Jak możesz zauważyć metodę run w klasie nadrzędnej, wydrukuj wiadomość powitalną i opisową. Ale dobrym pomysłem jest zachowanie tej funkcjonalności, a także dodanie dodatkowych w klasach potomnych. Zgodnie z tym używamy super, aby uruchomić cały kod metody nadrzędnej przed uruchomieniem następnego elementu.

Druga część funkcji run jest całkiem prosta. Prosi użytkownika o numer z komunikatem operacji, na którą musi odpowiedzieć. Następnie wynik porównuje się z rzeczywistym mnożeniem i jeśli są równe, dodaje punkt, jeśli nie odbierają 1 życia.

Warto powiedzieć, że używamy pętli while-else. To wykracza poza zakres tego artykułu, ale opublikuję go za kilka dni.

Wreszcie get_random_numbers używa funkcji random.randint, która zwraca losową liczbę całkowitą z określonego zakresu. Następnie zwraca krotkę dwóch losowych liczb całkowitych.

Losowa klasa mnożenia

„Drugi tryb” musi wyświetlać grę w formacie tabliczki mnożenia i upewnić się, że użytkownik odpowie poprawnie na co najmniej 2 tabliczki.

W tym celu ponownie użyjemy potęgi super i zmodyfikujemy atrybut klasy nadrzędnej points_to_win na 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()

Jak możesz zauważyć, modyfikujemy tylko metodę run tej klasy. Na tym polega magia dziedziczenia, piszemy raz logikę, której używamy w wielu miejscach i zapominamy o tym 😅.

W metodzie run używamy pętli for, aby uzyskać liczby od 1 do 10 i zbudowaliśmy operację, która jest pokazywana użytkownikowi.

Ponownie, jeśli liczba żyć zostanie wyczerpana lub zostanie osiągnięta liczba punktów potrzebnych do wygrania, pętla while zostanie przerwana i zostanie wyświetlony komunikat o wygranej lub przegranej.

TAK, stworzyliśmy dwa tryby gry, ale do tej pory, jeśli uruchomimy program, nic się nie stanie.

Skończmy więc program, implementując wybór trybu i tworząc instancje klas w zależności od tego wyboru.

Realizacja wyboru

Użytkownik będzie mógł wybrać, w jakim trybie chce grać. Zobaczmy więc, jak to wdrożyć.

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

Najpierw prosimy użytkownika o wybranie pomiędzy trybami 1 lub 2. Jeśli dane wejściowe są nieprawidłowe, skrypt przestaje działać. Jeśli użytkownik wybierze pierwszy tryb, program uruchomi tryb gry Losowe mnożenie, a jeśli wybierze drugi tryb, uruchomi się tryb mnożenia tablicowego.

Oto jak by to wyglądało.

Wniosek

Gratulacje, po prostu zbudować aplikację w Pythonie z programowaniem obiektowym.

Cały kod jest dostępny w Repozytorium Githuba.

W tym artykule nauczyłeś się:

  • Użyj konstruktorów klas Pythona
  • Stwórz funkcjonalną aplikację z OOP
  • Użyj funkcji super w klasach Pythona
  • Zastosuj podstawowe pojęcia dotyczące dziedziczenia
  • Zaimplementuj atrybuty klasy i instancji

Miłego kodowania 👨‍💻

Następnie zapoznaj się z najlepszymi środowiskami IDE w języku Python, aby zwiększyć produktywność.