Время от времени требуется сохранить на диск или отослать по сети объект со сложной структурой. Например, текущее состояние нейронной сети, находящейся в процессе обучения. Процесс перевода структуры данных в цепочку битов называется сериализацией.
После прочтения статьи вы будете знать:
- что такое сериализация и десериализация;
- как применять эти процессы для собственного удобства;
- какие существуют встроенные и сторонние библиотеки 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
для преобразования иерархии объектов со сложной структурой в поток байтов. Структуры можно сохранять на диск или передавать в виде байтовой строки по сети. Вы также знаете, что процесс десериализации нужно использовать с осторожностью. Если у вас остались вопросы, задайте их в комментарии под постом.
Комментарии