Время от времени требуется сохранить на диск или отослать по сети объект со сложной структурой. Например, текущее состояние нейронной сети, находящейся в процессе обучения. Процесс перевода структуры данных в цепочку битов называется сериализацией.
После прочтения статьи вы будете знать:
что такое сериализация и десериализация;
как применять эти процессы для собственного удобства;
какие существуют встроенные и сторонние библиотеки Python для сериализации;
чем отличаются протоколы pickle;
в чём преимущество dill перед pickle;
как с помощью dill сохранить сессию интерпретатора;
можно ли сжать сериализованные данные;
какие бывают проблемы с безопасностью процесса десериализации.
Сериализация в Python
Итак, сериализация (англ. serialization, marshalling) – это способ преобразования структуры данных в линейную форму, которую можно сохранить или передать по сети. Обратный процесс преобразования сериализованного объекта в исходную структуру данных называется десериализацией (англ. deserialization, unmarshalling).
Кроме того, Python поддерживает XML, который также можно применять для сериализации объектов.
Самый старый модуль из перечисленных – marshal. Он используется для чтения и записи байт-кода модулей Python и .pyc-файлов, создаваемых при импорте модулей Python. Хотя его и можно использовать для сериализации, делать это не рекомендуется.
Модуль json обеспечивает работу со стандартными файлами JSON. Это широко используемый формат обмена данными, удобный для чтения и не зависящий от языка программирования. С помощью модуля json вы можете сериализовать и десериализовать стандартные типы данных Python:
bool
dict
int
float
list
string
tuple
None
Наконец, ещё один встроенный способ сериализации и десериализации объектов в Python – модуль pickle. Он отличается от модуля json тем, что сериализует объекты в двоичном виде. То есть результат не может быть прочитан человеком. Кроме того, pickle работает быстрее и позволяет сериализовать многие другие типы Python, включая пользовательские.
Первые два метода применяются для сериализации, а два других – для обратного процесса. Разница между первыми двумя методами заключается в том, что dump создаёт файл, содержащий результат сериализации, а dumps – возвращает строку. То есть s в конце названия dumps – сокращение от string. То же самое относится к load и loads.
Рассмотрим пример. Допустим, есть пользовательский класс example_class с несколькими атрибутами (a_number, a_string, a_dictionary, a_list, a_tuple), каждый из которых имеет свой тип.
В коде ниже показано, как создаётся и сериализуется экземпляр класса. Затем мы изменяем значение внутреннего словаря. Для восстановления исходной структуры можно использовать сохранённый с помощью pickle объект.
Таким образом, pickle создаёт глубокую копию исходной структуры.
Форматы протоколов модуля pickle
Модуль pickle специфичен для Python — результаты сериализации могут быть прочитаны только другой программой на Python. Но даже если вы работаете только с Python, полезно знать, как модуль эволюционировал со временем. От версии протокола зависит совместимость. Сейчас существует 6 версий протоколов:
0 — в отличие от более поздних протоколов, был удобочитаемым.
1 — первый двоичный формат.
2 — представлен в Python 2.3.
3 — добавлен в Python 3.0. Его нельзя выбрать в версиях Python 2.x.
4 — добавлен в Python 3.4, поддерживает более широкий диапазон размеров и типов объектов, и является протоколом по умолчанию с версии 3.8.
5 — добавлен в Python 3.8, имеет поддержку внеполосных данных и улучшает скорость для внутриполосных.
Примечание
Более новые версии предлагают больше функций и улучшений, но ограничены более высокими версиями интерпретатора. Учитывайте это при выборе протокола. Самый высокий протокол, поддерживаемый интерпретатором, хранится в атрибуте pickle.HIGHEST_PROTOCOL.
Чтобы выбрать конкретный протокол, укажите версию протокола при вызове функции модуля. Иначе будет использоваться версия, соответствующая атрибуту pickle.DEFAULT_PROTOCOL.
Сериализуемые и несериализуемые типы
Мы уже знаем, что модуль pickle сериализует гораздо больше типов, чем json. Но всё-таки не все. Список несериализуемых с помощью pickle объектов включает соединения с базами данных, открытые сетевые сокеты и действующие потоки. Если вы столкнулись с несериализуемым объектом, есть несколько способов решения проблемы. Первый вариант – использовать стороннюю библиотеку dill.
Модуль dill расширяет возможности pickle. Согласно официальной документации он позволяет сериализовать менее распространённые типы данных, например, вложенные функции (inner functions) и лямбда-выражения. Проверим на примере:
Попытавшись запустить эту программу, мы получим исключение: pickle не может сериализовать лямбда-функцию:
Попробуем заменить pickle на dill (библиотеку можно установить с помощью pip):
Запустим код и увидим, что модуль dill сериализует лямбда-функцию без ошибок:
Ещё одна особенность dill заключается в том, что он умеет сериализовать сеанс интерпретатора:
В этом примере после запуска интерпретатора и ввода нескольких выражений мы импортируем модуль dill и вызываем dump_session() для сериализации сеанса в файле test.pkl в текущем каталоге:
Запустим новый экземпляр интерпретатора и загрузим файл test.pkl для восстановления последнего сеанса:
Примечание
Прежде чем начать использовать dill вместо pickle, имейте в виду, что dill не включён в стандартную библиотеку Python и обычно работает медленнее, чем pickle.
Модуль dill охватывает гораздо более широкий диапазон объектов, чем pickle, но не решает всех проблем сериализации. К примеру, даже dill не может сериализовать объект, содержащий соединение с базой данных.
В подобных случаях нужно исключить несериализуемый объект из процесса сериализации и повторно инициализировать после десериализации.
Чтобы указать, что должно быть включено в процесс сериализации, нужно использовать метод __getstate__(). Если этот метод не переопределён, будет использоваться дефолтный __dict__().
В следующем примере показано, как можно определить класс с несколькими атрибутами и исключить один атрибут из сериализации с помощью __getstate__():
В приведённом примере мы создаём объект с тремя атрибутами. Поскольку один из атрибутов – это лямбда-объект, его нельзя обработать с помощью pickle. Поэтому в __getstate__() мы сначала клонируем весь __dict__, а затем удаляем несериализуемый атрибут с.
Если мы запустим этот пример, а затем десериализуем объект, то увидим, что новый экземпляр не содержит атрибут c:
Мы также можем выполнить дополнительные инициализации в процессе десериализации. Например, добавить исключённый объект c обратно в десериализованную сущность. Для этого используется метод __setstate__():
Сжатие сериализованных объектов
Формат данных pickle является компактным двоичным представлением структуры объекта, но мы всё равно можем её оптимизировать, используя сжатие. Для bzip2-сжатия сериализованной строки можно использовать модуль стандартной библиотеки bz2:
Безопасность отправки данных в формате pickle
Процесс сериализации удобен, когда необходимо сохранить состояние объекта на диск или передать по сети. Однако это не всегда безопасно. Как мы обсудили выше, при десериализации объекта в методе __setstate__() может выполняться любой произвольный код. В том числе код злоумышленника.
Простое правило гласит: никогда не десериализуйте данные, поступившие из подозрительного источника или ненадёжной сети. Чтобы предотвратить атаку посредника, используйте модуль стандартной библиотеки hmac для создания подписей и их проверки.
В следующем примере показано, как десериализация файла pickle, присланного злоумышленником, открывает доступ к системе:
В этом примере в процессе распаковки в __setstate__() будет выполнена команда Bash, открывающая удалённую оболочку для компьютера 192.168.1.10 через порт 8080.
Вы можете протестировать этот скрипт на Mac или Linux, открыв терминал и набрав команду nc для прослушивания порта 8080:
Это будет терминал атакующего. Затем открываем терминал на том же компьютере (или другом компьютере той же сети) и выполняем приведённый код Python. IP-адрес в коде нужно заменить на IP-адрес атакующего терминала. Выполнив следующую команду, жертва предоставит атакующему доступ:
При запуске скрипта жертвой в терминале злоумышленника оболочка Bash перейдёт в активное состояние:
Эта консоль позволить атакующему работать непосредственно на вашей системе.
Заключение
Теперь вы знаете, как работать с модулями pickle и dill для преобразования иерархии объектов со сложной структурой в поток байтов. Структуры можно сохранять на диск или передавать в виде байтовой строки по сети. Вы также знаете, что процесс десериализации нужно использовать с осторожностью. Если у вас остались вопросы, задайте их в комментарии под постом.