Объектно-ориентированное программирование

Класс, __init__, атрибуты экземпляра и методы

Содержание курса

Типичные ошибки: забытый self и атрибут класса вместо атрибута экземпляра

Две ошибки встречаются настолько часто, что стоит разобрать их отдельно — особенно потому что обе дают неочевидные симптомы.

Забытый self в сигнатуре метода

Предположим, в спешке написали метод без первого параметра:

class Counter:
    def __init__(self, name):
        self.name = name
        self.value = 0

    def increment():          # забыли self
        self.value += 1

При попытке вызвать c.increment() Python выдаст:

TypeError: Counter.increment() takes 0 positional arguments but 1 was given

Сообщение кажется странным: мы же вызвали c.increment() без аргументов. Но Python при вызове метода через экземпляр автоматически передаёт сам объект первым аргументом. Метод объявлен без параметров — принять этот аргумент некуда. Отсюда и ошибка.

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

    def increment(self):      # правильно
        self.value += 1

Атрибут класса вместо атрибута экземпляра

Эта ошибка коварнее, потому что код не падает сразу — он просто ведёт себя неожиданно.

Если написать переменную прямо в теле класса, не внутри __init__, она становится атрибутом класса — то есть разделяется между всеми экземплярами:

class Counter:
    value = 0    # атрибут КЛАССА, не экземпляра

    def __init__(self, name):
        self.name = name

    def increment(self, step=1):
        self.value += step

Создадим два счётчика и попробуем проинкрементировать первый:

c1 = Counter('A')
c2 = Counter('B')

print(Counter.value)  # 0  — атрибут класса
print(c1.value)       # 0  — пока читается с класса
print(c2.value)       # 0

c1.increment(3)

print(c1.value)       # 3  — у c1 появился собственный атрибут-тень
print(c2.value)       # 0  — c2 всё ещё читает с класса
print(Counter.value)  # 0  — сам класс не изменился

Что произошло: при self.value += step Python сначала читает self.value. Поскольку у c1 нет своего атрибута value, он находит его на классе и берёт оттуда 0. Затем присваивает результат через self.value = ... — и теперь у c1 появляется собственный атрибут value, который «перекрывает» (shadowing) атрибут класса. Атрибут класса при этом не меняется.

Это называется shadowing: экземплярный атрибут с тем же именем скрывает классовый. Пока такого «теневого» атрибута нет, Python при чтении поднимается по цепочке до класса. Побочный эффект: поведение зависит от того, был ли метод вызван хотя бы раз, и это делает код трудноотлаживаемым.

Если атрибут должен быть индивидуальным для каждого объекта — а value счётчика очевидно должен быть таким — его нужно объявлять в __init__ через self:

class Counter:
    def __init__(self, name):
        self.name = name
        self.value = 0    # атрибут экземпляра — у каждого свой

    def increment(self, step=1):
        self.value += step

Теперь c1.value и c2.value — независимые переменные с самого начала, и никакого shadowing не возникает.

Атрибуты класса сами по себе — не ошибка. Они полезны, например, для констант или счётчика числа созданных экземпляров, который должен быть общим. Но если что-то должно быть своим у каждого объекта, место для этого — __init__ и self.