Russian (Pусский) translation by Ilya Nikov (you can also view the original English article)
Сериализация и десериализация объектов Python является важным аспектом любой нетривиальной программы. Если в Python вы сохраняете что-то в файле, если вы читаете файл конфигурации или отвечаете на HTTP-запрос, вы выполняете сериализацию и десериализацию объектов.
В некотором смысле, сериализация и десериализация - самые скучные вещи в мире. Кто заботится обо всех форматах и протоколах? Вы просто хотите сохранить или стримить некоторые объекты Python и вернуть их позже.
Это очень здоровый способ взглянуть на мир на концептуальном уровне. Но на прагматическом уровне, какая схема сериализации, формат или протокол, которые вы выбираете, могут определять, насколько быстро работает ваша программа, насколько она безопасна, насколько свободно вы должны поддерживать свое состояние и насколько хорошо вы собираетесь взаимодействовать с другими системы.
Причина в том, что существует так много вариантов, что разные обстоятельства требуют разных решений. Нет серебренной поли. В этом учебнике из двух частей я расскажу о преимуществах и недостатках самых успешных схем сериализации и десериализации, покажу, как их использовать, и дам рекомендации по выбору между ними, когда вы сталкиваетесь с конкретным вариантом использования.
Пример выполнения
В следующих разделах я буду сериализовать и десериализовать одни и те же графы объектов Python, используя различные сериализаторы. Чтобы избежать повторения, я определяю эти графы объектов здесь.
Простой Graph объект
Простой graph объект - это словарь, который содержит список целых чисел, строку, float, логическое и None.
simple = dict(int_list=[1, 2, 3], text='string', number=3.44, boolean=True, none=None)
Сложный Graph объект
Граф сложного объекта также является словарем, но он содержит объект datetime
и пользовательский экземпляр класса с атрибутом self.simple
, который устанавливается на простой граф объектов.
from datetime import datetime class A(object): def __init__(self, simple): self.simple = simple def __eq__(self, other): if not hasattr(other, 'simple'): return False return self.simple == other.simple def __ne__(self, other): if not hasattr(other, 'simple'): return True return self.simple != other.simple complex = dict(a=A(simple), when=datetime(2016, 3, 7))
Pickle
Pickle является основным продуктом. Это собственный формат сериализации объекта Python. Интерфейс pickle обеспечивает четыре метода: dump, dumps, load, и loads. Метод dump()
сериализует в открытый файл (файл-подобный объект). Метод dumps()
сериализует в строку. Метод load()
десериализует из открытого файлового объекта. Метод loads()
десериализует из строки.
Pickle поддерживает по умолчанию текстовый протокол, но имеет также двоичный протокол, который более эффективен, но не читается человеком (полезно при отладке).
Вот как вы распиливаете граф объекта Python на строку и в файл, используя оба протокола.
import cPickle as pickle pickle.dumps(simple) "(dp1\nS'text'\np2\nS'string'\np3\nsS'none'\np4\nNsS'boolean'\np5\nI01\nsS'number'\np6\nF3.4399999999999999\nsS'int_list'\np7\n(lp8\nI1\naI2\naI3\nas." pickle.dumps(simple, protocol=pickle.HIGHEST_PROTOCOL) '\x80\x02}q\x01(U\x04textq\x02U\x06stringq\x03U\x04noneq\x04NU\x07boolean\x88U\x06numberq\x05G@\x0b\x85\x1e\xb8Q\xeb\x85U\x08int_list]q\x06(K\x01K\x02K\x03eu.'
Бинарное представление может показаться большим, но это иллюзия из-за его представления. При сбрасывании в файл текстовый протокол составляет 130 байт, а бинарный протокол - всего 85 байт.
pickle.dump(simple, open('simple1.pkl', 'w')) pickle.dump(simple, open('simple2.pkl', 'wb'), protocol=pickle.HIGHEST_PROTOCOL) ls -la sim*.* -rw-r--r-- 1 gigi staff 130 Mar 9 02:42 simple1.pkl -rw-r--r-- 1 gigi staff 85 Mar 9 02:43 simple2.pkl
Выделение из строки так же просто, как:
x = pickle.loads("(dp1\nS'text'\np2\nS'string'\np3\nsS'none'\np4\nNsS'boolean'\np5\nI01\nsS'number'\np6\nF3.4399999999999999\nsS'int_list'\np7\n(lp8\nI1\naI2\naI3\nas.") assert x == simple x = pickle.loads('\x80\x02}q\x01(U\x04textq\x02U\x06stringq\x03U\x04noneq\x04NU\x07boolean\x88U\x06numberq\x05G@\x0b\x85\x1e\xb8Q\xeb\x85U\x08int_list]q\x06(K\x01K\x02K\x03eu.') assert x == simple
Обратите внимание, что pickle может автоматически определять протокол. Нет необходимости указывать протокол даже для двоичного.
Получить из файла так же просто. Вам просто нужно предоставить открытый файл.
x = pickle.load(open('simple1.pkl')) assert x == simple x = pickle.load(open('simple2.pkl')) assert x == simple x = pickle.load(open('simple2.pkl', 'rb')) assert x == simple
Согласно документации, вы должны открыть бинарные pickles, используя режим «rb», но, как видите, это работает в любом случае.
Посмотрим, как pickle справляется со сложным графом объектов.
pickle.dumps(complex) "(dp1\nS'a'\nccopy_reg\n_reconstructor\np2\n(c__main__\nA\np3\nc__builtin__\nobject\np4\nNtRp5\n(dp6\nS'simple'\np7\n(dp8\nS'text'\np9\nS'string'\np10\nsS'none'\np11\nNsS'boolean'\np12\nI01\nsS'number'\np13\nF3.4399999999999999\nsS'int_list'\np14\n(lp15\nI1\naI2\naI3\nassbsS'when'\np16\ncdatetime\ndatetime\np17\n(S'\\x07\\xe0\\x03\\x07\\x00\\x00\\x00\\x00\\x00\\x00'\ntRp18\ns." pickle.dumps(complex, protocol=pickle.HIGHEST_PROTOCOL) '\x80\x02}q\x01(U\x01ac__main__\nA\nq\x02)\x81q\x03}q\x04U\x06simpleq\x05}q\x06(U\x04textq\x07U\x06stringq\x08U\x04noneq\tNU\x07boolean\x88U\x06numberq\nG@\x0b\x85\x1e\xb8Q\xeb\x85U\x08int_list]q\x0b(K\x01K\x02K\x03eusbU\x04whenq\x0ccdatetime\ndatetime\nq\rU\n\x07\xe0\x03\x07\x00\x00\x00\x00\x00\x00\x85Rq\x0eu.' pickle.dump(complex, open('complex1.pkl', 'w')) pickle.dump(complex, open('complex2.pkl', 'wb'), protocol=pickle.HIGHEST_PROTOCOL) ls -la comp*.* -rw-r--r-- 1 gigi staff 327 Mar 9 02:58 complex1.pkl -rw-r--r-- 1 gigi staff 171 Mar 9 02:58 complex2.pkl
Эффективность двоичного протокола еще больше при использовании сложных объектов.
JSON
JSON (JavaScript Object Notation) является частью стандартной библиотеки Python с Python 2.5. На данный момент я буду считать его родным. Это текстовый формат и является неофициальным королем сети, поскольку идет сериализация объектов. Его система типов, естественно, моделирует JavaScript, поэтому она довольно ограничена.
Давайте сериализуем и десериализируем простые и сложные графы объектов и выясним, что происходит. Интерфейс почти идентичен интерфейсу pickle. У вас есть функции dump()
, dumps()
, load()
, и loads()
. Но нет выбора протоколов, и есть много дополнительных аргументов для управления процессом. Давайте начнем просто, сбросив простой граф объектов без каких-либо специальных аргументов:
import json print json.dumps(simple) {"text": "string", "none": null, "boolean": true, "number": 3.44, "int_list": [1, 2, 3]}
Результат выглядит довольно читаемым, но нет отступов. Для более крупного графа объектов это может быть проблемой. Давайте отпечатаем вывод:
print json.dumps(simple, indent=4) { "text": "string", "none": null, "boolean": true, "number": 3.44, "int_list": [ 1, 2, 3 ] }
Это выглядит намного лучше. Перейдем к графу сложных объектов.
json.dumps(complex) --------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-19-1be2d89d5d0d> in <module>() ----> 1 json.dumps(complex) /usr/local/Cellar/python/2.7.10/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/__init__.pyc in dumps(obj, skipkeys, ensure_ascii, check_circular, allow_nan, cls, indent, separators, encoding, default, sort_keys, **kw) 241 cls is None and indent is None and separators is None and 242 encoding == 'utf-8' and default is None and not sort_keys and not kw): --> 243 return _default_encoder.encode(obj) 244 if cls is None: 245 cls = JSONEncoder /usr/local/Cellar/python/2.7.10/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/encoder.pyc in encode(self, o) 205 # exceptions aren't as detailed. The list call should be roughly 206 # equivalent to the PySequence_Fast that ''.join() would do. --> 207 chunks = self.iterencode(o, _one_shot=True) 208 if not isinstance(chunks, (list, tuple)): 209 chunks = list(chunks) /usr/local/Cellar/python/2.7.10/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/encoder.pyc in iterencode(self, o, _one_shot) 268 self.key_separator, self.item_separator, self.sort_keys, 269 self.skipkeys, _one_shot) --> 270 return _iterencode(o, 0) 271 272 def _make_iterencode(markers, _default, _encoder, _indent, _floatstr, /usr/local/Cellar/python/2.7.10/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/encoder.pyc in default(self, o) 182 183 """ --> 184 raise TypeError(repr(o) + " is not JSON serializable") 185 186 def encode(self, o): TypeError: <__main__.A object at 0x10f367cd0> is not JSON serializable
Вау! Это не выглядит хорошо. Что случилось? Сообщение об ошибке состоит в том, что объект A не является сериализуемым JSON. Помните, что JSON имеет очень ограниченную систему типов и не может автоматически сериализовать пользовательские классы. Способ обращения к ней заключается в подклассе класса JSONEncoder, используемого модулем json, и реализации default()
, которая вызывается всякий раз, когда кодер JSON запускается в объект, который он не может сериализовать.
Задача пользовательского кодировщика состоит в том, чтобы преобразовать его в граф объектов Python, который кодировщик JSON способен кодировать. В этом случае у нас есть два объекта, для которых требуется специальная кодировка: объект datetime
и класс A. Следующий кодер выполняет задание. Каждый специальный объект преобразуется в dict
, где ключ - это имя типа, окруженного dunders (двойные подчеркивания). Это будет важно для декодирования.
from datetime import datetime import json class CustomEncoder(json.JSONEncoder): def default(self, o): if isinstance(o, datetime): return {'__datetime__': o.replace(microsecond=0).isoformat()} return {'__{}__'.format(o.__class__.__name__): o.__dict__}
Давайте попробуем еще раз с нашим пользовательским кодировщиком:
serialized = json.dumps(complex, indent=4, cls=CustomEncoder) print serialized { "a": { "__A__": { "simple": { "text": "string", "none": null, "boolean": true, "number": 3.44, "int_list": [ 1, 2, 3 ] } } }, "when": { "__datetime__": "2016-03-07T00:00:00" } }
Это прекрасно. Граф комплексного объекта был сериализован правильно, а исходная информация о типе компонентов была сохранена с помощью ключей: «__A__» и «__datetime__». Если вы используете dunders для ваших имен, вам нужно придумать другое соглашение для обозначения специальных типов.
Давайте расшифруем граф сложных объектов.
> deserialized = json.loads(serialized) > deserialized == complex False
Хм, десериализация работала (без ошибок), но она отличается от первоначального графа комплексного объекта, который мы сериализуем. Что-то не так. Давайте рассмотрим граф десериализованного объекта. Я использую функцию pprint
модуля pprint
для печати.
> from pprint import pprint > pprint(deserialized) {u'a': {u'__A__': {u'simple': {u'boolean': True, u'int_list': [1, 2, 3], u'none': None, u'number': 3.44, u'text': u'string'}}}, u'when': {u'__datetime__': u'2016-03-07T00:00:00'}}
Хорошо. Проблема в том, что модуль json ничего не знает о классе A или даже стандартном объекте datetime. Он просто десериализует все по умолчанию объекту Python, который соответствует его системе типов. Чтобы вернуться к богатому графику объектов Python, вам потребуется собственное декодирование.
Нет необходимости в подклассе пользовательского декодера. Функции load()
и loads()
предоставляют параметр "object_hook", который позволяет вам предоставить настраиваемую функцию, которая преобразует dicts в объекты.
def decode_object(o): if '__A__' in o: a = A() a.__dict__.update(o['__A__']) return a elif '__datetime__' in o: return datetime.strptime(o['__datetime__'], '%Y-%m-%dT%H:%M:%S') return o
Давайте расшифруем с помощью функции decode_object()
в качестве параметра параметра loads()
object_hook.
> deserialized = json.loads(serialized, object_hook=decode_object) > print deserialized {u'a': <__main__.A object at 0x10d984790>, u'when': datetime.datetime(2016, 3, 7, 0, 0)} > deserialized == complex True
Заключение
В первой части этого учебника вы узнали об общей концепции сериализации и десериализации объектов Python и изучили входы и выходы сериализации объектов Python с использованием Pickle и JSON.
Во второй части вы узнаете о YAML, проблемах производительности и безопасности и получите быстрый обзор дополнительных схем сериализации.