Программирование PythonМодель данныхPython-разработчик серверных приложений

При сериализации объекта через pickle класс определяет и reduce ex , и reduce : какой метод будет выбран пе...

При сериализации объекта через pickle класс определяет и reduce_ex, и reduce: какой метод будет выбран первым?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Сначала pickle вызывает __reduce_ex__(protocol). Метод __reduce__ используется, если __reduce_ex__ недоступен либо реализация __reduce_ex__ явно делегирует ему работу.

__reduce_ex__ получает номер протокола и поэтому может выбрать представление, совместимое с конкретной версией формата сериализации.

Исторический контекст

Сериализация объектов должна сохранять не только значения полей, но и способ восстановить объект: вызываемый объект, аргументы конструктора и состояние экземпляра. Дополнительная сложность возникает при развитии формата: новые протоколы могут поддерживать более эффективное представление данных, а старые — оставаться необходимыми для совместимости.

Для этого в модели данных Python предусмотрены два близких механизма. __reduce__ задаёт общее правило восстановления, а __reduce_ex__ позволяет учитывать используемый протокол.

Постановка проблемы

Если класс реализует оба метода и разработчик ожидает вызова __reduce__, это ожидание будет неверным: приоритет имеет __reduce_ex__. В результате сериализация может использовать другую схему восстановления, чем предполагалось.

Неправильная реализация reduction-хука приводит к ошибкам при pickle.dumps, невозможности восстановить объект или потере его состояния. Кроме того, pickle нельзя применять для загрузки недоверенных данных: восстановление может выполнить произвольные операции, описанные в сериализованном потоке.

Подробное решение

При сохранении объекта pickler ищет у него reduction-протокол. Если доступен __reduce_ex__, он вызывает его с номером выбранного протокола. Только затем рассматривается обычный __reduce__ как менее специализированный вариант.

Результатом должен быть допустимый reduction: обычно кортеж с вызываемым объектом и аргументами для создания экземпляра, а при необходимости — дополнительными данными состояния, итераторами элементов или состоянием словаря. Конкретная форма зависит от того, что требуется для восстановления объекта.

Минимальный пример:

import pickle class Token: def __reduce_ex__(self, protocol): print(f'reduce_ex:{protocol}') return (Token, ()) def __reduce__(self): print('reduce') return (Token, ()) pickle.dumps(Token(), protocol=4)

При выполнении будет вызван __reduce_ex__, и появится сообщение reduce_ex:4. Метод __reduce__ автоматически не вызывается как запасной после успешного вызова __reduce_ex__.

Если общая логика одна, __reduce_ex__ может сам вызвать __reduce__. Если нужны разные форматы, реализация должна проверять номер протокола и возвращать совместимую reduction-схему. Нельзя рассчитывать, что возврат NotImplemented автоматически переключит pickle на __reduce__: reduction-хук должен вернуть корректное описание восстановления, иначе сериализация завершится ошибкой.

Ситуация из практики

В библиотеке есть объект с большим внутренним кэшем. Кэш не нужно сохранять, но основные параметры объекта должны переживать сериализацию.

Вариант с одним __reduce__ прост и подходит, если формат восстановления одинаков для всех поддерживаемых протоколов. Его минус — отсутствие явного доступа к номеру протокола.

Вариант с __reduce_ex__ позволяет для новых протоколов возвращать компактное представление, а для старых — более совместимое. Минус — больше ветвлений и необходимость тестировать восстановление на каждом поддерживаемом протоколе.

Практический выбор — реализовать __reduce_ex__, исключить кэш из состояния и при необходимости делегировать базовую логику __reduce__. Это сохраняет совместимость, уменьшает размер данных и явно фиксирует поведение для разных протоколов.

Что кандидаты часто упускают

  1. Что произойдёт, если определить только __reduce__?

    Pickle всё равно может сериализовать объект через reduction-протокол. Отсутствие пользовательского __reduce_ex__ не означает, что объект обязательно несериализуем: будет использована доступная стандартная или пользовательская логика восстановления. Однако класс теряет возможность напрямую выбирать схему по номеру протокола.

  2. Зачем __reduce_ex__ передают номер протокола?

    Номер позволяет различать требования к совместимости и возможности формата. Для старого протокола можно вернуть более консервативное описание, а для нового — использовать поддерживаемые им способы компактного или быстрого сохранения. При этом результат должен корректно загружаться теми версиями Python и теми протоколами, для которых он предназначен.

  3. Почему нельзя считать реализацию reduction-хука обычным конструктором?

    Reduction описывает не только вызов конструктора, но и последующее восстановление состояния объекта. Если конструктор требует внешних ресурсов, а состояние содержит дескрипторы, открытые файлы или сетевые соединения, простого возврата класса и аргументов недостаточно или он может быть небезопасен. Такие ресурсы обычно восстанавливают отдельно, заменяют сериализуемым описанием либо исключают из состояния и создают заново после загрузки.