Сохранение состояния pickle и классы наследники
При возникновении ситуации, когда pickle отказывается сохранять
(сериализовать) некоторые атрибуты, как класса наследника, так
и родительского класса (либо они сами имеют очевидные зависимости
от текущего состояния: дескрипторы открытых файлов и т.п.),
кажется предпочтительным, что бы каждый класс решал проблемы
своих собственных атрибутов самостоятельно.
Для случая без наследования, в документации pickle описано, как
действовать с атрибутами, которые нельзя замариновать
(Handling Stateful Objects,
сделать копию __dict__, удалить и т.п.).
В случае чисто древовидной иерархии наследования, после учёта ряда
принципиальных замечаний @Vitalizzare, способ
работы с pickle может быть таким (слов много, зато кода мало):
Базовые классы, если содержат немаринуемые атрибуты, модифицируются как обычно, согласно Handling Stateful Objects, с одним дополнением, что если им требуются временные атрибуты, то они должны быть приватными атрибутами класса (см.
MySuperниже);Промежуточные классы, если содержат только хорошие атрибуты, остаются без изменений;
Если класс наследник содержит немаринуемый атрибут, то, при сохранении, он, при необходимости, может создавать приватные атрибуты класса, после чего должен вызвать
super().__getstate__()и удалить немаринуемые атрибуты. А при восстановлении, первым делом, вызватьsuper().__setstate__(state).
class MySuper:
def __getstate__(self):
... # Добавляем начинающиеся с `__` приватные атрибуты класса,
# если они необходимы.
state = self.__dict__.copy()
... # Только удаляем то, что спасти нельзя.
return state
def __setstate__(self, state):
self.__dict__.update(state)
... # Воссоздаём неспасаемое.
...
class MyIntermediate(MySuper):
...
class MyDerive1(MyIntermediate):
def __getstate__(self):
... # Добавляем начинающиеся с `__` приватные атрибуты класса,
# если они необходимы.
state = super().__getstate__()
... # Только удаляем то, что спасти нельзя.
return state
def __setstate__(self, state):
super().__setstate__(state)
... # Воссоздаём неспасаемое.
...
В случае множественного наследования, всё станет немного запутано,
т.к. придётся объединить исключения от мамы, папы и других
старших родственников. Но если все они делают исключения на основе
единой таблицы __dict__, то это не должно стать принципиальной
проблемой. Но, это увы, конечно, не один оператор.
Похоже на то, что, как вышесказанное, так и даже написанное в Handling Stateful Objects, верно только, если класс не участвует во множественном наследовании, никаким боком.
В общем же случае, одно из двух, либо атрибуты класса совместимы с pickle, тогда класс неизменен, либо его надо модифицировать следующим образом:
class Generic:
def __getstate__(self):
... # Добавляем начинающиеся с `__` приватные атрибуты класса,
# если они необходимы.
state = super().__getstate__().copy()
... # Только удаляем то, что спасти нельзя.
return state
def __setstate__(self, state):
getattr(super(), '__setstate__', self.__dict__.update)(state)
... # Воссоздаём неспасаемое.
...
А дальше уж, присвоение можно оптимизировать в общем виде, как:
state = super().__getstate__()
if state is self.__dict__:
state = state.copy()
, либо с учётом положение класса в иерархии наследования.
Я ничего не забыл? Нет ли каких подводных граблей в таком подходе?
Макет примера использования: https://github.com/Serge3leo/temp-cola/blob/main/ruSO/1614688-pickle-и-классы-наследники.ipynb
P.S.
Полезное приспособление для контроля совместимости
класса с pickle и определения проблемных атрибутов, в противном случае: https://stackoverflow.com/a/7218986/8585880 .
Тема взаимодействия __weakref__ и pickle, хотя и интересна (в том же Matplotlib они есть), но это, наверное, другой вопрос, который я пока не могу сформулировать.
Ответы (1 шт):
Я полагаю, что когда вы говорите "функции родительского класса работают с тем же __dict__, что и функции наследника", то под функциями класса наверное имеете ввиду методы объекта. Они работают не с атрибутом __dict__, а с самим объектом, который неявно передаётся первым аргументом при их вызове. И в этом смысле содержимое self.__dict__ для родительских методов будет тем же, что и для методов, заданных объекту классом-наследником (просто потому, что self в обоих случаях - это один и тот же объект).
Предположим, что вы нигде не используете slots, weakref и дескрипторы. Тогда в вашем коде остаётся потенциальный изъян в методах наследника. В вызове super().__setstate__(state) нужно передать не state, созданный и сохраненный наследником, а ту его часть, с которой может работать родитель:
class MyDerive1(MySuper):
...
def __setstate__(self, state):
superstate = ... # извлекаем родительскую часть данных
super().__setstate__(superstate)
...
...
Если мы определяем __setstate__, то сохраняемые данные не обязаны быть словарём, отображающим содержимое self.__dict__. Например, при воссоздании данных по косвенным указаниям, сохраняемое состояние будет содержать эти указания в каком-то picklable формате, а не данные из self.__dict__. Соответственно, ту часть, что передает нам super().__getstate__(), было бы разумно сохранить отдельным блоком, например, как часть кортежа:
class MyDerive1(MySuper):
...
def __getstate__(self):
superstate = super().__getstate__()
state = ...
return (superstate, state)
...
Если вы этого не делаете, то тогда получается, что класс-наследник диктует родителю определённый формат поведения, что противоречит идее наследования.
P.S. Замечание по добавленному в вопрос примеру. Полагаю, что вы смешали два понятия - содержимое __dict__ сохраняемого объекта и инструкцию для восстановления несохраняемой части. Прокомментирую часть вашего кода:
class MySuper:
def __getstate__(self):
state = self.__dict__.copy()
del state['scbt']
# !!! Эта часть вероятно описывает состояние, но не является им.
# Она НЕ должна добавляться в словарь атрибутов объекта,
# потому что: 1) может перекрыть ключи класса потомка;
# 2) при восстановлении может быть добавлена к объекту,
# повлияв на его последующее поведение.
state['__scbt_status__'] = self.scbt.get_status()
state['__figwidth__'] = self.fig.get_figwidth()
state['__figheight__'] = self.fig.get_figheight()
return state
def __setstate__(self, state):
# !!! Эта часть должна выполняться только после того,
# как вы удалите из сохраненного словаря
# добавленное описание несохраняемого состояния, см. ниже
self.__dict__.update(state)
# !!! Эта часть использует описание состояния, чтобы восстановить
# несохраняемую часть. Она не должна оказаться
# в словаре __dict__ атрибутов объекта, потому что
# это может неявно повлиять на его поведение.
self.fig.set_figwidth(state['__figwidth__'])
self.fig.set_figheight(state['__figheight__'])
self._MySuper_init_interface(
scbt_actives=state['__scbt_status__'])
...
Повторю ключевые моменты из комментариев к коду:
- при добавлении в
self.__dict__ключей, описывающих несохраняемое состояние, вы неявно выдвигаете требование к потомкам не использовать эти ключи в своей работе; - при восстановлении той части, которая описана добавленными ключами, эти ключи должны быть удалены из
stateперед восстановлениемself.__dict__, иначе само их появление может повлиять на поведение объекта.
Мы могли бы взять пример с дефолтного поведения, когда объект содержит __dict__ и __slots__, т.е. вернуть не один словарь __dict__ с вкраплениями [предположительно] новых ключей описания, а пару (__dict__, state), где state - это описание того, что нужно восстановить:
class MySuper:
def __getstate__(self):
__dict__ = self.__dict__.copy()
del __dict__['scbt']
state = {
'__scbt_status__': self.scbt.get_status(),
'__figwidth__': self.fig.get_figwidth(),
'__figheight__': self.fig.get_figheight(),
}
return __dict__, state
def __setstate__(self, state):
__dict__, state = state
self.__dict__.update(__dict__)
self.fig.set_figwidth(state['__figwidth__'])
self.fig.set_figheight(state['__figheight__'])
self._MySuper_init_interface(
scbt_actives=state['__scbt_status__'])
...
Аналогичное требование к потомку - не смешивать содержимое __dict__ и характеристики восстанавливаемых состояний. Пример:
class MyDerive1(MySuper):
def __getstate__(self):
superstate = super().__getstate__()
# !!! Предполагаем, что родитель вернет
# либо кортеж с __dict__ на первом месте,
# либо словарь __dict__; варианты None,
# (None, slots_mapping) и другие пользовательские
# форматы исключаем для простоты изложения.
# https://docs.python.org/3/library/pickle.html#object.__getstate__
if isinstance(superstate, tuple):
__dict__, *superstate = superstate
else:
__dict__, superstate = superstate, ()
del __dict__['dcbt1']
state = {'__dcbt1_status__': self.dcbt1.get_status()}
return __dict__, *superstate, state
def __setstate__(self, state):
*superstate, state = state
super().__setstate__(superstate)
self._MyDerive1_init_interface(
dcbt1_actives=state['__dcbt1_status__'])
...