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.

#

Давайте похожее на 1е прогоним еще раз: Условие: Создайте класс 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 со спам-флагом.

#

Создайте класс 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)