Сериализация и десериализация объектов Python — важный аспект любой нетривиальной программы. Если в Python вы что-то сохраняете в файл, читаете файл конфигурации или отвечаете на HTTP-запрос, вы выполняете сериализацию и десериализацию объекта.
В каком-то смысле сериализация и десериализация — самые скучные вещи в мире. Кого волнуют все форматы и протоколы? Вы просто хотите сохранить или передать некоторые объекты Python и вернуть их позже в целости и сохранности.
Это очень полезный способ взглянуть на мир на концептуальном уровне. Но на прагматическом уровне выбранная вами схема сериализации, формат или протокол могут определить, насколько быстро работает ваша программа, насколько она безопасна, сколько у вас есть свободы для поддержания своего состояния и насколько хорошо вы собираетесь взаимодействовать с другими системы.
Причина, по которой существует так много вариантов, заключается в том, что разные обстоятельства требуют разных решений. Не существует «одного размера для всех». В этом уроке, состоящем из двух частей, я расскажу о плюсах и минусах наиболее успешных схем сериализации и десериализации, покажу, как их использовать, и предоставлю рекомендации по выбору между ними при столкновении с конкретным вариантом использования.
Пример выполнения
В следующих разделах я сериализую и десериализую одни и те же графы объектов Python с использованием разных сериализаторов. Чтобы избежать повторения, я определю эти графы объектов здесь.
Простой граф объектов
Граф простых объектов — это словарь, содержащий список целых чисел, строку, число с плавающей запятой, логическое значение и значение None.
1
2
3
4
5
6
7
8
9
|
simple = dict(int_list=[1, 2, 3],
text=’string’,
number=3.44,
boolean=True,
none=None)
|
Граф сложного объекта
Граф сложных объектов также является словарем, но он содержит объект datetime
и пользовательский экземпляр класса, который имеет атрибут self.simple
, для которого установлен простой граф объектов.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
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))
|
Соленый огурец
Рассол является основным продуктом. Это родной формат сериализации объектов Python. Интерфейс pickle предоставляет четыре метода: dump, dumps, load и load. Метод dump()
сериализуется в открытый файл (файлоподобный объект). Метод dumps()
сериализуется в строку. Метод load()
десериализуется из открытого файлового объекта. Метод loads()
десериализуется из строки.
Pickle по умолчанию поддерживает текстовый протокол, но также имеет двоичный протокол, который более эффективен, но не удобен для чтения (полезно при отладке).
Вот как вы выбираете граф объекта Python в строку и в файл, используя оба протокола.
01
02
03
04
05
06
07
08
09
10
11
12
13
|
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 байтов.
01
02
03
04
05
06
07
08
09
10
11
|
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
|
Открепление от строки так же просто, как:
1
2
3
4
5
6
7
8
9
|
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
|
Обратите внимание, что рассол может выяснить протокол автоматически. Нет необходимости указывать протокол даже для двоичного.
Открепление от файла так же просто. Вам просто нужно предоставить открытый файл.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
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
|
Согласно документации, вы должны открывать бинарные соленья, используя режим ‘rb’, но, как вы можете видеть, это работает в любом случае.
Давайте посмотрим, как pickle работает с графом сложных объектов.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
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) является частью стандартной библиотеки Python начиная с Python 2.5. Я буду считать это родным форматом на данный момент. Это текстовый формат и неофициальный король Интернета в части сериализации объектов. Его система типов естественным образом моделирует JavaScript, поэтому она довольно ограничена.
Давайте сериализуем и десериализуем графы простых и сложных объектов и посмотрим, что произойдет. Интерфейс практически идентичен интерфейсу соления. У вас есть функции dump()
, dumps()
, load()
и load()
. Но нет никаких протоколов для выбора, и есть много необязательных аргументов для управления процессом. Давайте начнем с простого создания дампа простого графа объектов без каких-либо специальных аргументов:
1
2
3
4
5
|
import json
print json.dumps(simple)
{«text»: «string», «none»: null, «boolean»: true, «number»: 3.44, «int_list»: [1, 2, 3]}
|
Вывод выглядит довольно читабельным, но нет отступов. Для больших графов объектов это может быть проблемой. Давайте сделаем отступ в выводе:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
print json.dumps(simple, indent=4)
{
«text»: «string»,
«none»: null,
«boolean»: true,
«number»: 3.44,
«int_list»: [
1,
2,
3
]
}
|
Это выглядит намного лучше. Давайте перейдем к графу сложных объектов.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
|
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.
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
где ключ — это имя типа, окруженного дандерсом (двойное подчеркивание). Это будет важно для декодирования.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
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__}
|
Давайте попробуем еще раз с нашим пользовательским кодером:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
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 для своих имен, вам нужно придумать другое соглашение для обозначения специальных типов.
Давайте расшифруем граф сложного объекта.
1
2
3
4
5
|
> deserialized = json.loads(serialized)
> deserialized == complex
False
|
Хм, десериализация сработала (без ошибок), но она отличается от исходного графа сложных объектов, который мы сериализовали. Что-то не так. Давайте посмотрим на десериализованный граф объектов. Я буду использовать функцию pprint
модуля pprint
для красивой печати.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
> 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′}}
|
Ok. Проблема в том, что модуль json ничего не знает о классе A или даже о стандартном объекте datetime. Он просто десериализует все по умолчанию для объекта Python, который соответствует его системе типов. Чтобы вернуться к богатому графу объектов Python, вам нужно пользовательское декодирование.
Там нет необходимости для пользовательского подкласса декодера. Функции load()
и load()
предоставляют параметр «object_hook», который позволяет вам предоставить пользовательскую функцию, которая преобразует диктанты в объекты.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
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()
в качестве параметра для параметра load_ loads()
object_hook.
01
02
03
04
05
06
07
08
09
10
11
|
> 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, проблемах производительности и безопасности, а также кратко рассмотрите дополнительные схемы сериализации.
Выучить питон
Изучите Python с нашим полным руководством по питону, независимо от того, начинаете ли вы или начинающий программист, ищущий новые навыки.