2025, Sep 28 23:15
Динамический доступ к атрибутам в Python: setattr, getattr и __dict__
Разбираем, почему instance.tag не работает в цикле, и как корректно читать динамические атрибуты в Python через getattr и __dict__. Примеры кода и советы.
Динамический доступ к атрибутам в Python кажется простым, пока не пытаешься прочитать эти атрибуты внутри цикла. Запись obj.i в теле цикла не извлечёт значения для a, b или c — в этом и загвоздка. Задача — пройтись по данным, создать атрибуты на их основе и в том же проходе корректно их прочитать.
Постановка задачи
Рассмотрим минимальный шаблон, где атрибуты добавляются во время выполнения, а затем читаются:
instance = DemoType()
labels = ['a', 'b', 'c']
for tag in labels:
  attr_id = f"{tag}"
  setattr(instance, attr_id, [f"{tag}"])
  print(instance.tag)
Ожидается, что вы увидите значения для instance.a, instance.b и instance.c — каждое в виде списка из одного элемента. Но последняя строка буквально обращается к instance.tag, а такого атрибута мы не создавали.
Что происходит на самом деле
Цикл действительно создаёт атрибуты, имена которых формируются из текущего значения итерации. Однако обращение instance.tag не связано с переменной tag — интерпретатор ищет атрибут с именем ровно "tag". Поэтому попытка прочитать только что созданные атрибуты для "a", "b" и "c" не срабатывает. Чтобы получать атрибуты по вычисляемому имени, нужно делать поиск по имени.
Решение
Атрибуты обычного объекта Python доступны как словарь через instance.__dict__. Если проиндексировать это отображение именем, вычисленным в цикле, вы получите только что записанное значение. Так можно и вывести его, и собрать список для проверки.
instance = DemoType()
labels = ['a', 'b', 'c']
for tag in labels:
  attr_id = f"{tag}"
  setattr(instance, attr_id, [f"{tag}"])
  print(instance.__dict__[attr_id])
Если удобнее получать значение через встроенную функцию, симметричную setattr, используйте getattr с тем же вычисленным именем:
instance = DemoType()
labels = ['a', 'b', 'c']
for tag in labels:
  attr_id = f"{tag}"
  setattr(instance, attr_id, [f"{tag}"])
  print(getattr(instance, attr_id))
Чтобы собрать значения в список для проверки, пройдитесь по тем же ключам и считайте их по имени:
instance = DemoType()
labels = ['a', 'b', 'c']
for tag in labels:
  attr_id = f"{tag}"
  setattr(instance, attr_id, [f"{tag}"])
values_snapshot = [instance.__dict__[name] for name in labels]
print(values_snapshot)
Почему это важно
При динамическом создании атрибутов прямой доступ через точку со статичным идентификатором не учитывает переменную, вычисленную в цикле. Доступ по имени сохраняет согласованность: записали по имени — читайте по имени. Во многих случаях удобнее завести внутри класса отдельное «хранилище свойств» на базе dict, а иногда и вовсе использовать обычный словарь вместо атрибутов — это проще и нагляднее.
Итоги
Если атрибуты создаются через setattr, считывайте их по вычисленному имени. Доступ через instance.__dict__[name] или getattr(instance, name) симметричен записи и предотвращает неожиданные эффекты. Если по сути вам нужен key-value, присмотритесь к решению на базе словаря — API получится явным, а структура данных простой.
Статья основана на вопросе с StackOverflow от Javier Olivares и ответе от Richard.