Классы и объекты
что это и когда они действительно нужны
Что это
Класс — это описание того, как устроен объект: какие у него данные и что он умеет делать. Объект — конкретный экземпляр, созданный по этому описанию.
Для чтения кода достаточно понимать четыре вещи: class, __init__, self
и точку. Всё остальное — надстройки над этим.
Как выглядит в коде
class Cart:
def __init__(self, user_id: int) -> None:
self.user_id = user_id
self.items: list[dict] = []
def add(self, name: str, price: float) -> None:
self.items.append({"name": name, "price": price})
def total(self) -> float:
return sum(item["price"] for item in self.items)
cart = Cart(user_id=7)
cart.add("кофе", 100.0)
cart.add("хлеб", 45.0)
print(cart.total()) # 145.0
print(cart.user_id) # 7Четыре вещи, которые надо знать
class Cart: — описание. Как и def, само по себе ничего не запускает.
__init__ — выполняется в момент создания объекта. Строка
cart = Cart(user_id=7) вызывает именно его. Здесь задаются начальные данные.
self — сам объект. Первый аргумент любого метода, но при вызове его не
передают: cart.add("кофе", 100.0) — два аргумента, хотя в определении их
три. Python подставляет self сам.
Точка — обращение к данным или методам объекта: cart.items,
cart.total(). Со скобками — вызов метода, без скобок — чтение данных.
Что отличает класс от набора функций
Класс хранит состояние между вызовами. cart.items живёт в объекте: добавил
товар — он там и остался. С обычными функциями этот список пришлось бы
передавать туда-сюда вручную.
Отсюда правило чтения: ищи, где меняется состояние. Всё, что начинается с
self. и стоит слева от =, — изменение объекта. Это те же побочные эффекты
из статьи 2.5, только внутри класса.
Dataclass — класс только для данных
from dataclasses import dataclass
@dataclass
class Item:
name: str
price: float
qty: int = 1
item = Item("кофе", 100.0)
print(item) # Item(name='кофе', price=100.0, qty=1)@dataclass сам создаёт __init__ и красивый вывод. Встречается часто, и это
хороший знак: класс без методов, только данные, читается легко.
Наследование
class PremiumCart(Cart):
def total(self) -> float:
return super().total() * 0.9PremiumCart(Cart) значит «то же самое, что Cart, но с изменениями».
super().total() — вызов версии из родителя.
При чтении наследование неудобно тем, что код разъезжается по двум местам:
чтобы понять, что делает PremiumCart, надо открыть и Cart. Поэтому глубже
двух уровней оно почти всегда зло.
Как сказать ИИ
Что хранит этот объект между вызовами и какие методы
это состояние меняют?Этот класс оправдан или то же самое проще сделать
двумя функциями? Ответь прямо.Когда класс унаследован:
Какие методы PremiumCart переопределяет, а какие берёт
у родителя? Список.На что смотреть
Класс без состояния. Если в __init__ ничего не сохраняется, а все методы
могли бы быть обычными функциями, класс здесь лишний. ИИ создаёт такие классы
охотно — они выглядят «правильно».
self. присваивается вне __init__. Поле, которое появляется в середине
другого метода, легко пропустить при чтении: объект внезапно обзаводится
данными, которых раньше не было.
Глубокое наследование. Три уровня и больше — чтобы понять одну строку, придётся открыть четыре файла.
Изменяемое значение в поле класса. То же, что с аргументами по умолчанию
(статья 2.5): список, объявленный на уровне класса, а не в __init__, будет
общим для всех объектов.
Шпаргалка
Четыре вещи:
class·__init__·self· точка.__init__выполняется при создании объекта.selfпри вызове не передают, Python подставляет сам.Класс нужен ради состояния между вызовами; без него хватит функций.
Ищи
self.x = ...— это изменения объекта.Наследование глубже двух уровней почти всегда вредит чтению.