Наследование — не единственный способ построить класс из готовых частей. Корзина из модуля 2 не была подклассом списка или товара — она содержала список товаров. Это второй способ, композиция (composition): объект хранит другие объекты в атрибутах и пользуется ими. Как выбрать между ними?
«Является» или «содержит»
Простой тест — сказать фразу вслух:
- латте является напитком, сезонный товар является товаром → наследование:
class Latte(Drink); - заказ содержит корзину, кофемашина содержит бойлер → композиция:
self.cart = cart.
Если фраза «X является Y» звучит странно (заказ — это корзина? касса — это список?), наследование не подходит.
Как связать классы Order (заказ) и Customer (покупатель)?
Делегирование
Объект, который содержит другой объект, часто передаёт ему работу. Кофемашина не греет воду сама — она просит об этом свой бойлер. Это называется делегирование (delegation). Кофемашина при этом сама решает, какие возможности бойлера показать снаружи:
class Boiler:
def __init__(self):
self.temperature = 20
def heat(self, target):
self.temperature = target
return f"вода нагрета до {target} °C"
class CoffeeMachine:
def __init__(self):
self.boiler = Boiler() # кофемашина содержит бойлер
def brew(self, drink):
step = self.boiler.heat(93) # делегирует нагрев бойлеру
return f"{step}; {drink} готов"
machine = CoffeeMachine()
print(machine.brew("американо"))
print(machine.boiler.temperature)вода нагрета до 93 °C; американо готов 93
Для упражнений понадобятся знакомые товар и корзина:
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
def __repr__(self):
return f"Product({self.name!r}, {self.price})"
class Cart:
def __init__(self):
self.items = []
def add(self, product):
self.items.append(product)
@property
def subtotal(self):
return sum(p.price for p in self.items)
cart = Cart()
cart.add(Product("Латте", 220))
print(cart.items, cart.subtotal)[Product('Латте', 220)] 220Объявите класс Order: Order("Анна", cart) хранит имя покупателя в customer_name (для простоты — строкой) и корзину в cart. Метод
add(product) кладёт товар в корзину заказа, а свойство total возвращает сумму корзины (subtotal).
Не копируйте код корзины — делегируйте ей.
self.cart.total — свойство, которое возвращает self.cart.subtotal.Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
class Order:
def __init__(self, customer_name, cart):
...
def add(self, product):
...
@property
def total(self):
...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
class Order:
def __init__(self, customer_name, cart):
self.customer_name = customer_name
self.cart = cart
def add(self, product):
self.cart.add(product)
@property
def total(self):
return self.cart.subtotalЛовушка: class Menu(list)
Меню — это список товаров, так что хочется написать class Menu(list). Но подкласс получает все методы
списка — и вместе с ними способы сломать меню:
class ListMenu(list): # ошибка: меню — не список
pass
menu = ListMenu()
menu.append(Product("Латте", 220))
menu.append("кирпич") # и это тоже можно
print(len(menu), type(menu + []).__name__)
menu.clear()
print(menu)2 list []
Объявите класс Menu без наследования от list. Товары хранятся во внутреннем списке _items. Метод
add(product) добавляет товар, а если это не экземпляр Product — выбрасывает TypeError. Метод
names() возвращает список названий, метод find(name) — товар с таким названием или None.
add: if not isinstance(product, Product): raise TypeError("в меню можно добавить только товар").Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
class Menu:
def __init__(self):
...
def add(self, product):
...
def names(self):
...
def find(self, name):
...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
class Menu:
def __init__(self):
self._items = []
def add(self, product):
if not isinstance(product, Product):
raise TypeError("в меню можно добавить только товар")
self._items.append(product)
def names(self):
return [p.name for p in self._items]
def find(self, name):
for p in self._items:
if p.name == name:
return p
return NoneЧасти можно менять на ходу
У композиции есть ещё одно преимущество: часть объекта можно заменить другой, пока программа работает. Класс через наследование выбирается раз и навсегда, а атрибут можно переприсвоить. Покупатель передумал и вместо курьера выбрал самовывоз — меняется только атрибут заказа. Способы доставки — классы из модуля 5:
class Pickup:
def cost(self, km):
return 0
class Courier:
def cost(self, km):
return 150 + 30 * km
print(Pickup().cost(5), Courier().cost(5))0 300
Объявите класс DeliveryOrder: DeliveryOrder(cart, delivery) хранит корзину в cart и способ доставки
в delivery — любой объект с методом cost(km) (например, Courier() или Pickup() из демонстрации).
Метод total(km) возвращает сумму корзины плюс стоимость доставки. Затем создайте order — заказ
с корзиной cart из заготовки и курьером, сохраните в by_courier его total(5), поменяйте у этого же заказа
доставку на самовывоз и сохраните total(5) в by_pickup.
total складывает две вещи, которые знают вложенные объекты: сумму корзины и стоимость доставки.order.delivery = Pickup().Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
class DeliveryOrder:
def __init__(self, cart, delivery):
...
def total(self, km):
...
cart = Cart()
cart.add(Product("Латте", 220))
order = ...
by_courier = ...
by_pickup = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
class DeliveryOrder:
def __init__(self, cart, delivery):
self.cart = cart
self.delivery = delivery
def total(self, km):
return self.cart.subtotal + self.delivery.cost(km)
cart = Cart()
cart.add(Product("Латте", 220))
order = DeliveryOrder(cart, Courier())
by_courier = order.total(5)
order.delivery = Pickup()
by_pickup = order.total(5)Правило, которое часто повторяют: предпочитайте композицию наследованию. Наследование — для настоящего «является», когда подкласс годится везде, где ждут базовый класс. Во всех остальных случаях объект лучше собрать из частей.