Программирование с ИИ
ENRU
2.7

Классы и объекты

что это и когда они действительно нужны

Что это

Класс — это описание того, как устроен объект: какие у него данные и что он умеет делать. Объект — конкретный экземпляр, созданный по этому описанию.

Для чтения кода достаточно понимать четыре вещи: 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.9

PremiumCart(Cart) значит «то же самое, что Cart, но с изменениями». super().total() — вызов версии из родителя.

При чтении наследование неудобно тем, что код разъезжается по двум местам: чтобы понять, что делает PremiumCart, надо открыть и Cart. Поэтому глубже двух уровней оно почти всегда зло.

Как сказать ИИ

Запрос
Что хранит этот объект между вызовами и какие методы
это состояние меняют?
Запрос
Этот класс оправдан или то же самое проще сделать
двумя функциями? Ответь прямо.

Когда класс унаследован:

Запрос
Какие методы PremiumCart переопределяет, а какие берёт
у родителя? Список.

На что смотреть

Класс без состояния. Если в __init__ ничего не сохраняется, а все методы могли бы быть обычными функциями, класс здесь лишний. ИИ создаёт такие классы охотно — они выглядят «правильно».

self. присваивается вне __init__. Поле, которое появляется в середине другого метода, легко пропустить при чтении: объект внезапно обзаводится данными, которых раньше не было.

Глубокое наследование. Три уровня и больше — чтобы понять одну строку, придётся открыть четыре файла.

Изменяемое значение в поле класса. То же, что с аргументами по умолчанию (статья 2.5): список, объявленный на уровне класса, а не в __init__, будет общим для всех объектов.

Шпаргалка

  • Четыре вещи: class · __init__ · self · точка.

  • __init__ выполняется при создании объекта.

  • self при вызове не передают, Python подставляет сам.

  • Класс нужен ради состояния между вызовами; без него хватит функций.

  • Ищи self.x = ... — это изменения объекта.

  • Наследование глубже двух уровней почти всегда вредит чтению.