Matplotlib и pickle
Грубо говоря, есть интерактивное Python приложение для
многочасового подбора всякой разности с использованием
Matplotlib, OpenCV, SciPy, Numpy в среде JupyterLab (IPython
записка .ipynb). Хотелось бы к нему добавить новый
функционал, что-то, типа, сохранения/восстановления на
контрольных точках, а может и в любой момент.
В принципе, в стандартной библиотеке Python есть модуль pickle
с похожей функциональностью. Похоже, он более менее совместим
со всеми компонентами, кроме Matplotlib, у которого, в основном,
интерактивные "кнопки" не желают мариноваться (сериализоваться).
К сожалению, ни я, в документации Maptplotlib, ни Яндекс, по запросу "pickle site:matplotlib.org", не нашёл связанного изложения и/или рецепта. Есть только несколько десятков, вопросов и ответов с противоречивыми вердиктами.
В документации pickle описаны способ обхода такого рода проблем: Handling Stateful Objects, но применить его можно различными способами.
Вариант "прямого" кодирования в целевых классах
После "творческого теоретического домысливания" Сохранение состояния pickle и классы наследники выходит примерно таким:
class MySuper:
def __getstate__(self):
# Ввиду разного порядка добавления элементов,
# размерчики могут становится немного странными.
self.__figwidth = self.fig.get_figwidth()
self.__figheight = self.fig.get_figheight()
self.__scbt_status = self.scbt.get_status()
state = self.__dict__.copy()
# pickle не любит кнопок и т.п.
del state['scbt']
return state
def __setstate__(self, state):
self.__dict__.update(state)
self.fig.set_figwidth(self.__figwidth)
self.fig.set_figheight(self.__figheight)
self.__init_interface(scbt_actives=self.__scbt_status)
def __init_interface(self, scbt_actives=None):
self.ax['super'].clear() # Внезапная опытная заплатка
self.scbt = mpl.widgets.CheckButtons(self.ax['super'],
["Super", "Hyper"],
actives=scbt_actives)
self.scbtcid = self.scbt.on_clicked(self.scbt_on_clicked)
def __init__(self):
self.fig, self.ax = plt.subplot_mosaic([
['plt', 'super'],
['plt', 'derive'],
['plt', '.']],
width_ratios=[80, 20],
height_ratios=[15, 15, 70],
)
self.ax['plt'].plot(np.arange(-1, 10)**2)
self.__init_interface()
self.scbt_cnt = 0
def scbt_on_clicked(self, label):
print(f"MySuper.scbt_on_clicked({label, self.scbt_cnt=})")
self.scbt_cnt += 1
class MyIntermediate(MySuper):
def __init__(self):
super().__init__()
self._has_intermediate = True
class MyDerive1(MyIntermediate):
def __getstate__(self):
self.__dcbt1_status = self.dcbt1.get_status()
state = super().__getstate__()
del state['dcbt1']
return state
def __setstate__(self, state):
super().__setstate__(state)
self.__init_interface(dcbt1_actives=self.__dcbt1_status)
def __init_interface(self, dcbt1_actives=None):
self.ax['derive'].clear() # Внезапная опытная заплатка
self.dcbt1 = mpl.widgets.CheckButtons(self.ax['derive'],
["Derive1", "Child1"],
actives=dcbt1_actives)
self.dcbt1cid = self.dcbt1.on_clicked(self.dcbt1_on_clicked)
assert self._has_intermediate
def __init__(self):
super().__init__()
self.__init_interface()
self.dcbt1_cnt = 0
def dcbt1_on_clicked(self, label):
print(f"MyDerive1.dcbt1_on_clicked({label, self.dcbt1_cnt=})")
self.dcbt1_cnt += 1
Полностью, с примером использования, в записке: https://github.com/Serge3leo/temp-cola/blob/main/ruSO/1614688-pickle-и-классы-наследники.ipynb
Как бы, 7 упоминаний названия для каждой кнопки или 4 "self.fig", на любителя, зато все ограничения и грабли на поверхности, т.е. имеются все удобства для их обхода.
Правда, при обнаружении новых опытных граблей, что не исключено, придётся исправлять всюду.
Вариант обёрток проблемных классов Matplotlib
class PicklingFigure:
"""Обёртка, для сохранения размера рисунка.
Ввиду разного порядка добавления элементов, после
восстановления размерчики могут становится немного
странными. Использование:
>>> fig, ax = plt.subplots()
>>> fig_pickling = PicklingFigure(fig)
"""
def __getstate__(self):
self.__figwidth = self._fig.get_figwidth()
self.__figheight = self._fig.get_figheight()
return self.__dict__.copy()
def __setstate__(self, state):
self.__dict__.update(state)
self._fig.set_figwidth(self.__figwidth)
self._fig.set_figheight(self.__figheight)
def __init__(self, fig):
assert isinstance(fig, mpl.figure.Figure)
self._fig = fig
class PicklingCheckButtons(mpl.widgets.CheckButtons):
"""Обёртка для использования совместно с `pickle`.
Всюду использовать `PicklingCheckButtons(ax, ...)`
вместо `mpl.widgets.CheckButtons(ax, ...)`.
"""
def __getstate__(self):
for l in self.labels:
if l.figure != self.ax.figure:
print(f"WARNING: {type(self).__name__}.__getstate__: "
f"{l.figure, self.ax.figure=}")
assert self.ax.figure is not None
return {'_pickling_ax': self.ax,
'_pickling_labels': self.labels,
'_pickling_eventson': self.eventson, # Вроде упоминается в документации
'_pickling_status': self.get_status(),
'_pickling_init_args': self._pickling_init_args,
'_pickling_init_kwargs': self._pickling_init_kwargs,
'_pickling_cids_funcs': self._pickling_cids_funcs,
'_pickling_next_cid': self._pickling_next_cid,
'_pickling_ax_figure': self.ax.figure,
}
def __setstate__(self, state):
state['_pickling_ax'].clear()
self.__init__(*state['_pickling_init_args'],
**state['_pickling_init_kwargs'])
self.ax = state['_pickling_ax']
self.labels = state['_pickling_labels']
self.eventson = state['_pickling_eventson']
current = self.get_status() # У 3.8 нет `set_active(i, state=s)`
for i in range(len(state['_pickling_status'])):
if current[i] != state['_pickling_status'][i]:
self.set_active(i)
self._pickling_next_cid = state['_pickling_next_cid']
for cid, (_, func) in state['_pickling_cids_funcs'].items():
self.on_clicked(func, _pickling_cid=cid)
# Минимальный контроль потенциальных граблей
assert state['_pickling_ax_figure'] == self.ax.figure, (
f"{state['_pickling_ax_figure'], self.ax.figure=}")
assert self.ax.figure is not None
# Внезапные опытные грабли, которых почему-то не было у альтернативы
for l in self.labels:
if l.figure != self.ax.figure:
print(f"WARNING: {type(self).__name__}.__setstate__: "
f"{l.figure, self.ax.figure=}")
l.figure = self.ax.figure
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._pickling_init_args = args
self._pickling_init_kwargs = kwargs
self._pickling_cids_funcs = {}
self._pickling_next_cid = 0
def on_clicked(self, func, _pickling_cid=None):
scid = super().on_clicked(func)
if _pickling_cid is None:
_pickling_cid = self._pickling_next_cid
self._pickling_next_cid += 1
self._pickling_cids_funcs[_pickling_cid] = (scid, func)
return _pickling_cid
def disconnect(self, cid):
scid, func = self._pickling_cids_funcs[cid]
super().disconnect(scid)
del self._pickling_cids_funcs[cid]
Полностью, с примером использования, в записке: https://github.com/Serge3leo/temp-cola/blob/main/ruSO/1614729-Matplotlib-и-pickle.ipynb
Немного объёмно, зато в использовании кратко. Однако, ограничения и грабли скрыты под капотом.
К примеру, CheckButtons не даёт документированного доступа к своим
атрибутам и атрибутам своих частей, приходится запоминать аргументы
в момент создания, что как-то не очень.
Альтернативы pickle
Мотивом рассмотрения pickle для сохранения состояния послужила
сравнительная простота использования. К примеру, если сравнивать
со стандартным модулем json, то у того сразу возникают проблемы:
namedtuple, вообще, полная беда;- типы Numpy;
- пользовательские классы.
В целом, использование json требует немаленьких ручных доработок
(реализация функций default() и object_hook() для многих, почти
всех классов), как для классов целевого кода, так и для классов
кода отображения. Правда, можно надеяться, результат окажется более
надёжным и предсказуемым, ну, после отладки и тестирования.
Напротив, использование pickle, в моём случае, требует сравнительно
небольших доработок только некоторых частей кода отображения.
Но, конечно, объём файла сохраненного состояния pickle в тысячи раз больше (десятки мегабайт pickle против десятков килобайт json).
Кроме того уже встретились и могут ещё выскочить странные
грабли. Потому и есть вопросы. Возможно, кто-то уже наступал?
P.S.
Пока не разобрался в способах "просмотра"/"анализа" файла pickle, в случае если его, по тем или иным
причинам не получается загрузить.