Kodokon kodokon.com

ООП: классы, __init__, методы и наследование

Структурируй код с помощью хорошо продуманных классов: инициализация, атрибуты экземпляра или класса, наследование и его компромиссы.

10 мин · 3 вопросов

Открыть этот урок в Kodokon

Ты постоянно работаешь с объектами Python: строками, списками, файлами. Написать свой класс - значит собрать состояние (атрибуты) и поведение (методы) за одним связным интерфейсом. Терминологический момент, который часто понимают неправильно: __init__ - это не конструктор. К моменту, когда __init__ получает self, объект уже существует (его создал __new__); задача метода - инициализировать экземпляр. И в self нет ничего волшебного: это первый параметр любого метода экземпляра, который Python передаёт автоматически при вызове.

PYTHON
class Account:
    def __init__(self, owner, balance=0.0):
        self.owner = owner
        self.balance = balance

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("amount must be > 0")
        self.balance += amount
        return self.balance

    def __repr__(self):
        return f"Account({self.owner!r}, {self.balance})"

account = Account("Ada")
account.deposit(120.0)
print(account)
Полноценный класс: __init__, бизнес-метод и __repr__

Ключевое различие на практике: атрибуты класса объявляются в теле класса и общие для всех экземпляров; атрибуты экземпляра создаются на self, обычно в __init__. При чтении Python сначала смотрит на экземпляр, затем обращается к классу, потом к родительским классам. Присваивание self.x = ... всегда создаёт атрибут экземпляра, который перекрывает одноимённый атрибут класса. Классическая ловушка из реальных проектов - изменяемый атрибут класса.

PYTHON
class Basket:
    items = []

    def add(self, item):
        self.items.append(item)

a = Basket()
b = Basket()
a.add("apple")
print(b.items)  # ['apple']: shared list!
Ловушка изменяемого атрибута класса

Наследование выражает отношение «является»: Car является Vehicle. Подкласс наследует методы, может их переопределять, а super() позволяет вызвать родительскую версию - это незаменимо в __init__, чтобы не оборвать инициализацию родителя. Python разрешает вызовы, следуя MRO (Method Resolution Order). Не хватайся за наследование по любому поводу: чтобы переиспользовать код без отношения «является», лучше выбрать композицию (один объект владеет другим). Глубокие иерархии хрупки: каждый родительский класс становится неявным API для всех своих потомков.

PYTHON
class Vehicle:
    def __init__(self, brand):
        self.brand = brand

    def describe(self):
        return self.brand

class Car(Vehicle):
    def __init__(self, brand, doors):
        super().__init__(brand)
        self.doors = doors

    def describe(self):
        base = super().describe()
        return f"{base}, {self.doors} doors"

print(Car("Aster", 5).describe())
Наследование и super(): расширяем, не переписывая

Проверка знаний

Убедись, что запомнил ключевые моменты этого урока.

  1. В чём именно состоит роль __init__?
    • Создать объект в памяти
    • Инициализировать уже созданный экземпляр
    • Объявить атрибуты класса
    • Вызываться при каждом обращении к атрибуту
  2. Класс объявляет items = [] в своём теле. После a.add(1), который выполняет self.items.append(1), что увидит другой экземпляр b?
    • b.items - пустой список
    • b.items содержит 1: список общий
    • Python выбросит AttributeError
  3. В Car.__init__ зачем нужен вызов super().__init__(brand)?
    • Чтобы создать второй экземпляр Vehicle
    • Чтобы выполнить инициализацию, описанную в родительском классе
    • Чтобы скопировать методы Vehicle в Car
    • Чтобы проверить, что Car действительно наследуется от Vehicle