Программирование

Как получить атрибут класса только из самого класса

Пошаговое руководство: как в Python получить атрибут класса только если он определён в самом классе (без наследования). Объяснение __dict__/vars(), отличие от getattr/hasattr, дескрипторы и sentinel.

Как в Python получить атрибут класса только если он определён в самом классе (без наследования)?

python
class Система:
 \"\"\"Группа команд.\"\"\"

 Команды = {}

 def __init__(self, *args, **kwargs):
 pass

 def пользовательИмеетДоступ(self, bot, event):
 return True

 def __init_subclass__(cls, *args, **kwargs):
 if not hasattr(cls, 'Команды'): return
 print(cls, *args, **kwargs)
 cls.Команды[cls.__name__] = cls
 print(cls.__name__,cls.Команды)

class Another(Система):
 pass

getattr(Another, 'Команды') # должно вернуть None

Как проверить или получить атрибут ‘Команды’ только у класса Another (из его dict) и вернуть None, если атрибут унаследован?

Чтобы взять атрибут только если он определён прямо в классе (без наследования), смотрите в его dict или через vars(cls): например, Another.__dict__.get('Команды') вернёт значение, только если ключ есть в самом классе, иначе — None. Функции getattr/hasattr будут возвращать унаследованные значения, поэтому они здесь не годятся; при необходимости различать “отсутствует” и “присвоено None” используйте уникальный sentinel.


Содержание


Как получить атрибут класса (только из самого класса)

Самый простой и прямой способ — проверить наличие имени в cls.__dict__ или воспользоваться vars(cls) (они дают одно и то же — read‑only mappingproxy). Пример для вашего кода:

python
class Система:
 Команды = {}

class Another(Система):
 pass

# Получить только «собственный» атрибут класса Another
print(Another.__dict__.get('Команды')) # None — атрибут не определён в Another
print(getattr(Another, 'Команды')) # {} — унаследованное значение из Система

Если нужно просто булево‑проверить, используйте 'Команды' in Another.__dict__. Это надёжно показывает, присутствует ли запись в самом словаре класса, без просмотра базовых классов. Подробнее о dict и пространствах имён — см. руководство по классам и экземплярам на Hexlet: https://ru.hexlet.io/courses/python-oop-basics/lessons/instances/theory_unit.


Почему getattr / hasattr возвращают унаследованное значение

Атрибуты ищутся по правилам разрешения имён: сначала — в dict экземпляра (если это обращение с экземпляра), затем в dict самого класса, затем — по MRO в родительских классах. Поэтому hasattr(cls, name) и getattr(cls, name) обходят MRO и найдут унаследованные значения, а не только те, что лежат в cls.__dict__. Это описано в статьях о механизме поиска атрибутов и MRO: https://habr.com/ru/companies/otus/articles/889754/.

Пример ошибки, которую вы уже увидели: в __init_subclass__ проверка через hasattr(cls, 'Команды') вернёт True и для наследников, поэтому код будет работать с унаследованным словарём, а не с “своим” атрибутом.


Практические варианты и шаблоны кода

Ниже — несколько коротких решений, от простого к более явному.

  1. Самый короткий и часто достаточный вариант:
python
own = Another.__dict__.get('Команды') # None если унаследовано/отсутствует
  1. Явная проверка и возврат только если определено в классе:
python
if 'Команды' in Another.__dict__:
 val = Another.__dict__['Команды']
else:
 val = None
  1. Утилитная функция:
python
def get_own_class_attr(cls, name, default=None):
 return cls.__dict__.get(name, default)
  1. Применение в вашем init_subclass (чтобы регистрироваться только если подкласс сам определил Команды):
python
class Система:
 Команды = {}

 def __init_subclass__(cls, *args, **kwargs):
 super().__init_subclass__(*args, **kwargs)
 if 'Команды' not in cls.__dict__:
 return
 cls.Команды[cls.__name__] = cls

Так вы избежите случайного изменения базового словаря из‑за унаследованного атрибута.


Подводные камни: дескрипторы, метаклассы и значение None

  • Дескрипторы и свойства: в cls.__dict__ вы получите сам объект дескриптора (функцию, property, descriptor), а не результат его get. Если вам нужен результат поведения дескриптора на классе, учитывайте это: иногда придётся вызвать descriptor.__get__(None, cls) или просто getattr(cls, name) если вы хотите “вызвать” дескриптор. Подробно о дескрипторах — https://habr.com/ru/articles/914650/.
  • Метакласс: атрибут может быть определён в метаклассе (type(cls).dict), тогда он виден через getattr(cls, name), но не будет в cls.__dict__. Если хотите считать такие атрибуты «своими», проверьте также type(cls).__dict__.
  • None vs отсутствие: если подкласс явно присвоил Команды = None, то cls.__dict__.get('Команды') вернёт None — и вы не сможете понять, это «явное None» или «отсутствие» без дополнительной проверки. Чтобы различать, используйте sentinel:
python
_missing = object()
val = Another.__dict__.get('Команды', _missing)
if val is _missing:
 # атрибут унаследован или отсутствует
 result = None
else:
 # атрибут определён (возможно, равен None)
 result = val
  • mappingproxy: cls.__dict__ — это mappingproxy (read‑only). Менять его нельзя напрямую; для получения изменяемой копии делайте dict(cls.__dict__).

Как узнать, в каком классе атрибут определён (MRO)

Иногда полезно не только знать “есть ли у самого класса”, но и найти класс, где атрибут впервые определён. Для этого обойдите MRO:

python
import inspect

def defining_class(cls, name):
 for base in inspect.getmro(cls):
 if name in base.__dict__:
 return base
 return None

print(defining_class(Another, 'Команды')) # вернёт Система, если Команды определён там

Можно использовать и cls.__mro__ напрямую. Это удобно, если нужно зарегистрировать класс только когда он сам определил атрибут, либо отладить, откуда пришло значение. Подробнее об inspect и MRO — https://mngb.ru/programmy/20/kak-uznat-klass-obekta-python.


Источники


Заключение

Коротко: чтобы получить атрибут класса только если он определён в самом классе, смотрите в cls.__dict__ или vars(cls). Для вашего примера замените hasattr(cls, 'Команды') на проверку 'Команды' in cls.__dict__ или используйте Another.__dict__.get('Команды') — это вернёт None, если атрибут унаследован. Если важна точная различимость «отсутствует» и «явно None», примените уникальный sentinel.

Авторы
Проверено модерацией
Модерация