Створіть програму таблиці множення Python за допомогою ООП

У цьому посібнику ви розробите застосунок для тренування таблиці множення, використовуючи принципи об’єктно-орієнтованого програмування (ООП) на Python.

Ви зможете поглибити свої знання основних концепцій ООП і навчитеся їх практичному застосуванню у створенні повноцінної програми.

Python – це мова програмування, яка підтримує різні парадигми, що дає нам, розробникам, можливість вибирати найбільш відповідний підхід для кожної конкретної ситуації. Об’єктно-орієнтоване програмування є однією з найпоширеніших парадигм для створення масштабованих програм протягом багатьох років.

Основи ООП: Короткий Огляд

Ми швидко розглянемо ключову концепцію ООП в Python – класи.

Клас — це свого роду шаблон, де ми визначаємо структуру та поведінку об’єктів. Цей шаблон дозволяє нам створювати екземпляри, які є нічим іншим, як окремими об’єктами, що відповідають структурі класу.

Розглянемо простий клас книги з атрибутами “назва” і “колір”. Він буде визначений наступним чином:

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

Щоб створити екземпляри класу “Книга”, потрібно викликати клас і передати йому необхідні аргументи.

# Створення екземплярів класу Book
blue_book = Book("Синій малюк", "Синій")
green_book = Book("Історія жаби", "Зелений")

Чудовою ілюстрацією нашої поточної програми буде:

Важливо зазначити, що при перевірці типу об’єктів “blue_book” і “green_book”, ми отримаємо тип “Book”.

# Виведення типу об'єктів
print(type(blue_book))
# <class '__main__.Book'>
print(type(green_book))
# <class '__main__.Book'>

З цими базовими поняттями, ми готові розпочати розробку нашого проекту 😃.

Опис Проекту

Згідно з дослідженнями thenewstack, розробники витрачають значну частину свого часу не на написання коду, а на його аналіз та розуміння.

Лише третину часу займає написання або рефакторинг коду.

Решту часу ми присвячуємо вивченню коду інших розробників та аналізу завдань, над якими працюємо. Тому для цього проекту я спочатку сформулюю проблему, а потім ми проаналізуємо, як перетворити її на працюючу програму. Таким чином, ми пройдемо повний цикл від обдумування рішення до його реалізації за допомогою коду.

Вчитель початкових класів хоче створити гру для перевірки навичок множення учнів 8-10 років.

Гра повинна мати систему життя та набору очок. Учень починає з 3 життями і повинен набрати певну кількість очок для перемоги. У випадку втрати всіх життів, програма повинна вивести повідомлення “Програш”.

Гра має два режими: “випадкове множення” та “табличне множення”.

У першому режимі учню пропонуються випадкові приклади на множення чисел від 1 до 10. За кожну правильну відповідь він отримує одне очко. Якщо відповідь неправильна, учень втрачає одне життя. Для перемоги необхідно набрати 5 очок.

У другому режимі потрібно відтворити таблицю множення від 1 до 10. Учень вводить результат відповідного множення. Гра закінчується поразкою, якщо учень тричі помилиться, і перемогою, якщо він успішно пройде дві таблиці.

Я розумію, що вимоги можуть здатися складними, але обіцяю, що ми їх розв’яжемо в рамках цієї статті 😁.

Метод “Розділяй та Владарюй”

Ключовою навичкою в програмуванні є вміння розв’язувати проблеми. Для цього потрібно скласти план дій перед початком написання коду.

Завжди рекомендую розбивати велику проблему на менші, які можна вирішити простіше та ефективніше.

Тому, якщо ви хочете створити гру, почніть з визначення її основних компонентів. Ці складові частини буде набагато легше реалізувати окремо.

Після цього вам стане зрозуміло, як реалізувати та інтегрувати все за допомогою коду.

Спробуємо візуалізувати структуру нашої гри.

На цій діаграмі зображено взаємозв’язки між об’єктами нашої програми. Як видно, двома основними об’єктами є “випадкове множення” і “табличне множення”. Загальними атрибутами для обох є кількість очок (“Points”) і кількість життів (“Lives”).

З огляду на це, перейдемо до написання коду.

Створення Базового Класу Ігри

В об’єктно-орієнтованому програмуванні важливо уникати дублювання коду. Цей принцип називається DRY (не повторюйся).

Примітка: Мета полягає не в написанні меншої кількості рядків коду, а в абстрагуванні часто використовуваної логіки. Якість коду не повинна вимірюватися кількістю рядків.

Згідно з цією ідеєю, базовий клас нашої програми повинен встановити структуру та поведінку для двох інших класів.

Подивимося, як це буде зроблено.

class BaseGame:

    # Довжина повідомлення для центрування
    message_lenght = 60
    
    description = ""    
        
    def __init__(self, points_to_win, n_lives=3):
        """Базовий клас гри

        Args:
            points_to_win (int): кількість очок, необхідна для перемоги
            n_lives (int): кількість життів гравця. За замовчуванням 3.
        """
        self.points_to_win = points_to_win

        self.points = 0
        
        self.lives = n_lives

    def get_numeric_input(self, message=""):

        while True:
            # Отримання вводу користувача
            user_input = input(message) 
            
            # Якщо ввід є числом, повернути його
            # Якщо ні, вивести повідомлення і повторити
            if user_input.isnumeric():
                return int(user_input)
            else:
                print("Ввід повинен бути числом")
                continue     
             
    def print_welcome_message(self):
        print("ІГРА МНОЖЕННЯ НА PYTHON".center(self.message_lenght))

    def print_lose_message(self):
        print("ВИ, НА ЖАЛЬ, ВТРАТИЛИ ВСІ ЖИТТЯ".center(self.message_lenght))

    def print_win_message(self):
        print(f"ВІТАЄМО! ВИ НАБРАЛИ {self.points} ОЧОК".center(self.message_lenght))
        
    def print_current_lives(self):
        print(f"Зараз у вас {self.lives} життів\n")

    def print_current_score(self):
        print(f"\nВаш рахунок {self.points}")

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

    # Основний метод запуску
    def run(self):
        self.print_welcome_message()
        
        self.print_description()

Так, цей клас виглядає доволі об’ємним. Давайте детальніше його розглянемо.

Почнемо з атрибутів класу та конструктора.

Атрибути класу — це змінні, які визначені всередині класу, але поза конструктором чи будь-яким методом.

Атрибути екземпляра — це змінні, які створюються лише в конструкторі.

Основна відмінність між ними полягає в області видимості. Тобто, атрибути класу доступні як з об’єкта екземпляра, так і з самого класу. Натомість атрибути екземпляра доступні лише з об’єкта екземпляра.

game = BaseGame(5)

# Доступ до атрибуту класу message_lenght через об'єкт
print(game.message_lenght) # 60

# Доступ до атрибуту класу message_lenght через клас
print(BaseGame.message_lenght)  # 60

# Доступ до атрибуту екземпляра points через об'єкт
print(game.points) # 0

# Спроба доступу до атрибуту екземпляра points через клас
print(BaseGame.points) # Помилка: AttributeError

Цю тему можна розглянути глибше в окремій статті. Залишайтеся з нами, щоб не пропустити її.

Функція “get_numeric_input” призначена для запобігання введення користувачем будь-яких нечислових значень. Як ви могли помітити, метод запитує дані у користувача до тих пір, поки не отримає числове значення. Це буде використано в процесі гри.

Методи виведення повідомлень допомагають уникнути повторення одного і того ж коду при кожній події в грі.

І, нарешті, метод “run” — це просто “оболонка”, яка використовуватиметься класами “випадкового множення” і “табличного множення” для взаємодії з користувачем та забезпечення функціональності гри.

Створення Дочірніх Класів

Після створення базового класу, який задає структуру та основні можливості нашої програми, настав час створити класи, що реалізують режими гри, використовуючи механізм успадкування.

Клас “Випадкове Множення”

Цей клас реалізує перший режим нашої гри. Він використовує модуль “random”, який дає змогу генерувати випадкові приклади множення чисел від 1 до 10. Ось цікава стаття про модуль random та інші важливі модулі 😉.

import random # Модуль для випадкових операцій
class RandomMultiplication(BaseGame):

    description = "У цій грі потрібно правильно розв'язувати приклади на випадкове множення\nВи виграєте, якщо наберете 5 очок, або програєте, якщо втратите всі життя"

    def __init__(self):
        # Кількість очок, необхідних для перемоги, становить 5
        # Передаємо 5 в аргумент "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):
        
        # Виклик методу батьківського класу для виведення привітальних повідомлень
        super().run()
        

        while self.lives > 0 and self.points_to_win > self.points:
            # Отримання двох випадкових чисел
            number1, number2 = self.get_random_numbers()

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

            # Запит відповіді у користувача
            # Запобігання помилкам введення
            user_answer = self.get_numeric_input(message=operation)

            if user_answer == number1 * number2:
                print("\nВаша відповідь правильна\n")
                
                # Додавання одного очка
                self.points += 1
            else:
                print("\nНа жаль, ваша відповідь неправильна\n")

                # Віднімання одного життя
                self.lives -= 1
            
            self.print_current_score()
            self.print_current_lives()
            
        # Виконується лише після завершення гри
        # І коли жодна з умов не є істинною
        else:
            # Виведення фінального повідомлення
            
            if self.points >= self.points_to_win:
                self.print_win_message()
            else:
                self.print_lose_message()

Так, це ще один великий клас 😅. Але, як я зазначав раніше, важлива не кількість рядків, а те, наскільки код зрозумілий та ефективний. Python дозволяє розробникам створювати чистий і читабельний код, ніби вони спілкуються звичайною мовою.

В цьому класі є один момент, який може викликати запитання, але я поясню його максимально просто.

    # Батьківський клас
    def __init__(self, points_to_win, n_lives=3):
        "...
    # Дочірній клас
    def __init__(self):
        # Кількість очок, необхідна для перемоги, становить 5
        # Передаємо 5 в аргумент "points_to_win"
        super().__init__(5)

Конструктор дочірнього класу викликає функцію “super”, яка, в свою чергу, посилається на конструктор батьківського класу (BaseGame). Простіше кажучи, це повідомляє Python:

“Присвой атрибуту points_to_win батьківського класу значення 5!”

Немає потреби використовувати self в super().__init__(), оскільки ми викликаємо super в конструкторі, і це буде зайвим.

Функція super також використовується в методі run, і ми зараз розглянемо, що відбувається в цьому фрагменті коду.

    # Основний метод запуску
    # Метод батьківського класу
    def run(self):
        self.print_welcome_message()
        
        self.print_description()
    def run(self):
        
        # Виклик методу батьківського класу для виведення привітальних повідомлень
        super().run()
        
        .....

Як ви можете бачити, метод run в батьківському класі виводить привітальне повідомлення та опис гри. Але було б зручно зберегти цю функціональність і додати додаткові дії в дочірніх класах. Тому ми використовуємо super для виконання коду батьківського методу перед тим, як виконається наступний код.

Інша частина функції запуску є досить простою. Вона запитує у користувача число і виводить приклад множення, який потрібно розв’язати. Потім результат порівнюється з правильним значенням. Якщо вони рівні, додається очко, інакше знімається одне життя.

Варто зазначити, що ми використовуємо цикли while-else. Це вже виходить за рамки цієї статті, але я підготую про це матеріал в найближчі дні.

Нарешті, get_random_numbers використовує функцію random.randint, яка повертає випадкове ціле число в заданому діапазоні. Потім функція повертає кортеж з двох випадкових цілих чисел.

Клас “Табличне Множення”

Другий режим повинен відображати таблицю множення і перевіряти, що користувач правильно розв’язує щонайменше 2 таблиці.

Знову ж таки, ми використовуємо можливості super і змінюємо атрибут points_to_win батьківського класу на 2.

class TableMultiplication(BaseGame):

    description = "У цій грі потрібно правильно розв'язати всю таблицю множення\nВи виграєте, якщо розв'яжете 2 таблиці"
    
    def __init__(self):
        # Потрібно розв'язати 2 таблиці для перемоги
        super().__init__(2)

    def run(self):

        # Виведення привітальних повідомлень
        super().run()

        while self.lives > 0 and self.points_to_win > self.points:
            # Отримання випадкового числа
            number = random.randint(1, 10)            

            for i in range(1, 11):
                
                if self.lives <= 0:
                    # Забезпечення завершення гри
                    # у разі втрати всіх життів

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

                user_answer = self.get_numeric_input(message=operation)

                if user_answer == number * i:
                    print("Чудово! Ваша відповідь правильна")
                else:
                    print("На жаль, ваша відповідь неправильна") 

                    self.lives -= 1

            self.points += 1
            
        # Виконується лише після завершення гри
        # І коли жодна з умов не є істинною
        else:
            # Виведення фінального повідомлення
            
            if self.points >= self.points_to_win:
                self.print_win_message()
            else:
                self.print_lose_message()

Як бачите, ми лише модифікували метод запуску в цьому класі. Це і є магія успадкування – ми записуємо логіку в одному місці, а потім просто використовуємо її у різних частинах програми 😅.

У методі run ми використовуємо цикл for для отримання чисел від 1 до 10 і створюємо вираз для виведення користувачу.

Знову ж таки, якщо життя закінчилися або досягнута кількість очок, необхідна для перемоги, цикл while перерветься, і відобразиться повідомлення про виграш або програш.

ТАК, ми створили два режими гри, але поки що, якщо запустити програму, нічого не відбудеться.

Тому давайте завершимо програму, додавши вибір режиму та створивши екземпляри класів відповідно до цього вибору.

Реалізація Вибору Режиму

Користувач має можливість вибрати, в якому режимі він хоче грати. Давайте подивимося, як це реалізувати.

if __name__ == "__main__":

    print("Виберіть режим гри")

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

    if choice == "1":
        game = RandomMultiplication()
    elif choice == "2":
        game = TableMultiplication()
    else:
        print("Будь ласка, виберіть коректний режим гри")
        exit()

    game.run()

Спершу ми пропонуємо користувачу вибрати між режимами 1 і 2. Якщо введений вибір невірний, програма припиняє роботу. Якщо користувач вибирає режим 1, запускається гра у режимі “випадкового множення”, інакше запускається режим “табличного множення”.

Ось так це виглядає на практиці.

Висновок

Вітаю, ви щойно створили програму на Python з використанням об’єктно-орієнтованого програмування.

Весь код доступний в репозиторії Github.

У цій статті ви навчилися:

  • Використовувати конструктори класів Python
  • Створювати функціональні програми за допомогою ООП
  • Застосовувати функцію super у класах Python
  • Застосовувати основні принципи успадкування
  • Реалізовувати атрибути класу та екземпляра

Успіхів у програмуванні 👨‍💻

Далі ознайомтеся з найкращими IDE для Python для підвищення продуктивності.