Практическая ситуация: два экземпляра класса неожиданно видят изменения одного изменяемого атрибута; какой ...

Практическая ситуация: два экземпляра класса неожиданно видят изменения одного изменяемого атрибута; какой механизм Python это объясняет?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

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

Исторический контекст

Модель атрибутов Python разделяет данные класса и данные экземпляра. Атрибуты класса предназначены для общих настроек, констант и методов, а экземплярные атрибуты — для состояния конкретного объекта.

Такой подход позволяет не дублировать общие значения в каждом экземпляре. Обратная сторона — изменяемый объект класса может стать непреднамеренным общим состоянием.

Постановка проблемы

При обращении к атрибуту экземпляра Python сначала ищет подходящее значение в самом экземпляре, затем в его классе и далее в базовых классах. Если в экземпляре собственного атрибута нет, используется найденный объект класса.

Операция изменения объекта, например добавление элемента в общий список, не создаёт атрибут экземпляра. В результате состояние одного объекта влияет на остальные, что особенно опасно в обработчиках запросов, кэшах и накопителях данных.

Подробное решение

Ключевое различие — между изменением объекта и присваиванием имени. Если экземпляр получил список из класса, вызов операции изменения списка меняет сам общий список. Если же выполнить присваивание атрибуту экземпляра, Python создаст в экземпляре отдельное имя, скрывающее одноимённый атрибут класса.

class Basket: items = [] first = Basket() second = Basket() first.items.append('яблоко') print(second.items) # ['яблоко'] first.items = ['груша'] print(first.items) # ['груша'] print(second.items) # ['яблоко']

В первом случае оба обращения находят один список в Basket.__dict__. После присваивания first.items появляется отдельный атрибут в first.__dict__; second.items по-прежнему разрешается через класс.

Безопасный вариант — создавать изменяемое значение отдельно для каждого экземпляра, обычно в __init__. Для неизменяемых значений атрибут класса часто безопасен, поскольку операция вроде присваивания числа экземпляру создаёт отдельное экземплярное имя.

Следует учитывать наследование: если подкласс не переопределил атрибут, он также использует объект из базового класса. Изменение этого объекта будет видно экземплярам обоих классов. При проектировании API нужно явно решить, является ли состояние общим или принадлежащим экземпляру.

Ситуация из практики

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

Можно было копировать словарь перед каждым использованием. Это уменьшает риск случайного изменения общего состояния, но требует дисциплины и не решает проблему, если сам экземпляр должен владеть изменениями.

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

Что кандидаты часто упускают

  1. Дополнительный вопрос: Что произойдёт, если изменяемый атрибут класса переопределить присваиванием через экземпляр?

Ответ: Присваивание создаст атрибут в словаре конкретного экземпляра и не изменит атрибут класса. После этого данный экземпляр будет находить собственное значение первым, а остальные экземпляры продолжат использовать значение класса.

  1. Дополнительный вопрос: Почему неизменяемый атрибут класса обычно не создаёт той же проблемы?

Ответ: Изменяемая операция над объектом класса может затронуть его непосредственно. Для неизменяемого объекта операция изменения невозможна: новое значение создаётся заново, а присваивание имени экземпляру обычно формирует собственный атрибут. Однако вложенный изменяемый объект всё равно может разделяться, например список внутри кортежа.

  1. Дополнительный вопрос: Как корректно задать независимое изменяемое состояние при наследовании?

Ответ: Нужно создавать его на уровне экземпляра, например в __init__, а не полагаться на атрибут класса. Если состояние должно отличаться для каждого подкласса, его можно переопределить отдельным объектом в каждом классе, но это всё равно будет общим состоянием внутри соответствующего класса, а не каждого экземпляра.