Как получить атрибут класса только из самого класса
Пошаговое руководство: как в Python получить атрибут класса только если он определён в самом классе (без наследования). Объяснение __dict__/vars(), отличие от getattr/hasattr, дескрипторы и sentinel.
Как в 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.
Содержание
- Как получить атрибут класса (только из самого класса)
- Почему getattr / hasattr возвращают унаследованное значение
- Практические варианты и шаблоны кода
- Подводные камни: дескрипторы, метаклассы и значение None
- Как узнать, в каком классе атрибут определён (MRO)
- Источники
- Заключение
Как получить атрибут класса (только из самого класса)
Самый простой и прямой способ — проверить наличие имени в cls.__dict__ или воспользоваться vars(cls) (они дают одно и то же — read‑only mappingproxy). Пример для вашего кода:
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 и для наследников, поэтому код будет работать с унаследованным словарём, а не с “своим” атрибутом.
Практические варианты и шаблоны кода
Ниже — несколько коротких решений, от простого к более явному.
- Самый короткий и часто достаточный вариант:
own = Another.__dict__.get('Команды') # None если унаследовано/отсутствует
- Явная проверка и возврат только если определено в классе:
if 'Команды' in Another.__dict__:
val = Another.__dict__['Команды']
else:
val = None
- Утилитная функция:
def get_own_class_attr(cls, name, default=None):
return cls.__dict__.get(name, default)
- Применение в вашем init_subclass (чтобы регистрироваться только если подкласс сам определил Команды):
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:
_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:
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.
Источники
- https://ru.hexlet.io/courses/python-oop-basics/lessons/instances/theory_unit
- https://habr.com/ru/companies/otus/articles/889754/
- https://habr.com/ru/articles/914650/
- 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.