Python · Синтаксис · Початковий рівень

ООП: успадкування та dunder-методи

9 завдань

Завдання по ООП в Python на спадкування та спеціальні методи: дочірні класи, перевизначення поведінки, __str__, __len__, розширення базових класів та робота з об'єктною моделлю.

Успадкування — розширення існуючих класів

#
Успадкування дозволяє одному класу повторно використовувати весь код іншого, а потім розширювати або змінювати конкретні частини. Клас, що успадковує, називається підкласом або дочірнім; клас, від якого успадковують, — батьківським або базовим. ```python class Animal: def __init__(self, name): self.name = name class Dog(Animal): def bark(self): return f"{self.name} says woof" d = Dog("Rex") print(d.name) # Rex — успадковано від Animal print(d.bark()) # Rex says woof ``` Dog успадковує __init__ від Animal, тому Dog("Rex") працює без власного __init__. Dog також отримує всі інші методи Animal. Dog визначає лише те, що є новим або відрізняється. Коли дочірній клас потребує власного __init__, але хоче зберегти ініціалізацію батька — викликаємо super().__init__(): ```python class Admin(User): def __init__(self, name, role): super().__init__(name) # виконує __init__ User self.role = role ``` super() повертає проксі, що делегує до батьківського класу. Це позбавляє від дублювання логіки ініціалізації батька. Перевизначення методу означає визначення методу з тим самим ім'ям у дочірньому класі. Python завжди викликає найспецифічнішу версію — метод дочірнього класу має пріоритет над методом батьківського.

Dunder-методи — __str__ і __len__

#
Dunder-методи (скорочення від "double underscore" — подвійне підкреслення) — це спосіб, яким Python дозволяє вашому класу реагувати на вбудовані операції. __init__ ви вже бачили. Ще два зустрічаються у цих завданнях. __str__ контролює те, що Python відображає при print об'єкта або виклику str() на ньому: ```python class Book: def __init__(self, title, pages): self.title = title self.pages = pages def __str__(self): return f"Book: {self.title}, {self.pages} pages" b = Book("Python Basics", 300) print(b) # Book: Python Basics, 300 pages ``` Без __str__ print(b) показав би щось на кшталт <__main__.Book object at 0x...> — стандартне представлення об'єкта, яке рідко буває корисним. __len__ контролює те, що Python повертає при виклику len() на об'єкті: ```python class Box: def __init__(self, items_count): self.items_count = items_count def __len__(self): return self.items_count ``` Після визначення __len__ вираз len(Box(5)) повертає 5. Python викликає __len__ автоматично — ви ніколи не викликаєте його напряму. Угода про іменування з подвійними підкресленнями існує, щоб уникнути конфліктів із вашими власними іменами методів. __str__ і __len__ зарезервовані для цієї конкретної мети.

Патерни успадкування і dunder-методів

#
Успадкування __init__ і додавання нової поведінки — дочірній клас додає власні атрибути поверх батьківських: ```python class Playlist: def __init__(self): self.songs = [] def add(self, song): self.songs.append(song) def remove(self, song): self.songs.remove(song) class CustomPlaylist(Playlist): def __init__(self, name): super().__init__() # створює self.songs = [] self.name = name # додає новий атрибут ``` super().__init__() обов'язково викликати, коли __init__ батька налаштовує стан, від якого залежить дочірній клас. Пропуск означає, що self.songs ніколи не буде створено, і кожен виклик add() викине AttributeError. Метод, що використовує random і успадковує всю іншу поведінку: ```python from random import choice class MagicBox(Box): def random_item(self): return choice(self.items) ``` MagicBox отримує все, що має Box. Йому потрібно визначити лише одну нову річ. Клас Message із атрибутом-прапором — прапор встановлюється викликачем, клас надає до нього доступ через метод: ```python class Message: def __init__(self, text): self.text = text self.spam = False def mark_spam(self): self.spam = True ``` Атрибути не обов'язково мають приходити з параметрів. spam починається як False для кожного нового Message — це правильне значення за замовчуванням.

Швидкий довідник: наслідування та dunder-методи

#
**Синтаксис наслідування** ```python class Батько: def __init__(self, x): self.x = x def greet(self): return 'hello' class Дитина(Батько): # Дитина наслідує від Батька def __init__(self, x, y): super().__init__(x) # спочатку __init__ батька self.y = y # потім власні атрибути def greet(self): # перевизначення — замінює версію батька return 'hi' ``` **`super()` — коли і чому** | Ситуація | Що робити | |---|---| | Дочірній клас має власний `__init__` | викликати `super().__init__(...)` першим | | Дочірній клас лише додає нові методи | `__init__` не потрібен — наслідується від батька | | Перевизначення методу але з логікою батька | викликати `super().назва_методу()` | **Dunder-методи** | Метод | Викликається коли… | Приклад повернення | |---|---|---| | `__init__(self, ...)` | `НазваКласу(...)` | — ініціалізує об'єкт | | `__str__(self)` | `print(obj)` або `str(obj)` | зрозумілий рядок | | `__repr__(self)` | `repr(obj)`, налагодження | однозначний рядок | | `__len__(self)` | `len(obj)` | ціле число | | `__eq__(self, other)` | `obj == other` | `bool` | | `__lt__(self, other)` | `obj < other` | `bool` | ```python class Book: def __init__(self, title, pages): self.title = title self.pages = pages def __str__(self): return f'{self.title} ({self.pages} pages)' def __len__(self): return self.pages def __eq__(self, other): return self.title == other.title b = Book('Python', 300) print(b) # Python (300 pages) len(b) # 300 ``` **Що дочірній клас наслідує автоматично** Все що є у батька: всі методи, всі атрибути встановлені в `__init__` батька. Дочірній клас визначає лише те що є *новим* або *відмінним*.
01

Animal і Dog через наслідування.

#

Створіть клас Animal , у створюваних ним об’єктів має бути атрибут name . Значення в атрибут передається під час ініціалізації об’єкта. Тобто — об’єкт описує тварину (будь-яку) і зберігає її кличку. Потім створіть клас Dog , який наслідується від Animal . У класі Dog додайте метод __str__ , який повертає рядок, у якому міститься тип тварини (ахахах, вибачаюсь, "тип тварини" звучить смішно) та її кличка: "Dog: Rex" # приклад рядка, що повертається Сенс такий: Базовий клас Animal зберігає основну "поведінку" об’єктів тварин, а клас Dog деталізує її під конкретний тип тварини.

class Animal:
    pass

class Dog(Animal):
    pass
Рішення
class Animal:
    def __init__(self, name):
        self.name = name


class Dog(Animal):
    def __str__(self):
        return f"Dog: {self.name}"


# Можна деталізувати:
class Dog(Animal):
    def __str__(self):
        return f"Dog: {self.name}"

    def tell(self):
        print("Bark!!!")  # "Гав" англійською. Собакам теж потрібен перекладач..

class Cat(Animal):
    def __str__(self):
        return f"Cat: {self.name}"

    def tell(self):
        print("Cats don't care about human commands!")

# self все одно потрібно прийняти, навіть якщо в методі не використовується.
02

Book із рядковим представленням.

#

Створіть клас Book з атрибутами для об’єктів title і pages . Додайте метод __str__ , який повертає: "Book: Python Basics (120 pages)" Можна додати метод __len__ , який повертатиме кількість сторінок книги.

class Book:
    pass
Рішення
class Book:
    def __init__(self, title, pages):
        self.title = title
        self.pages = pages

    def __len__(self):
        return self.pages

    def __str__(self):
        return f"Book: {self.title} ({self.pages} pages)"
03

User і Admin.

#

Давайте ще раз пройдемо щось схоже на перше: Умова: Створіть клас User з атрибутом name . Створіть клас Admin , який наслідується від User . Перевизначте метод __str__ , щоб вивід був: "Admin: Alex" А у звичайного User виводилось через __str__ просто: "Alex"

class User:
    pass

class Admin(User):
    pass
Рішення
class User:
    def __init__(self, name):
        self.name = name
    def __str__(self):
        return self.name

class Admin(User):
    def __str__(self):
        return f"Admin: {self.name}"
04

Коробка з предметами.

#

Створіть клас Box з атрибутом items_count . Додайте метод __len__ , який повертає кількість предметів. І додайте методи add і remove , які можуть класти та забирати об’єкт із коробки. Це можна зробити просто через лічильник items_count (просто рахувати без складнощів), або зробити список предметів у коробці (рядків) і підлаштувати клас для роботи зі списком (список — атрибут об’єкта).

class Box:
    pass
Рішення
class Box:
    def __init__(self, items_count):
        self.items_count = items_count
    
    def add(self, count=1):
        self.items_count += count
    
    def remove(self, count=1):
        self.items_count -= count

    def __len__(self):
        return self.items_count


# Зі списком складніше і по-іншому:
class Box:
    def __init__(self):
        self.items = []
        self.items_count = len(self.items)
    
    def add(self, item):
        self.items.append(item)
        self.items_count = len(self.items)
    
    def remove(self, item):
        if item in self.items:
            self.items.remove(item)
            self.items_count = len(self.items)
        else:
            print("Кіт із коробки каже: Такого тут немає, до побачення!")

    def __len__(self):
        return self.items_count
05

Message зі spam-прапором.

#

Створіть клас Message з атрибутом text і атрибутом spam. У text текст повідомлення передається під час створення об’єкта, а атрибут spam спочатку просто має значення None. Додайте методи: - is_spam - який перевіряє, чи є в тексті якісь слова як критерій спаму, і повертає True/False - під час виклику методу is_spam , окрім повернення цим методом True/False, у атрибуті self.spam встановлюється відповідне значення. - і додайте __len__ , який повертає довжину тексту.

class Message:
    pass
Рішення
class Message:
    def __init__(self, text):
        self.text = text
        self.spam = None
        self.spam_triggers = ["куплю", "дорого"]  # Так, я про це не говорив,
                                                  # але й не забороняв.

    def is_spam(self):
        if self.spam is not None:  # пояснення нижче, тут я вже "перегнув"
            return self.spam
        
        for trigger in self.spam_triggers:
            if trigger in self.text:
                self.spam = True
                return True
            
        self.spam = False
        return False

    def __len__(self):
        return len(self.text)
    

# Наявність self.spam після першого виклику is_spam дозволить при повторних перевірках
# не запускати механізм перевірки, а просто подивитися в УЖЕ ОБЧИСЛЕНИЙ атрибут (оптимізація).

m = Message("Куплю старые телевизоры дорого!!!")

if m.is_spam():  # виконується реальна перевірка
    ...

if m.is_spam():  # вже буде взято із self.spam
    ...
06

Employee і Manager.

#

Створіть клас Employee (працівник) з атрибутами name і salary . Зробіть метод у Employee , який може змінювати зарплату. Створіть клас Manager , який наслідується від Employee . Перевизначте метод __str__ так, щоб вивід був: Manager John earns 5000 Переконайтеся, що метод зміни зарплати також працює для Manager

class Employee:
    pass

class Manager(Employee):
    pass
Рішення
class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    def change_salary(self, coef): # наприклад коефіцієнт 1.1 - збільшити на 10%
        self.salary = round(self.salary * coef) # без другого аргументу округлить до цілого


class Manager(Employee):
    def __str__(self):
        return f"Manager {self.name} earns {self.salary}"
    

chel = Manager("Bob", 1000)
chel.change_salary(1.1)
print(chel)  # Manager Bob earns 1100
07

Playlist і CustomPlaylist.

#

Створіть клас Playlist з атрибутом songs (список). Додайте методи add , remove - додавання/видалення пісень. Додайте метод reverse , який перевертає список треків. Додайте метод __len__ , який повертає кількість пісень. Додайте метод __bool__ - порожній плейлист чи ні. А потім зробіть клас-нащадок CustomPlaylist , у якому метод add дозволятиме додавання дублікатів пісень у плейлист. (так, перший відповідно не повинен.)

class Playlist:
    pass
Рішення
class Playlist:
    def __init__(self, songs):  # 1 аргумент - список із треками
        self.songs = songs

    def add(self, song):
        if song not in self.songs:
            self.songs.append(song)
    
    def remove(self, song):
        if song in self.songs:
            self.songs.remove(song)

    def reverse(self):
        self.songs.reverse()

    def __len__(self):
        return len(self.songs)
    
    def __bool__(self):
        return bool(self.songs)

# або

# class Playlist:
#     def __init__(self, *songs):  # треки як окремі аргументи
#         self.songs = list(songs) # просто вирішив нагадати про *args

#     # решта така сама.


class CustomPlaylist(Playlist):
    def add(self, song):
        self.songs.append(song)
08

Іменований CustomPlaylist.

#

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

class CustomPlaylist(Playlist):
    def __init__???
        ???
    def add(self, song):
        self.songs.append(song)
Рішення
class CustomPlaylist(Playlist):
    def __init__(self, songs, name):
        super().__init__(songs)
        self.name = name

    def add(self, song):
        self.songs.append(song)

    def __str__(self):
        return f"Playlist: {self.name}"
09

MagicBox із випадковим предметом.

#

Давайте повернемося до класу Box (код нижче є), давайте від нього наслідуємося і зробимо клас MagicBox . Єдина додаткова поведінка — це те, що під час створення об’єкта коробки в ній з’являтиметься випадковий предмет зі списку. Список можливих випадкових предметів визначте де завгодно, це не важливо, хоч у глобальній області. Заготовка:

from random import choice  # функція випадкового вибору з послідовності

class Box:
    def __init__(self):
        self.items = []
        self.items_count = len(self.items)
    
    def add(self, item):
        self.items.append(item)
        self.items_count = len(self.items)
    
    def remove(self, item):
        if item in self.items:
            self.items.remove(item)
            self.items_count = len(self.items)
        else:
            print("Кот из коробки говорит: Такого тут нет, досвидания!")

    def __len__(self):
        return self.items_count


things = ["Шапка", "Зонтик", "Кружка"]


class MagicBox(Box):
    def __init__(self):
        ...
Рішення
from random import choice  # функція випадкового вибору з послідовності

class Box:
    def __init__(self):
        self.items = []
        self.items_count = len(self.items)
    
    def add(self, item):
        self.items.append(item)
        self.items_count = len(self.items)
    
    def remove(self, item):
        if item in self.items:
            self.items.remove(item)
            self.items_count = len(self.items)
        else:
            print("Кот из коробки говорит: Такого тут нет, досвидания!")

    def __len__(self):
        return self.items_count


things = ["Шапка", "Зонтик", "Кружка"]


class MagicBox(Box):
    def __init__(self):
        super().__init__()
        self.items.append(choice(things))
        self.items_count = len(self.items)


# АБО:
class MagicBox(Box):
    def __init__(self):
        super().__init__()
        self.add(choice(things))  # так крутіше

x = MagicBox()

print(x.items)