Удаление атрибута экземпляра срабатывает на дескриптор класса: в каком случае Python вызывает его __delete__, а не удаляет запись экземпляра?
Python вызывает __delete__, если атрибут класса является дескриптором данных, реализующим метод __delete__. В этом случае операция удаления делегируется дескриптору, поэтому одноимённая запись в __dict__ экземпляра не удаляется напрямую.
Если __delete__ не определён, удаление через такой дескриптор невозможно и обычно завершается AttributeError. Реализация __delete__ делает дескриптор участником управления жизненным циклом значения: он может удалить данные из внешнего хранилища, сбросить состояние или запретить операцию.
Дескрипторы появились как универсальный механизм управления доступом к атрибутам объектов. Они позволили выразить в едином протоколе свойства, связанные методы, вычисляемые атрибуты и управляемые поля без специальной обработки каждого отдельного класса.
Поддержка удаления является частью этой модели. Она нужна для случаев, когда атрибут нельзя корректно удалить простым удалением записи из словаря экземпляра: значение может находиться в базе данных, кэше, внешнем ресурсе или требовать обязательной очистки.
Обычное удаление атрибута подразумевает удаление его значения из состояния экземпляра. Но для управляемого атрибута это может быть неверно: запись в __dict__ может быть лишь кэшем, а настоящие данные — частью другого хранилища.
Если дескриптор не перехватывает удаление, возможны рассинхронизация состояния, утечка ресурса или обход бизнес-правил. Например, удаление локального кэша не должно автоматически считаться удалением записи из постоянного хранилища.
При поиске атрибута Python сначала учитывает атрибут класса. Дескриптор, у которого есть __set__ или __delete__, считается дескриптором данных. Для операции удаления такой дескриптор получает экземпляр и имя удаляемого атрибута через вызов __delete__(self, obj).
Минимальный пример:
Операция del user.name не ищет и не удаляет ключ name из user.__dict__. Вместо этого Python вызывает Field.__delete__(user), а дескриптор сам определяет, что считать удалением.
Если дескриптор реализует только __delete__, он всё равно является дескриптором данных. Это важно для приоритета: наличие метода удаления уже достаточно, чтобы дескриптор участвовал в управлении атрибутом как дескриптор данных.
__delete__ не вызывается при удалении атрибута самого класса, например при удалении User.name. Такая операция изменяет пространство имён класса и относится к метаклассу, а не к экземпляру. Также удаление может быть запрещено намеренно: метод может всегда возбуждать AttributeError, если атрибут должен существовать на протяжении всей жизни объекта.
Главный компромисс — перенос ответственности за согласованность состояния в дескриптор. Это даёт контроль, но усложняет код: необходимо явно решить, что именно удаляется, как обрабатывается отсутствие значения и что происходит при ошибке внешнего хранилища.
В ORM-модели поле хранится в строке базы данных, а экземпляр объекта содержит только загруженное значение. Наивная реализация __delattr__ могла бы удалить локальное значение, но оставить запись в базе, создав иллюзию удаления.
Рассматривались два варианта. Переопределить __delattr__ в модели проще для одного класса, но логика быстро дублируется и начинает зависеть от имён полей. Реализовать отдельный дескриптор сложнее, зато правила чтения, записи и удаления сосредоточены рядом и могут быть переиспользованы.
Выбран дескриптор с __delete__, который выполняет проверку прав, удаляет запись из постоянного хранилища и только после успешной операции очищает локальное состояние. Если хранилище недоступно, исключение прерывает операцию, а объект не оказывается ошибочно помечен как очищенный.
__delete__, чтобы дескриптор считался дескриптором данных?Да. Дескриптор данных определяется наличием __set__ или __delete__, а не обязательным наличием обоих методов. Поэтому дескриптор только с __delete__ имеет приоритет над одноимённым атрибутом в __dict__ экземпляра при операциях, которыми управляет протокол дескрипторов.
Это отличается от дескриптора только с __get__: такой объект является дескриптором без данных, и атрибут экземпляра может иметь более высокий приоритет при чтении. Наличие __delete__ меняет именно категорию дескриптора и его поведение в разрешении атрибутов.
__delete__ ничего не удаляет?Python не контролирует фактическое изменение состояния после вызова метода. Если __delete__ завершился без исключения, операция del считается успешно выполненной, даже если дескриптор не изменил ни словарь экземпляра, ни внешнее хранилище.
Поэтому ответственность за семантику удаления полностью лежит на дескрипторе. Если удаление невозможно, следует явно возбудить подходящее исключение, обычно AttributeError или исключение уровня хранилища, а не молча сообщать об успехе.
__dict__ экземпляра в обход __delete__?Да, прямое изменение obj.__dict__ технически обходит обычный протокол доступа к атрибутам. Например, удаление ключа из словаря не вызывает __delete__, потому что это операция над самим словарём, а не del obj.attribute.
Однако такой обход опасен: дескриптор может хранить данные не в __dict__, а в другом месте, и прямое изменение словаря нарушит инварианты объекта. Поэтому подобный приём допустим только для отладки или внутренней реализации, когда намеренно требуется обойти публичный протокол.