Как реализуются атрибуты в классах Python
Что проверяют
Различает ли кандидат атрибуты класса и атрибуты экземпляра, понимает ли, что они лежат в словарях (__dict__), и знает ли порядок поиска. Хороший ответ доходит до дескрипторов, потому что property это именно дескриптор, а не «метод, который притворяется полем».
Как отвечать
Атрибут экземпляра создаётся присваиванием через self и живёт в obj.__dict__, свой у каждого объекта. Атрибут класса объявляется в теле класса, лежит в Cls.__dict__ и общий для всех экземпляров.
class Dog:
species = "Canis lupus familiaris" # атрибут класса
def __init__(self, name, age):
self.name = name # атрибут экземпляра
self.age = age # уходит в сеттер property
@property
def age(self):
return self._age
@age.setter
def age(self, value):
if value < 0:
raise ValueError("возраст не может быть отрицательным")
self._age = valueОбратите внимание: в __init__ присваивается self.age, а не self._age. Присваивание напрямую в приватное поле обходит валидацию, и объект спокойно создастся с возрастом минус пять.
Порядок поиска obj.x (это то, ради чего вопрос и задают):
- дата-дескриптор с именем
x, найденный вtype(obj).__mro__(у него есть__set__или__delete__, как уproperty); obj.__dict__["x"];- обычный атрибут или не-дата-дескриптор (функция,
classmethod,staticmethod) из классов по MRO; - если ничего не нашлось, вызывается
__getattr__, иначеAttributeError.
Отсюда два практических следствия. Первое: property перебивает одноимённую запись в __dict__ экземпляра, поэтому валидацию не обойти. Второе: обычный атрибут класса, наоборот, перекрывается атрибутом экземпляра, и dog.species = "x" не меняет класс, а создаёт новую запись в экземпляре.
Отдельная ловушка: изменяемый атрибут класса. tricks = [] в теле класса это один список на все объекты, и dog.tricks.append("сидеть") мутирует общий, а не свой. Списки и словари инициализируются в __init__.
Прочее, что уместно упомянуть:
- динамика:
setattr(obj, "name", v),getattr(obj, "name", default),hasattr,vars(obj)какobj.__dict__; __slots__ = ("name", "_age")убирает__dict__, экономит память на массовых объектах и запрещает заводить новые атрибуты;- имена вида
__xвнутри класса переписываются в_Cls__x(name mangling), это защита от случайного пересечения имён в наследнике, а не приватность; @dataclassгенерирует__init__с атрибутами экземпляра, а изменяемые значения по умолчанию требует объявлять черезfield(default_factory=list)ровно по причине из ловушки выше.
Где спотыкаются
Кладут изменяемый объект в атрибут класса и получают общее состояние на все экземпляры. И объясняют property как «метод с декоратором», не понимая, что это дата-дескриптор на классе, и потому не могут ответить, почему self.age = value внутри сеттера не уходит в бесконечную рекурсию, а self._age не конфликтует с ним.