![](https://habrastorage.org/getpro/habr/upload_files/a82/4aa/799/a824aa799b6db40595c585b4d461793c.png)
Привет, Хабр!
Сегодня с вами участник профессионального сообщества NTA Милованов Максим.
В процессе работы периодически возникает необходимость выгрузить обрабатываемую в программе информацию, для её хранения или дальнейшей передачи. При этом важно не утратить информацию или её часть, и быть уверенным, что тот, кому файл будет передаваться, сможет с ним работать. И тогда на помощь приходит сериализация данных.
Сериализация данных — это преобразование данных, обрабатываемых в программе (структур и объектов) в форматы, которые можно хранить и передавать.
Задача состоит в том, чтобы можно было в дальнейшем воссоздать точную копию сохранённых данных, не утратив какую‑либо информацию. В Python есть разнообразные способы сделать это, некоторые их которых рассмотрю и сравню в этом посте.
Навигация по посту
Сериализация данных с помощью pickle
В начале рассмотрю стандартную библиотеку pickle. Библиотека работает с двоичными потоками данных, как в файл, так и по сети. Открыв один поток можно последовательно добавлять в него данные, при этом повторное добавление данных не приводит к их задвоению в итоговом файле, так как модуль pickle хранит историю.
В общем случае, сохранение и загрузка с использованием модуля pickle выглядит так:
import pickle
with open('pickle_dump_ex', 'wb') as output_file:
pickle.dump(data_to_save, output_file)
with open('pickle_dump_ex', 'rb') as input_file:
data_to_load = pickle.load(input _file)
Pickle позволяет сериализовать большое количество разнообразных объектов, используемых в Python. Можно даже выполнять сериализацию пользовательских классов и функций, с тем нюансом что код функций или классов не сериализуется, а сериализуются только конкретные объекты и ссылки на функции. Это значит, что для успешного распаковывания объектов требуется исходный код.
Примечательно, что библиотека позволяет исполнять программный код при десериализации данных. Функционал библиотеки позволяет добавить в класс методы getstate, setstate, и reduce, который описывает поведение объекта при сериализации/десериализации. Поэтому очень важно знать, что в файле не содержится вредоносного кода. С другой стороны, это может быть удобным подспорьем если требуется, например, напомнить себе о том на каком этапе находилась обработка перед сохранением объекта. Выглядит это следующим образом:
class remind:
counter = 0
def __reduce__(self):
msg = 'currently on iteration ' + str(self.counter)
return print, (msg,)
new_reminder = remind()
remind.counter = 10
with open('pickle_code_execution', 'wb') as f:
pickle.dump(new_reminder, f)
При загрузке файла будет отображено следующее:
import pickle
input_file = open('pickle_code_execution', 'rb')
funct = pickle.load(input_file)
Output: “currently on iteration 10”
Очевидно, что подобный функционал может привести к негативным последствиям, если в десериализуемом файле будет содержаться нежелательный исполняемый код.
В целом, для большинства типовых задач сериализации данных в Python модуль pickle является более чем достаточным, однако у него есть ряд слабых сторон, для решения которых можно использовать другие подходы.
Альтернативные способы сериализации
Файлы, полученные с помощью pickle не подходят для использования вне Python, но иногда такая необходимость возникает. К счастью, есть разнообразные форматы, поддержка которых присутствует во многих языках программирования, за счёт встроенных или доступных для дозагрузки библиотек. Два из таких, для сериализации в текст или в бинарный файл, сейчас разберу.
JSON
В первую очередь поговорим про сериализацию в формат json. Изначально разработанный для передачи данных по сети с использованием javascript, он получил широкое распространение, и сейчас поддерживается практически везде. Сформированные с его помощью файлы имеют структуру, которую можно прочитать просто открыв файл в блокноте, так как он просто хранит текстовые данные.
Основным недостатком для применения является ограниченный список поддерживаемых по умолчанию форматов данных: словари, списки, числа, строки, булевы переменные. Для всех остальных форматов потребуется предварительно перевести данные к одному из них. Но для поддерживаемых форматов (например, список) работа с форматом выглядит так:
Сохранение:
with open('json_dump_ex', 'w') as filestream:
json.dump(data_to_save, filestream)
и загрузка:
with open('json_dump_ex', 'r') as filestream:
data_to_load = json.load(filestream)
Примечательно, что файл открывается не как поток байтов, а для чтения и записи текста, характерная особенность формата JSON. Полученный после этого файл можно без проблем передать в любую среду, что является сильной стороной подобных способов сериализации.
Msgpack
Другим примером универсальной сериализации является двоичный формат msgpack. Официально реализованный для богатого набора самых популярных языков программирования, он обеспечивает схожую с JSON кросс‑платформенность, при этом авторы формата утверждают, что при его использованиии достигается большая скорость работы и меньший объём выходных файлов. При этом, просто открыть файл для редактирования уже не представляется возможным. По умолчанию он поддерживает те же форматы данных в Python что и библиотека json, а простейший пример работы с ним выглядит следующим образом:
with open('msgpack_dump_ex', 'wb') as filestream:
msgpack.dump(data_to_save, filestream)
и загрузка:
with open(' msgpack _dump_ex', 'rb') as filestream:
data_to_load = msgpack.load(filestream)
Dill
С другой стороны, когда важнее расширение поддерживаемых типов данных для сериалиции, подходит модуль dill. С его помощью, например, можно сериализовать не только объекты класса, но и сам класс или функцию, открытые потоки подключения к файлам или к базам данных, и ряд других. Поддерживается даже сериализация в целом состояния ядра, что очень удобно поскольку одной командой позволяет сохранить всё, с чем в текущий момент работаешь, и так же легко полностью восстановить. Так как dill создан для того чтобы расширить возможности pickle, его простое использование не отличается от pickle:
with open('dill_dump_ex', 'wb') as output_file:
dill.dump(data_to_save, output_file)
with open('dill_dump_ex', 'rb') as input_file:
data_to_load = dill.load(input _file)
Сохранение и загрузка текущего состояния ядра выполняется ещё проще:
dill.dump_session('dill_dump_session')
dill.load_session('dill_dump_session')
Вот так, двумя простыми командами можно выгрузить и загрузить текущую работу, со всеми классами, функциями, переменными и даже открытыми потоками и подключениями.
Интересно, что dill используется вместо pickle в форке стандартной библиотеки multiprocessing под названием multiprocess, для передачи информации между потоками. Это позволяет, например, использовать одно подключение к базе данных, вместо того чтобы открывать его в каждом из потоков.
Расширенная поддержка форматов является очевидным преимуществом перед pickle, но dill не является стандартной библиотекой, это может служить определённым ограничением при её использовании. Так же остаётся актуальным ограничение на работу только внутри python совместимых версий, и риск исполнения нежелательного кода при распаковке сериализованных файлов.
Сравнение работы
Напоследок, сравнюна простых примерах скорость работы представленных библиотек и размеры выходных файлов. В качестве объекта данных будет использоваться список чисел размером 1000000*20, а также numpy array аналогичного размера.
Таблица 1. Сравнение сериализации для числового массива
Вид сериализации | Размер выходного файла, кБ | Время сериализации, сек. | Время десериализации, сек. |
json | 397855 | 61,17 | 17,2 |
msgpack | 178711 | 1,95 | 6,73 |
pickle | 179715 | 1,15 | 2,67 |
dill | 179715 | 71,8 | 2,47 |
Таблица 2. Сравнение сериализации для numpy array
Вид сериализации | Размер выходного файла, кБ | Время сериализации, сек. | Время десериализации, сек. |
pickle | 156251 | 0,62 | 0,75 |
dill | 156251 | 0,63 | 0,73 |
Заключение
Видно, что поведение алгоритмов сериализации сильно изменяется, в зависимости от того какие данные требуется сохранить, но, в целом, для json характерны наибольший размер выходного файла и значительное время работы.
Если итоговый файл будет обрабатываться не в Python, или версии Python сильно отличаются, а читаемость файла человеком не требуется, выгоднее использовать msgpack.
Когда работа идёт только внутри Python, модуль pickle показывает высокую эффективность практически в любом случае.
Dill, ожидаемо, оказывается несколько медленнее, так как является надстройкой над pickle, расширяющей его функциональность. Дополнительные функции, и поддержка расширенного списка объектов, которые можно сериализовать, в определённых ситуациях будут приводить к замедлению в работе, но расширение списка поддерживаемых объектов вплоть до возможности сериализации всей сессии целиком делает его весьма удобным.