Атрибуты класса vs атрибуты экземпляра
Все объекты одного класса создаются по одному шаблону — но у каждого своя жизнь. Как одни данные принадлежат конкретному объекту, а другие — всему классу сразу? И почему одна невинная ошибка с общим списком может сломать сразу все объекты? Сегодня разберём это до конца.
🗂️ Два вида атрибутов — в чём разница?
В предыдущих уроках мы создавали атрибуты через self.name = ... внутри __init__. Это один из двух видов атрибутов. В Python их ровно два:
self.имя = ... внутри __init__. У каждого объекта свой — независимо от других объектов.Давайте сразу увидим оба в одном коде:
class Cat:
# Атрибут КЛАССА — объявлен прямо в теле класса
species = "Felis catus" # все кошки — один вид
def __init__(self, name, age):
# Атрибуты ЭКЗЕМПЛЯРА — создаются у каждого объекта свои
self.name = name
self.age = age
luna = Cat("Луна", 3)
mars = Cat("Марс", 5)
print(luna.name) # Луна — атрибут экземпляра
print(mars.name) # Марс — свой у каждого
print(luna.species) # Felis catus — атрибут класса
print(mars.species) # Felis catus — тот же самый атрибут!
# Можно обратиться и напрямую через класс:
print(Cat.species) # Felis catus
💡 Аналогия из жизни: представьте школьный класс. У каждого ученика есть своё имя, свой возраст, свой дневник — это атрибуты экземпляра. Но у всех один и тот же номер класса — «8Б» — и один учитель. Это атрибут класса: одно значение, общее для всех.
🔍 Где это всё физически хранится: __dict__
В Python каждый класс и каждый объект хранят свои атрибуты в специальном встроенном словаре — он называется __dict__. Это не магия, это самый обычный словарь Python, просто Python создаёт его автоматически.
Самый надёжный способ понять, что происходит внутри — просто посмотреть на эти словари напрямую:
class Cat:
species = "Felis catus"
def __init__(self, name, age):
self.name = name
self.age = age
luna = Cat("Луна", 3)
mars = Cat("Марс", 5)
# Смотрим словарь КЛАССА
print(Cat.__dict__)
# {'species': 'Felis catus', '__init__': <function Cat.__init__>, ...}
# Здесь хранятся атрибуты класса и методы
# Смотрим словарь ОБЪЕКТА luna
print(luna.__dict__)
# {'name': 'Луна', 'age': 3}
# Только атрибуты экземпляра! species здесь НЕТ
# Смотрим словарь ОБЪЕКТА mars
print(mars.__dict__)
# {'name': 'Марс', 'age': 5}
# У каждого объекта свой словарь
Заметили? В luna.__dict__ нет species! Он хранится только в Cat.__dict__. Так как же тогда работает luna.species?
🎯 Важно: luna.__dict__ — это срез реальности. Именно там живут атрибуты экземпляра. Cat.__dict__ — там живут атрибуты класса. Когда вы пишете luna.species, Python сначала смотрит в luna.__dict__, не находит — и идёт искать в Cat.__dict__.
🔎 Порядок поиска: кто побеждает?
Когда вы пишете объект.атрибут, Python ищет его строго по порядку:
instance.__dict__Class.__dict__Что происходит, если у объекта и у класса есть атрибут с одним и тем же именем? Побеждает объект — потому что Python нашёл его первым и дальше не пошёл:
class Cat:
sound = "Мяу" # атрибут класса
def __init__(self, name):
self.name = name
luna = Cat("Луна")
mars = Cat("Марс")
print(luna.sound) # Мяу — берёт из класса (у luna нет своего sound)
print(mars.sound) # Мяу — тоже из класса
# Добавляем атрибут ЭКЗЕМПЛЯРА с тем же именем
luna.sound = "Пррр" # теперь у luna есть СВОЯ версия
print(luna.sound) # Пррр — берёт из ЭКЗЕМПЛЯРА (нашёл первым!)
print(mars.sound) # Мяу — у mars нет своего, берёт из класса
print(Cat.sound) # Мяу — класс не изменился!
# Посмотрим в словари:
print(luna.__dict__) # {'name': 'Луна', 'sound': 'Пррр'} — появился!
print(mars.__dict__) # {'name': 'Марс'} — нет sound
Это очень важный момент: когда вы делаете luna.sound = "Пррр", вы не меняете атрибут класса — вы создаёте новый атрибут экземпляра, который перекрывает классовый. Класс при этом не трогается!
💡 Правило: атрибут экземпляра всегда затеняет одноимённый атрибут класса — для этого конкретного объекта. Остальные объекты продолжают видеть классовый атрибут как ни в чём не бывало.
⚠️ ГЛАВНАЯ ЛОВУШКА: изменяемые атрибуты класса
Сейчас будет самый важный момент урока. Из-за этой ошибки у программистов ломались проекты, и они часами не могли понять почему. Запомните это раз и навсегда.
Никогда не используйте изменяемые объекты (списки, словари, множества) как атрибуты класса, если вы хотите, чтобы у каждого объекта был свой список.
Давайте разберём на конкретном примере. Представьте: вы пишете систему для школы, и у каждого ученика должен быть свой список оценок.
Вот как делать НЕЛЬЗЯ:
# ❌ ОПАСНЫЙ КОД — список как атрибут КЛАССА
class Student:
grades = [] # Кажется, что у каждого будет свой список... но нет!
def __init__(self, name):
self.name = name
def add_grade(self, grade):
self.grades.append(grade) # Вот тут и зарыта проблема
alice = Student("Алиса")
bob = Student("Боб")
alice.add_grade(5)
alice.add_grade(4)
bob.add_grade(3)
print(alice.grades) # [5, 4, 3] — СТОП! Откуда у Алисы тройка?!
print(bob.grades) # [5, 4, 3] — Боб тоже видит все оценки!
print(Student.grades)# [5, 4, 3] — Это ОДИН список для всех!
🤔 Почему это происходит? Когда Python ищет self.grades для вызова .append(), он не находит grades в alice.__dict__ и идёт в Student.__dict__. Там находит список — и вызывает append прямо на нём. Список один — оба объекта пишут в один и тот же список. Вы не создаёте новый атрибут экземпляра — вы мутируете единственный список класса!
Сравните с числами и строками: когда вы делаете luna.sound = "Пррр" — вы присваиваете новое значение, и Python создаёт новый атрибут экземпляра. Но когда вы вызываете self.grades.append() — вы не присваиваете, вы мутируете уже найденный объект в классе. Разница принципиальная!
Вот правильное решение — список создаётся в __init__:
# ✅ ПРАВИЛЬНЫЙ КОД — список как атрибут ЭКЗЕМПЛЯРА
class Student:
def __init__(self, name):
self.name = name
self.grades = [] # Каждый раз при создании объекта — НОВЫЙ список!
def add_grade(self, grade):
self.grades.append(grade)
alice = Student("Алиса")
bob = Student("Боб")
alice.add_grade(5)
alice.add_grade(4)
bob.add_grade(3)
print(alice.grades) # [5, 4] ✅ Только оценки Алисы
print(bob.grades) # [3] ✅ Только оценки Боба
# Убедимся что это разные списки:
print(alice.grades is bob.grades) # False — разные объекты!
Теперь при каждом вызове Student("имя") внутри __init__ выполняется self.grades = [] — создаётся новый пустой список и сразу записывается в словарь этого конкретного экземпляра. У каждого объекта свой список.
✅ Когда атрибуты класса действительно полезны
Атрибуты класса — не зло. Просто нужно знать, где они уместны. Вот три типичных случая:
🔢 Демо: счётчик созданных объектов
Один из самых классических и полезных примеров атрибута класса — счётчик. Нам нужно знать, сколько объектов было создано. Это знание принадлежит не одному объекту, а всему классу.
Идея простая: при каждом создании нового объекта (то есть в __init__) мы увеличиваем счётчик класса на 1:
class Robot:
count = 0 # атрибут класса — счётчик
def __init__(self, name):
self.name = name
Robot.count += 1 # увеличиваем КЛАССОВЫЙ счётчик
# Важно: пишем Robot.count, а не self.count!
# self.count += 1 создало бы атрибут экземпляра
def introduce(self):
print(f"Я робот {self.name}. Всего роботов создано: {Robot.count}")
r1 = Robot("Р2-Д2")
r2 = Robot("С-3ПО")
r3 = Robot("ВАЛ-И")
r1.introduce() # Я робот Р2-Д2. Всего роботов создано: 3
r2.introduce() # Я робот С-3ПО. Всего роботов создано: 3
r3.introduce() # Я робот ВАЛ-И. Всего роботов создано: 3
print(Robot.count) # 3
Обратите внимание: мы пишем Robot.count += 1, а не self.count += 1. Почему? Потому что self.count += 1 — это присваивание нового значения, и Python создал бы атрибут экземпляра count, который перекрыл бы классовый. Счётчик класса остался бы нулём!
⚠️ Ловушка №2: self.count += 1 это сокращение от self.count = self.count + 1. Python сначала читает self.count (находит в классе — значение 0), прибавляет 1, и записывает результат через присваивание — то есть создаёт count в словаре экземпляра. Классовый счётчик не меняется. У каждого объекта будет свой count = 1, а Robot.count навсегда останется 0.
Проверим эту ловушку в деле, чтобы убедиться:
# Демо ловушки с self.count
class RobotBroken:
count = 0
def __init__(self, name):
self.name = name
self.count += 1 # ❌ Неправильно!
r1 = RobotBroken("А")
r2 = RobotBroken("Б")
r3 = RobotBroken("В")
print(r1.count) # 1 — атрибут экземпляра
print(r2.count) # 1 — тоже у каждого своя единица
print(RobotBroken.count) # 0 — классовый счётчик НЕ изменился!
📊 Сравнительная таблица: быстрая шпаргалка
| Критерий | Атрибут класса | Атрибут экземпляра |
|---|---|---|
| Где объявляется | В теле класса, вне методов | В __init__ через self.x = ... |
| Где хранится | Class.__dict__ |
instance.__dict__ |
| Доступ | Class.attr или obj.attr |
Только obj.attr |
| Уникальность | Один для всех объектов | Своё у каждого объекта |
| Типичные задачи | Константы, счётчики, настройки | Имя, возраст, данные объекта |
| Изменяемые типы | ⚠️ Опасно! | ✅ Безопасно |
🎯 Что важно запомнить из этого урока
- Атрибут класса — объявлен в теле класса, общий для всех. Хранится в
Class.__dict__. - Атрибут экземпляра — создаётся в
__init__черезself.имя = .... Свой у каждого объекта. Хранится вinstance.__dict__. - Порядок поиска: Python ищет сначала в объекте, потом в классе. Атрибут экземпляра затеняет атрибут класса с тем же именем.
- ⚠️ Ловушка: списки и словари как атрибуты класса — один объект для всех. Всегда создавайте изменяемые структуры в
__init__. - Для счётчика объектов обновляйте атрибут класса явно:
ClassName.count += 1, а неself.count += 1.
🚀 В следующем уроке мы познакомимся со специальными методами — __str__, __repr__ и другими «dunder»-методами, которые позволяют объектам вести себя как встроенные типы Python.