Как хранить объекты Python со сложной структурой
Простой способ сериализовать объекты Python – встроенный модуль pickle. Если его возможностей не хватает, на выручку придёт dill. Демонстрируем работу обеих библиотек.
Время от времени требуется сохранить на диск или отослать по сети объект со сложной структурой. Например, текущее состояние нейронной сети, находящейся в процессе обучения. Процесс перевода структуры данных в цепочку битов называется сериализацией.
После прочтения статьи вы будете знать:
- что такое сериализация и десериализация;
- как применять эти процессы для собственного удобства;
- какие существуют встроенные и сторонние библиотеки Python для сериализации;
- чем отличаются протоколы
pickle
; - в чём преимущество
dill
передpickle
; - как с помощью
dill
сохранить сессию интерпретатора; - можно ли сжать сериализованные данные;
- какие бывают проблемы с безопасностью процесса десериализации.
Сериализация в Python
Итак, сериализация (англ. serialization, marshalling) – это способ преобразования структуры данных в линейную форму, которую можно сохранить или передать по сети. Обратный процесс преобразования сериализованного объекта в исходную структуру данных называется десериализацией (англ. deserialization, unmarshalling).
В стандартной библиотеке Python три модуля позволяют сериализовать и десериализовать объекты:
Кроме того, Python поддерживает XML, который также можно применять для сериализации объектов.
Самый старый модуль из перечисленных – marshal
. Он используется для чтения и записи байт-кода модулей Python и .pyc
-файлов, создаваемых при импорте модулей Python. Хотя его и можно использовать для сериализации, делать это не рекомендуется.
Модуль json
обеспечивает работу со стандартными файлами JSON. Это широко используемый формат обмена данными, удобный для чтения и не зависящий от языка программирования. С помощью модуля json
вы можете сериализовать и десериализовать стандартные типы данных Python:
bool
dict
int
float
list
string
tuple
None
Наконец, ещё один встроенный способ сериализации и десериализации объектов в Python – модуль pickle
. Он отличается от модуля json
тем, что сериализует объекты в двоичном виде. То есть результат не может быть прочитан человеком. Кроме того, pickle
работает быстрее и позволяет сериализовать многие другие типы Python, включая пользовательские.
Внутри модуля pickle
Модуль pickle
содержит четыре основные функции:
pickle.dump(obj, file, protocol=None, *, fix_imports=True, buffer_callback=None)
pickle.dumps(obj, protocol=None, *, fix_imports=True, buffer_callback=None)
pickle.load(file, *, fix_imports=True, encoding="ASCII", errors="strict", buffers=None)
pickle.loads(bytes_object, *, fix_imports=True, encoding="ASCII", errors="strict", buffers=None)
Первые два метода применяются для сериализации, а два других – для обратного процесса. Разница между первыми двумя методами заключается в том, что dump
создаёт файл, содержащий результат сериализации, а dumps
– возвращает байтовую строку. То же самое относится к 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
для преобразования иерархии объектов со сложной структурой в поток байтов. Структуры можно сохранять на диск или передавать в виде байтовой строки по сети. Вы также знаете, что процесс десериализации нужно использовать с осторожностью. Если у вас остались вопросы, задайте их в комментарии под постом.