Урок 06 из 12 · 2 минуты чтения

Модель данных — создание объектов, атрибуты и дескрипторы

__new__ и __init__, согласование __eq__ и __hash__, __getattr__ и __getattribute__, дескрипторы и __slots__.

Как создаётся объект

Point(1, 2) вызывает два метода по очереди:

  1. Point.__new__(Point, 1, 2) — создаёт и возвращает экземпляр;
  2. obj.__init__(1, 2) — инициализирует его и ничего не возвращает.

Если __new__ вернул объект не своего класса, __init__ не вызывается. Переопределять __new__ нужно редко:

  • подклассы неизменяемых типов — значение int, str, tuple задаётся при создании, в __init__ его уже не поменять;
  • кэши и синглтоны — вернуть уже существующий экземпляр;
  • метаклассы.
class Celsius(float):
    def __new__(cls, value):
        if value < -273.15:
            raise ValueError("ниже абсолютного нуля")
        return super().__new__(cls, value)

__eq__ и __hash__

Хешируемый объект имеет хеш, неизменный за всё время жизни. Правило: равные объекты обязаны иметь равные хеши — иначе словарь и множество перестанут их находить.

Если класс переопределяет __eq__ и не определяет __hash__, Python ставит __hash__ = None — экземпляры становятся нехешируемыми. Хеш считают по тем же полям, что и равенство, и только по неизменяемым:

def __eq__(self, other):
    return isinstance(other, Point) and (self.x, self.y) == (other.x, other.y)

def __hash__(self):
    return hash((self.x, self.y))

Проще всего — @dataclass(frozen=True): он сгенерирует согласованную пару.

__getattribute__ и __getattr__

  • __getattribute__ вызывается при каждом обращении к атрибуту — это сам механизм поиска. Переопределять его опасно: любое self.x внутри снова вызовет его же — бесконечная рекурсия. Обращаются через object.__getattribute__(self, name).
  • __getattr__ вызывается только если атрибут не найден обычным путём. Для прокси, обёрток и ленивых атрибутов используют его; для неизвестных имён он должен бросать AttributeError.
class Proxy:
    def __init__(self, target):
        self._target = target

    def __getattr__(self, name):
        return getattr(self._target, name)

Дескрипторы

Дескриптор — объект в атрибуте класса, у которого есть __get__ и, возможно, __set__ и __delete__. При обращении obj.attr Python вызывает эти методы вместо простого чтения. На дескрипторах построены property, обычные методы (функция — дескриптор, который привязывает self), staticmethod, classmethod и __slots__.

class Positive:
    def __set_name__(self, owner, name):
        self.name = "_" + name

    def __get__(self, obj, owner):
        return getattr(obj, self.name)

    def __set__(self, obj, value):
        if value <= 0:
            raise ValueError("нужно положительное число")
        setattr(obj, self.name, value)

class Order:
    count = Positive()

Дескриптор с __set__ (data descriptor) важнее словаря экземпляра, а без него уступает словарю — поэтому метод можно перекрыть атрибутом экземпляра. Так устроены поля ORM в Django и SQLAlchemy.

__slots__

__slots__ = ("x", "y") заменяет у экземпляров словарь __dict__ фиксированными ячейками. Объект занимает в несколько раз меньше памяти, доступ чуть быстрее, но добавить новый атрибут нельзя. Выигрыш заметен на миллионах мелких объектов. Наследник без своих __slots__ снова получает __dict__; для слабых ссылок нужен слот __weakref__. С Python 3.10 — @dataclass(slots=True).

Что запомнить

  • __new__ создаёт, __init__ настраивает.
  • Равные объекты — равные хеши; переопределили __eq__ — определите __hash__.
  • Для прокси — __getattr__, а не __getattribute__.
  • property и методы — это дескрипторы.

Проверка урока

Войдите, чтобы проверить себя: сданные проверки всех уроков курса отмечают его темы пройденными на карте и в ИПР.

Вопросы по теме

Читайте также

Партнёр платформы

Разбор собеседования с sotto

После собеседования sotto разбирает разговор: оценка, каждый вопрос — хорошо, частично или слабо, чего не хватило в ответе и над чем поработать к следующему разу. Первые 30 минут бесплатно.

Разбор собеседования