В проекте про скидки базовый класс 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 сразу при создании, а не где-то потом:
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'
Объявите абстрактный класс Beverage: __init__(self, name) записывает название в name, абстрактный метод
ingredients() должен вернуть список ингредиентов, а обычный метод card() возвращает строку вида
"Латте: эспрессо, молоко" — название и ингредиенты через запятую.
class Beverage(ABC):, над методом ingredients — @abstractmethod, тело — ....card вызывает self.ingredients() — подкласс подставит свой список, а соединить его поможет ", ".join(...).Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
from abc import ABC, abstractmethod
class Beverage: # сделайте класс абстрактным
def __init__(self, name):
...
def ingredients(self): # сделайте метод абстрактным
...
def card(self):
...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
from abc import ABC, abstractmethod
class Beverage(ABC):
def __init__(self, name):
self.name = name
@abstractmethod
def ingredients(self):
...
def card(self):
return f"{self.name}: {', '.join(self.ingredients())}"Объявите два подкласса Beverage: Espresso — ингредиенты ["эспрессо"], Latte — ["эспрессо", "молоко"].
Название передаётся при создании: Latte("Латте").
ingredients; __init__ и card наследуются.Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
class Espresso(Beverage):
def ingredients(self):
...
class Latte(Beverage):
def ingredients(self):
...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
class Espresso(Beverage):
def ingredients(self):
return ["эспрессо"]
class Latte(Beverage):
def ingredients(self):
return ["эспрессо", "молоко"]Шаблонный метод
В 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 шт.
Объявите подкласс SalesReport класса Report из демонстрации. SalesReport({"Латте": 41, "Раф": 17})
хранит словарь продаж в атрибуте sales. Заголовок отчёта — "Продажи за день", строки — "Латте: 41",
"Раф: 17" и так далее в порядке словаря. Метод render не переопределяйте.
__init__ не обязан вызывать super().__init__, если у базового класса его нет.lines() возвращает список строк — по одной на пару «название, количество» из словаря self.sales.Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
class SalesReport(Report):
def __init__(self, sales):
...
def title(self):
...
def lines(self):
...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
class SalesReport(Report):
def __init__(self, sales):
self.sales = sales
def title(self):
return "Продажи за день"
def lines(self):
return [f"{name}: {count}" for name, count in self.sales.items()]У абстрактного класса Report два абстрактных метода: title и lines. Подкласс реализовал только
lines. Когда появится ошибка?
TypeError.Абстрактный класс — это договор: «каждый способ доставки умеет cost», «каждый отчёт умеет title и
lines». Код, который пользуется такими объектами, может на этот договор полагаться.