Класс, __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.
