Урок курса
Класс, __init__, атрибуты экземпляра и методы
Python для продвинутых: ООП, типизация и тестированиеВ предыдущем уроке мы разобрали *args и **kwargs — гибкие сигнатуры функций, которые принимают произвольное количество аргументов. Этот же механизм скоро пригодится в конструкторах классов, но сначала нужно понять, что такое класс и как он устроен изнутри.
Определение класса и роль init
Класс в Python объявляется ключевым словом class, после которого идёт имя и двоеточие. По соглашению имена классов пишут в CamelCase — Counter, UserProfile, OrderItem. Тело класса — это обычный блок с отступом, внутри которого определяются методы.
class Counter:
pass
Пока это просто пустой класс. Он уже работает: Python создал новый тип Counter. Но чтобы объект при создании получал начальное состояние, нужен специальный метод __init__.
__init__ — это инициализатор экземпляра. Python вызывает его автоматически сразу после того, как создан новый объект. Важно: __init__ не выделяет память и не «конструирует» объект в низкоуровневом смысле — этим занимается __new__. Задача __init__ проще: получить только что созданный пустой объект и заполнить его данными.
Первый параметр __init__ — это self. Он ссылается на тот самый объект, который был только что создан. Именно через self метод знает, с каким объектом работает.
class Counter:
def __init__(self, name):
print(f"Создаётся объект Counter с именем {name}")
print(f"self — это объект по адресу: {id(self)}")
Когда Python выполняет Counter('A'), он:
- Создаёт новый объект типа
Counter. - Передаёт этот объект в
__init__какself. - Передаёт
'A'какname. - Выполняет тело
__init__.
Имя self — соглашение, а не синтаксическое требование. Python не проверяет, как называется первый параметр. Но отступать от этого соглашения не стоит: любой читающий код ожидает именно self.
Сам вызов Counter('A') выглядит как вызов функции, но это вызов класса как фабрики объектов. Python знает, что нужно создать экземпляр Counter и запустить __init__ с переданными аргументами. Аргумент для self не указывают явно — интерпретатор передаёт его сам.
class Counter:
def __init__(self, name):
print(f"__init__ вызван для объекта {id(self)}, name={name}")
c = Counter('A')
print(type(c)) # <class '__main__.Counter'>
После выполнения этого кода c — полноценный объект типа Counter. __init__ уже отработал. На следующем шаге мы запишем в self первые данные и превратим инициализатор в нечто полезное.
Атрибуты экземпляра и методы экземпляра
Когда __init__ запущен, у нас есть self — свежий пустой объект. Чтобы объект стал полезным, нужно записать в него данные. Делается это простым присваиванием через точку:
class Counter:
def __init__(self, name):
self.name = name
self.value = 0
self.name и self.value — это атрибуты экземпляра. После того как __init__ отработал, они живут внутри объекта и доступны везде, где есть ссылка на этот объект. Прочитать их можно точно так же: объект.атрибут.
Важный момент: self.name и просто name — разные вещи. name — это локальный параметр __init__, он исчезнет, когда метод завершится. self.name — это данные, привязанные к объекту, они останутся.
c = Counter('A')
print(c.name) # 'A'
print(c.value) # 0
Теперь про методы. Метод экземпляра — это обычная функция внутри класса, первым параметром которой всегда идёт self. Через self метод получает доступ к атрибутам именно того объекта, на котором он вызван:
class Counter:
def __init__(self, name):
self.name = name
self.value = 0
def increment(self, step=1):
self.value += step
def reset(self):
self.value = 0
def status(self):
return f"{self.name}: {self.value}"
Когда пишем c.increment(3), Python внутри делает вот что: берёт объект c, передаёт его как self в метод increment, передаёт 3 как step. Метод читает self.value, прибавляет 3 и записывает результат обратно — всё это происходит с атрибутом именно объекта c.
c = Counter('A')
c.increment(3)
print(c.status()) # A: 3
c.reset()
print(c.status()) # A: 0
Методы могут не только изменять атрибуты, но и читать их и возвращать результат — как status() выше. Ключевой принцип один: всё взаимодействие с данными объекта идёт через self. Нет self — нет доступа к состоянию конкретного экземпляра.
Инстанцирование и независимое состояние объектов
Каждый вызов класса как функции создаёт отдельный, полностью независимый объект. Напишем c1 = Counter('A') и c2 = Counter('B') — Python дважды выполнит __init__, оба раза передавая в self разный, только что созданный объект. В итоге c1 и c2 не знают друг о друге и никак не связаны.
class Counter:
def __init__(self, name):
self.name = name
self.value = 0
def increment(self, step=1):
self.value += step
def reset(self):
self.value = 0
def status(self):
return f"{self.name}: {self.value}"
c1 = Counter('A')
c2 = Counter('B')
c1.increment(3)
c1.increment()
print(c1.status()) # A: 4
print(c2.status()) # B: 0
c1.increment(3) читает c1.value, изменяет его и записывает результат обратно в c1.value. c2.value при этом остаётся нулём — у него своя копия атрибута, лежащая в совершенно другом месте памяти.
Чтобы убедиться в этом напрямую, достаточно проверить id объектов:
print(id(c1), id(c2)) # разные адреса
print(c1 is c2) # False
Оба объекта — типа Counter, оба имеют атрибуты name и value, оба понимают методы increment, reset, status. Но данные у них свои. Когда Python выполняет c2.increment(), он передаёт c2 как self — и метод работает только с c2.value.
Именно здесь проявляется разница между классом и объектом. Класс Counter — один. Он описывает структуру и поведение. Объектов Counter может быть сколько угодно, и каждый несёт своё независимое состояние.
Можно создать произвольное количество счётчиков и управлять ими независимо:
counters = [Counter(letter) for letter in 'ABC']
counters[0].increment(10)
counters[1].increment(5)
# counters[2] остаётся на 0
for c in counters:
print(c.status())
# A: 10
# B: 5
# C: 0
Каждый счётчик в списке — самостоятельный объект. Обращение к любому из них через self внутри метода гарантирует, что изменения затронут только этот конкретный экземпляр.
Типичные ошибки: забытый 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.
Попробуйте решить
Дан класс:
class Box:
def __init__(self, label, capacity):
self.label = label
self.capacity = capacity
self.items = []
def add(self, item):
self.items.append(item)
Разработчик создаёт два объекта:
b1 = Box('A', 10)
b2 = Box('B', 5)
b1.add('apple')
Чему равно b2.items после выполнения этого кода?
Продолжить с проверкой и прогрессом
Откройте интерактивный раннер с заданиями урока.
