Нашли ошибку?
Скачать чистый
урок 1 из 4≈ 20 минупражнения: 0 из 3

Абстрактные классы

Абстрактный класс — наследник ABC с методами @abstractmethod: создать его нельзя, а подкласс обязан эти методы реализовать. TypeError при создании и шаблонный метод.

Python запустится при первом выполнении

В проекте про скидки базовый класс Discount был «заглушкой»: его метод amount возвращал 0, а подклассы его переопределяли. Но ничто не мешало создать сам Discount или подкласс, где amount забыли, — касса просто посчитала бы скидку 0, и ошибку никто бы не заметил. Нужен способ сказать: «этот класс — только основа для других, а такой-то метод подкласс обязан написать».

ABC и @abstractmethod

Такой класс называется абстрактным (abstract base class, ABC). В Python его объявляют наследником класса ABC из модуля abc стандартной библиотеки, а обязательные методы помечают декоратором @abstractmethod; такой метод называют абстрактным методом (abstract method). Тело абстрактного метода обычно пустое — ...: реализацию дадут подклассы. Декоратор работает только в наследнике ABC: в обычном классе @abstractmethod молча ничего не запрещает.

[ ]Абстрактный способ доставки
from abc import ABC, abstractmethod


class Delivery(ABC):
    def __init__(self, title):
        self.title = title

    @abstractmethod
    def cost(self, km):
        ...  # реализацию дадут подклассы

    def describe(self, km):  # обычный метод — пользуется абстрактным
        return f"{self.title}: {self.cost(km)} ₽"


print(Delivery.__name__, issubclass(Delivery, ABC))
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
Delivery True

Создать экземпляр абстрактного класса нельзя — TypeError сразу при создании, а не где-то потом:

[ ]Абстрактный класс не создаётсяошибка: TypeError
Delivery("какая-то доставка")
Ошибкасохранённый · нажмите «Выполнить», чтобы получить свой
TypeError: Can't instantiate abstract class Delivery without an implementation for abstract method 'cost'

То же с подклассом, который реализовал не все абстрактные методы. А подкласс, где все они есть, — обычный класс:

[ ]Подкласс без метода и с методом
class Pickup(Delivery):
    def cost(self, km):
        return 0


class BrokenCourier(Delivery):
    def cots(self, km):  # опечатка: cost так и не реализован
        return 150 + 30 * km


print(Pickup("самовывоз").describe(5))
try:
    BrokenCourier("курьер")
except TypeError as error:
    print("TypeError:", error)
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
самовывоз: 0 ₽
TypeError: Can't instantiate abstract class BrokenCourier without an implementation for abstract method 'cost'
[ ]Упражнение 1Абстрактный напиток решено

Объявите абстрактный класс Beverage: __init__(self, name) записывает название в name, абстрактный метод ingredients() должен вернуть список ингредиентов, а обычный метод card() возвращает строку вида "Латте: эспрессо, молоко" — название и ингредиенты через запятую.

from abc import ABC, abstractmethod


class Beverage:  # сделайте класс абстрактным
    def __init__(self, name):
        ...

    def ingredients(self):  # сделайте метод абстрактным
        ...

    def card(self):
        ...

[ ]Упражнение 2Эспрессо и латте решено

Объявите два подкласса Beverage: Espresso — ингредиенты ["эспрессо"], Latte — ["эспрессо", "молоко"]. Название передаётся при создании: Latte("Латте").

class Espresso(Beverage):
    def ingredients(self):
        ...


class Latte(Beverage):
    def ingredients(self):
        ...

Шаблонный метод

В card() вы написали обычный метод абстрактного класса, который вызывает абстрактный. Это частый приём — шаблонный метод (template method): базовый класс задаёт порядок действий целиком, а подклассы заполняют отдельные шаги. Порядок не повторяется в каждом подклассе, и по договорённости подклассы шаблонный метод не переопределяют — хотя Python этого не запрещает.

[ ]Отчёт: шаблон в базовом классе
class Report(ABC):
    def render(self):  # шаблон: порядок задан здесь
        lines = [self.title(), "-" * len(self.title())]
        lines += self.lines()
        return "\n".join(lines)

    @abstractmethod
    def title(self):
        ...

    @abstractmethod
    def lines(self):
        ...


class StockReport(Report):
    def __init__(self, stock):
        self.stock = stock

    def title(self):
        return "Остатки"

    def lines(self):
        return [f"{name} — {left} шт." for name, left in self.stock.items()]


print(StockReport({"зёрна": 12, "молоко": 4}).render())
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
Остатки
-------
зёрна — 12 шт.
молоко — 4 шт.
[ ]Упражнение 3Отчёт о продажах решено

Объявите подкласс SalesReport класса Report из демонстрации. SalesReport({"Латте": 41, "Раф": 17}) хранит словарь продаж в атрибуте sales. Заголовок отчёта — "Продажи за день", строки — "Латте: 41", "Раф: 17" и так далее в порядке словаря. Метод render не переопределяйте.

class SalesReport(Report):
    def __init__(self, sales):
        ...

    def title(self):
        ...

    def lines(self):
        ...

Вопрос

У абстрактного класса Report два абстрактных метода: title и lines. Подкласс реализовал только lines. Когда появится ошибка?

Абстрактный класс — это договор: «каждый способ доставки умеет cost», «каждый отчёт умеет title и lines». Код, который пользуется такими объектами, может на этот договор полагаться.

Нашли ошибку?