W tym opracowaniu stworzymy program do nauki tabliczki mnożenia, wykorzystując paradygmat programowania obiektowego (OOP) w języku Python.
Przećwiczymy podstawowe założenia OOP i pokażemy, jak je stosować w praktycznej, działającej aplikacji.
Python jest językiem wieloparadygmatowym, co oznacza, że programiści mają możliwość wyboru najodpowiedniejszego podejścia do danego zadania. Programowanie obiektowe jest jednym z najczęściej wybieranych paradygmatów do tworzenia rozbudowanych aplikacji w ostatnich dekadach.
Podstawowe informacje o OOP
Skupimy się na kluczowym elemencie OOP w Pythonie, czyli klasach.
Klasa to wzorzec, w którym określamy strukturę i zachowanie obiektów. Ten szablon umożliwia nam tworzenie Instancji, które są konkretnymi obiektami utworzonymi zgodnie ze specyfikacją klasy.
Przykładowa klasa książki, posiadająca właściwości tytułu i koloru, zostałaby zdefiniowana w następujący sposób.
class Book: def __init__(self, title, color): self.title = title self.color = color
Aby utworzyć obiekty klasy Book, należy wywołać klasę i przekazać do niej argumenty.
# Tworzenie obiektów klasy Book blue_book = Book("Niebieski urwis", "Niebieski") green_book = Book("Opowieść o żabie", "Zielony")
Zobrazowaniem działania naszego programu byłoby:
Ciekawym faktem jest to, że sprawdzenie typu obiektów blue_book i green_book, pokaże nam, że są one typu „Book”.
# Wyświetlenie typu obiektów print(type(blue_book)) # <class '__main__.Book'> print(type(green_book)) # <class '__main__.Book'>
Mając jasne zrozumienie tych założeń, możemy przejść do budowy projektu 😃.
Opis projektu
W pracy programisty, znaczna część czasu nie jest poświęcana na pisanie kodu. Badania pokazują, że około jedną trzecią czasu spędzamy na tworzeniu lub modyfikacji kodu.
Pozostałe dwie trzecie zajmuje nam czytanie kodu napisanego przez innych i analizowanie problemów, nad którymi pracujemy.
W tym projekcie przedstawię opis problemu, a następnie przeanalizujemy, jak stworzyć z niego naszą aplikację. Prześledzimy cały proces, od opracowania rozwiązania do jego wdrożenia w kodzie.
Nauczyciel w szkole podstawowej potrzebuje gry, która będzie sprawdzać umiejętności mnożenia uczniów w wieku 8-10 lat.
Gra powinna mieć system żyć i punktów. Uczeń zaczyna z 3 życiami i musi zdobyć określoną liczbę punktów, aby wygrać. Program powinien wyświetlać informację o przegranej, jeśli uczeń straci wszystkie życia.
Gra powinna mieć dwa tryby: losowe mnożenie i mnożenie z tabliczki.
W pierwszym trybie uczeń otrzymuje losowe zadanie mnożenia w zakresie 1-10. Poprawna odpowiedź jest nagradzana punktem. Błędna odpowiedź powoduje utratę życia. Uczeń wygrywa po zdobyciu 5 punktów.
W drugim trybie wyświetlana jest tabliczka mnożenia w zakresie 1-10. Uczeń musi wprowadzić wynik odpowiedniego działania. Trzy nieudane próby oznaczają przegraną, a ukończenie dwóch tabliczek mnożenia oznacza zwycięstwo.
Zdaję sobie sprawę, że wymagania mogą być nieco rozbudowane, ale zapewniam, że w tym artykule krok po kroku je zrealizujemy 😁.
Dziel i zwyciężaj
Kluczową umiejętnością w programowaniu jest rozwiązywanie problemów. Przed przystąpieniem do pisania kodu, konieczne jest opracowanie planu.
Zawsze sugeruję, aby rozłożyć większy problem na mniejsze, łatwiejsze do rozwiązania części.
Podzielenie zadania na mniejsze elementy pozwala lepiej zrozumieć, jak podejść do problemu i skutecznie zintegrować poszczególne elementy w kodzie.
Stwórzmy schemat, który zobrazuje strukturę naszej gry.
Grafika przedstawia zależności pomiędzy obiektami w naszej aplikacji. Dwa główne obiekty to losowe mnożenie i tabliczka mnożenia. Wspólnymi atrybutami dla obu trybów są Punkty i Życia.
Mając to wszystko na uwadze, przejdźmy do kodu.
Tworzenie bazowej klasy gry
W programowaniu obiektowym dążymy do uniknięcia powielania kodu. Chodzi o zasadę DRY (nie powtarzaj się).
Uwaga: celem nie jest napisanie jak najmniejszej ilości linii kodu, ale abstrakcja najczęściej wykorzystywanych fragmentów logiki.
Zgodnie z tym założeniem, bazowa klasa naszej aplikacji powinna definiować strukturę i pożądane zachowanie dla pozostałych dwóch klas.
Zobaczmy, jak to zaimplementować.
class BaseGame: # Długość wiadomości dla wycentrowania message_lenght = 60 description = "" def __init__(self, points_to_win, n_lives=3): """Bazowa klasa gry Args: points_to_win (int): liczba punktów potrzebna do ukończenia gry n_lives (int): liczba żyć gracza. Domyślnie 3. """ self.points_to_win = points_to_win self.points = 0 self.lives = n_lives def get_numeric_input(self, message=""): while True: # Pobranie danych od użytkownika user_input = input(message) # Sprawdzenie, czy dane są liczbą # Jeśli tak, zwracamy liczbę if user_input.isnumeric(): return int(user_input) else: print("Wprowadź liczbę") continue def print_welcome_message(self): print("GRA TABLICZKA MNOŻENIA".center(self.message_lenght)) def print_lose_message(self): print("PRZEGRAŁEŚ, STRACIŁEŚ WSZYSTKIE ŻYCIA".center(self.message_lenght)) def print_win_message(self): print(f"GRATULACJE, OSIĄGNĄŁEŚ {self.points}".center(self.message_lenght)) def print_current_lives(self): print(f"Aktualnie masz {self.lives} żyćn") def print_current_score(self): print(f"Twój wynik to {self.points}") def print_description(self): print("nn" + self.description.center(self.message_lenght) + "n") # Podstawowa metoda uruchamiania def run(self): self.print_welcome_message() self.print_description()
Wygląda na sporą klasę. Wyjaśnijmy ją szczegółowo.
Na początku przyjrzyjmy się atrybutom klasy i konstruktora.
Atrybuty klasy to zmienne zdefiniowane wewnątrz klasy, ale poza konstruktorem lub jakąkolwiek inną metodą.
Atrybuty instancji są tworzone tylko wewnątrz konstruktora.
Główną różnicą jest zakres ich dostępności. Atrybuty klasy są dostępne zarówno z instancji obiektu, jak i z samej klasy. Natomiast atrybuty instancji są dostępne tylko z instancji obiektu.
game = BaseGame(5) # Dostęp do atrybutu klasy message_lenght z poziomu obiektu print(game.message_lenght) # 60 # Dostęp do atrybutu klasy message_lenght z poziomu klasy print(BaseGame.message_lenght) # 60 # Dostęp do atrybutu instancji points z poziomu obiektu print(game.points) # 0 # Dostęp do atrybutu instancji points z poziomu klasy print(BaseGame.points) # Błąd - AttributeError
Szersze omówienie tego tematu może pojawić się w osobnym artykule. Zachęcam do śledzenia.
Funkcja get_numeric_input zapewnia, że użytkownik będzie mógł wprowadzić tylko dane liczbowe. Metoda pyta o dane, dopóki nie zostaną wprowadzone dane typu liczbowego. Wykorzystamy ją w klasach pochodnych.
Metody drukowania umożliwiają nam uniknięcie powtarzania kodu podczas wyświetlania komunikatów.
Metoda run jest ogólnym szkieletem, który klasy Random Multiplication i Table Multiplication będą wykorzystywały do interakcji z użytkownikiem i inicjalizowania gry.
Tworzenie klas pochodnych
Po utworzeniu klasy bazowej, która definiuje strukturę i niektóre funkcje naszej aplikacji, możemy przejść do tworzenia klas trybów gry, wykorzystując dziedziczenie.
Klasa losowego mnożenia
Ta klasa będzie odpowiedzialna za obsługę „pierwszego trybu” naszej gry. Wykorzysta moduł random do generowania losowych operacji mnożenia w zakresie 1-10. Polecam artykuł na temat modułów random (i innych ważnych modułów) 😉.
import random # Moduł do operacji losowych
class RandomMultiplication(BaseGame): description = "W tej grze musisz poprawnie odpowiadać na losowe zadania mnożenia.\nWygrasz, jeśli zdobędziesz 5 punktów, a przegrasz, jeśli stracisz wszystkie życia." def __init__(self): # Liczba punktów potrzebnych do wygranej wynosi 5 # Przekazujemy 5 jako argument "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): # Wywołanie metody run z klasy bazowej super().run() while self.lives > 0 and self.points_to_win > self.points: # Pobranie dwóch losowych liczb number1, number2 = self.get_random_numbers() operation = f"{number1} x {number2}: " # Prośba o odpowiedź na działanie user_answer = self.get_numeric_input(message=operation) if user_answer == number1 * number2: print("nPoprawna odpowiedźn") # Dodanie punktu self.points += 1 else: print("nNiestety, odpowiedź jest błędnan") # Utrata życia self.lives -= 1 self.print_current_score() self.print_current_lives() # Wykonanie po zakończeniu gry # Gdy żaden z warunków pętli nie jest prawdziwy else: # Wyświetlenie komunikatu końcowego if self.points >= self.points_to_win: self.print_win_message() else: self.print_lose_message()
Kolejna rozbudowana klasa 😅. Jak już wspominałem, nie chodzi o liczbę linii, ale o czytelność i wydajność. Python umożliwia pisanie czystego i zrozumiałego kodu.
W tej klasie jest jeden element, który może być niejasny, ale postaram się go wytłumaczyć najprościej jak to możliwe.
# Klasa bazowa def __init__(self, points_to_win, n_lives=3): "... # Klasa pochodna def __init__(self): # Liczba punktów potrzebnych do wygranej wynosi 5 # Przekazujemy 5 jako argument "points_to_win" super().__init__(5)
Konstruktor klasy pochodnej wywołuje funkcję super, która odnosi się do klasy bazowej (BaseGame). Mówiąc inaczej:
Wypełnij atrybut „points_to_win” klasy nadrzędnej wartością 5!
Nie musimy umieszczać self w super().__init__(), ponieważ wywołujemy super wewnątrz konstruktora.
Używamy funkcji super również w metodzie run, aby zachować funkcjonalność klasy bazowej.
# Podstawowa metoda run # Metoda klasy bazowej def run(self): self.print_welcome_message() self.print_description() def run(self): # Wywołanie metody run z klasy bazowej super().run() .....
Metoda run z klasy bazowej wyświetla powitanie i opis gry. Chcemy zachować tę funkcjonalność, ale też dodać coś od siebie w klasie pochodnej. Dlatego używamy super, aby wywołać kod metody klasy bazowej przed wykonaniem kodu z klasy pochodnej.
Druga część metody run jest prosta. Prosi użytkownika o podanie wyniku działania. Wynik jest porównywany z poprawnym wynikiem mnożenia, a następnie dodawany jest punkt lub odejmowane życie.
Warto wspomnieć o pętli while-else. W tym artykule nie będziemy jej omawiać szczegółowo.
Funkcja get_random_numbers wykorzystuje funkcję random.randint, która zwraca losową liczbę całkowitą z określonego zakresu. Funkcja zwraca krotkę dwóch losowych liczb całkowitych.
Klasa mnożenia z tabliczki
„Drugi tryb” wyświetla grę w formacie tabliczki mnożenia i sprawdza, czy użytkownik poprawnie rozwiąże co najmniej dwie tabliczki.
Ponownie użyjemy potęgi super i zmodyfikujemy atrybut klasy nadrzędnej points_to_win na 2.
class TableMultiplication(BaseGame): description = "W tej grze musisz rozwiązać poprawnie całą tabliczkę mnożenia.\nWygrasz, jeśli rozwiążesz 2 tabliczki." def __init__(self): # Do wygranej trzeba rozwiązać 2 tabliczki super().__init__(2) def run(self): # Wyświetlenie powitania super().run() while self.lives > 0 and self.points_to_win > self.points: # Pobranie losowej liczby number = random.randint(1, 10) for i in range(1, 11): if self.lives <= 0: # Zapewnienie, że gra się zatrzyma # jeśli użytkownik straci życia self.points = 0 break operation = f"{number} x {i}: " user_answer = self.get_numeric_input(message=operation) if user_answer == number * i: print("Brawo! Poprawna odpowiedź") else: print("Niestety, odpowiedź jest niepoprawna") self.lives -= 1 self.points += 1 # Wykonanie po zakończeniu gry # Gdy żaden z warunków pętli nie jest prawdziwy else: # Wyświetlenie komunikatu końcowego if self.points >= self.points_to_win: self.print_win_message() else: self.print_lose_message()
Jak widzimy, zmieniamy tylko metodę run w tej klasie. Na tym polega siła dziedziczenia. Logikę piszemy raz, a możemy z niej korzystać w wielu miejscach 😅.
W metodzie run używamy pętli for, która iteruje po liczbach od 1 do 10. Budujemy napis, który jest wyświetlany użytkownikowi jako zadanie do rozwiązania.
Gdy gracz straci wszystkie życia lub osiągnie wystarczającą liczbę punktów, pętla while zostanie przerwana i zostanie wyświetlony odpowiedni komunikat.
Stworzyliśmy dwa tryby gry, ale jeśli teraz uruchomimy program, nic się nie wydarzy.
Uzupełnijmy program, implementując wybór trybu i tworząc instancje klas zależnie od wyboru użytkownika.
Realizacja wyboru
Użytkownik będzie miał możliwość wyboru trybu, w którym chce grać. Zobaczmy, jak to zaimplementować.
if __name__ == "__main__": print("Wybierz tryb gry") choice = input("[1],[2]: ") if choice == "1": game = RandomMultiplication() elif choice == "2": game = TableMultiplication() else: print("Wybierz poprawny tryb gry") exit() game.run()
Program prosi użytkownika o wybór trybu 1 lub 2. Jeżeli wybór jest nieprawidłowy, skrypt przestaje działać. Jeśli użytkownik wybierze pierwszy tryb, program uruchomi tryb losowego mnożenia. Jeżeli wybierze drugi tryb, program uruchomi tryb tabliczki mnożenia.
Tak wyglądałoby to w praktyce.
Podsumowanie
Gratulacje! Właśnie stworzyłeś aplikację w Pythonie z wykorzystaniem programowania obiektowego.
Cały kod jest dostępny w repozytorium na GitHubie.
W tym artykule nauczyłeś się:
- Wykorzystywać konstruktory klas w Pythonie
- Tworzyć funkcjonalną aplikację z OOP
- Używać funkcji super w klasach Pythona
- Stosować podstawowe zasady dziedziczenia
- Implementować atrybuty klasy i instancji
Miłego kodowania 👨💻
Zachęcam do zapoznania się z najlepszymi środowiskami IDE w Pythonie, aby zwiększyć swoją produktywność.
newsblog.pl
Maciej – redaktor, pasjonat technologii i samozwańczy pogromca błędów w systemie Windows. Zna Linuxa lepiej niż własną lodówkę, a kawa to jego główne źródło zasilania. Pisze, testuje, naprawia – i czasem nawet wyłącza i włącza ponownie. W wolnych chwilach udaje, że odpoczywa, ale i tak kończy z laptopem na kolanach.