Поведение общих (классовых) атрибутов в Python
@python_quizРазберем этот квиз
Код из квиза:

Что здесь произойдет и почему? Кратко: на экран будет выведен список с одним элементом — ['admin'].
Ниже — подробное объяснение, разбор возможных вариантов и способы исправить (если нужно) такое поведение.
Почему вывод именно такой
- Переменная
tagsопределена на уровне класса (class User: tags = []). Это означает, что она принадлежит самому классу и является общей для всех экземпляров. - При обращении
u1.tagsиu2.tagsPython сначала смотрит в словарь экземпляра, не находит атрибут, и возвращает атрибут класса. В обоих случаях это один и тот же список. - Вызов
u1.tags.append('admin')изменяет этот общий список. Поэтому последующийprint(u2.tags)покажет тот же изменённый список:['admin'].
Анализ вариантов ответов (кратко)
- Пустой список
[]— не случится, потому что append изменил общий список. ['admin']— соответствует описанному поведению: оба экземпляра видят одно и то же изменение.- Ошибка при создании второго объекта — не происходит: создание второго экземпляра обычное и ошибок нет.
['admin', 'admin']— возможен только если append выполнится дважды или если код явно дублирует элемент; в данном фрагменте это не так.
Как исправить (если нужно, чтобы список был независимым у каждого экземпляра)
Обычно ожидаемое поведение — у каждого пользователя свой список тегов. Для этого определяйте атрибут в конструкторе:

Теперь u1.tags и u2.tags — разные списки.
Еще один прием — если вы хотите иметь дефолтные теги у класса, но позволить экземплярам иметь свои копии по умолчанию:

Когда полезно иметь общий атрибут класса
Иногда поведение с общим атрибутом — ожидаемое и полезное: например, общий кэш, счетчик созданных экземпляров или глобальные настройки класса. В этом случае явно делайте атрибут класс-уровня и документируйте его назначение, чтобы не было сюрпризов.
Выводы
- Атрибут, определённый в теле класса (вне методов), является общим для всех экземпляров.
- Мутируемые объекты (списки, словари) при такой схеме разделяются между экземплярами — изменение через один экземпляр видно в других.
- Если нужно, чтобы каждый объект имел отдельный список, инициализируйте атрибут в
__init__(или используйте копирование дефолтного значения).