Глава 13. Сериализация объектов Python
Содержание главы
Погружение
На первый взгляд, идея сериализации проста. У вас есть структура данных в памяти, которую вы хотите сохранить, использовать повторно, или кому-либо отправить. Как бы вы это сделали? Это зависит от того, как вы хотите ее сохранить, как вы хотите повторно ее использовать, и кому вы хотите ее отправить. Многие игры позволяют вам сохранять ваш прогресс перед выходом, а при следующем запуске возобновлять игру с того же места (на самом деле, многие неигровые приложения также позволяют это делать). В этом случае, структура, которая хранит ваш прогресс в игре, должна быть сохранена на диске, когда вы выходите из игры, и загружена с диска, когда вы ее снова запускаете. Данные предназначены только для использования той же программой, что и создала их, они никогда не посылаются по сети и никогда не читаются ничем, кроме создавшей их программы. Поэтому проблемы совместимости ограничены тем, чтобы более поздние версии программы могли читать данные, созданные ранними версиями.
Для таких случаев идеально подходит модуль pickle
. Он входит в стандартную библиотеку Python и поэтому доступен всегда. Он быстр; большая его часть написана на C, как и сам интерпретатор Python. Он может сохранять произвольные сложные структуры данных Python.
Что может сохранять модуль pickle
?
- Все встроенные типы данных Python: логические значения, целые числа, числа с плавающей точкой, комплексные числа, строки, объекты
bytes
, массивы байтов иNone
. - Списки, кортежи, словари и множества, содержащие любую комбинацию встроенных типов данных
- Списки, кортежи, словари и множества, содержащие любую комбинацию списков, кортежей, словарей и множеств, содержащих любую комбинацию встроенных типов данных (и так далее, вплоть до максимального уровня вложенности, который поддерживает Python).
- Функции, классы и экземпляры классов.
Если для вас этого недостаточно, то модуль pickle
еще и расширяем. Если вас заинтересовала его расширяемость, то смотрите ссылки в разделе «Материалы для дальнейшего чтения» в конце главы.
13.1.1 Небольшое замечание о примерах в данной главе
Данная глава рассказывает об истории с двумя интерактивными оболочками (консолями) Python. Все примеры в этой главе – часть одной большей истории. По мере демонстрации работы с модулями pickle
и json
вам нужно будет переключаться назад и вперед между двумя оболочками Python.
Чтобы не запутаться, откройте интерактивную оболочку Python и определите следующую переменную:
>>> shell = 1
Оставьте это окно открытым. Теперь откройте еще одну консоль Python и определите следующую переменную:
>>> shell = 2
В данной главе я буду использовать переменную shell
, чтобы показать, какую именно интерактивную оболочку Python я использую в каждом примере.
13.2 Сохранение данных в файл pickle
Модуль pickle
работает со структурами данных. Давайте создадим одну.
>>> shell ①
1
>>> entry = {} ②
>>> entry['title'] = 'Dive into history, 2009 edition'
>>> entry['article_link'] = 'http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition'
>>> entry['comments_link'] = None
>>> entry['internal_id'] = b'\xDE\xD5\xB4\xF8'
>>> entry['tags'] = ('diveintopython', 'docbook', 'html')
>>> entry['published'] = True
>>> import time
>>> entry['published_date'] = time.strptime('Fri Mar 27 22:20:42 2009') ③
>>> entry['published_date']
time.struct_time(tm_year=2009, tm_mon=3, tm_mday=27, tm_hour=22, tm_min=20, tm_sec=42, tm_wday=4, tm_yday=86, tm_isdst=-1)
- Строка 1. Всё дальнейшее происходит в консоли Python #1.
- Строка 3. Идея в том, чтобы создать словарь, который будет представлять что-нибудь полезное, например, элемент
entry
в рассылке Atom. Но, чтобы раскрыть возможности модуляpickle
, я хочу убедиться, что он содержит несколько разных типов данных. Не вчитывайтесь слишком сильно в эти переменные. - Строка 11. Модуль
time
содержит структуру данных (struct_time
) для представления момента времени (с точностью до миллисекунд) и функции для работы с этими структурами. Функцияstrptime()
принимает форматированную строку и преобразует ее вstruct_time
. Это строка в стандартном формате, но вы можете контролировать ее при помощи кодов форматирования. Для более подробного описания загляните в описание модуляtime
.
Теперь у нас есть симпатичный словарь Python. Давайте сохраним его в файл.
>>> shell ①
1
>>> import pickle
>>> with open('entry.pickle', 'wb') as f: ②
... pickle.dump(entry, f) ③
...
- Строка 1. Мы всё еще в первой консоли
- Строка 4. Чтобы открыть файл, используем функцию
open()
. Чтобы открыть файл для записи в двоичном режиме, установим режим работы с файлом в 'wb
'. Обернем это всё в операторwith
, чтобы быть уверенными, что, когда мы завершим с ним работу, файл закроется автоматически. - Строка 5. Функция
dump()
модуляpickle
принимает сериализуемую структуру данных Python, сериализует ее в двоичный, Python-зависимый формат, используя последнюю версию протоколаpickle
, и сохраняет ее в открытый файл.
Последнее предложение было очень важным.
- Модуль
pickle
принимает структуру данных Python и сохраняет ее в файл. - Чтобы выполнить это, он сериализует структуру данных, используя формат данных под названием «протокол pickle».
- Протокол pickle зависит от Python; здесь нет гарантий совместимости с другими языками. Вы, возможно, не сможете взять только что созданный файл entry.pickle и сделать с ним что-либо полезное на Perl, PHP, Java или любом другом языке программирования.
- Модулем
pickle
может быть сериализована не любая структура данных Python. Протоколpickle
менялся несколько раз с добавлением новых типов данных в язык Python, но у него всё еще есть ограничения. - Как результат этих изменений, нет гарантии совместимости даже между разными версиями Python. Новые версии Python поддерживают старые форматы сериализации, но старые версии Python не поддерживают новые форматы (поскольку не поддерживают новые типы данных).
- Пока вы не укажете иное, функции модуля
pickle
будут использовать последнюю версию протоколаpickle
. Это сделано для уверенности в том, что вам будет предоставлена максимальная гибкость в типах данных, которые вы можете сериализовать, но это также значит, что полученный файл будет невозможно прочитать при помощи более старых версий Python, которые не поддерживают последнюю версию протоколаpickle
. - Последняя версия протокола
pickle
– это двоичный формат. Убедитесь, что открываете файлыpickle
в двоичном режиме, или данные при записи будут повреждены.
13.3 Загрузка данных из фала pickle
Теперь переключитесь во вторую консоль Python, т.е. не в ту, где вы создали словарь entry
.
>>> shell ①
2
>>> entry ②
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'entry' is not defined
>>> import pickle
>>> with open('entry.pickle', 'rb') as f: ③
... entry = pickle.load(f) ④
...
>>> entry ⑤
{'comments_link': None,
'internal_id': b'\xDE\xD5\xB4\xF8',
'title': 'Dive into history, 2009 edition',
'tags': ('diveintopython', 'docbook', 'html'),
'article_link':
'http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition',
'published_date': time.struct_time(tm_year=2009, tm_mon=3, tm_mday=27, tm_hour=22, tm_min=20, tm_sec=42, tm_wday=4, tm_yday=86, tm_isdst=-1),
'published': True}
- Строка 1. Это вторая консоль Python.
- Строка 3. Здесь переменная
entry
не определена. Мы определили переменнуюentry
в первой консоли Python, но это полностью другое окружение со своим собственным состоянием. - Строка 8. Откроем файл entry.pickle, который мы создали в первой консоли Python. Модуль
pickle
использует двоичный формат данных, поэтому открывать pickle-файлы мы всегда должны в двоичном режиме. - Строка 9. Функция
pickle.load()
принимает потоковый объект, читает сериализованные данные из потока, создает новый объект Python, восстанавливает сериализованные данные в этот новый объект Python, и возвращает этот новый объект Python. - Строка 11. Теперь переменная
entry
– это словарь со знакомыми ключами и значениями.
Результат цикла pickle.dump()
/pickle.load()
– это новая структура данных эквивалентная исходной структуре данных.
>>> shell ①
1
>>> with open('entry.pickle', 'rb') as f: ②
... entry2 = pickle.load(f) ③
...
>>> entry2 == entry ④
True
>>> entry2 is entry ⑤
False
>>> entry2['tags'] ⑥
('diveintopython', 'docbook', 'html')
>>> entry2['internal_id']
b'\xDE\xD5\xB4\xF8'
- Строка 1. Переключаемся обратно в первую консоль Python.
- Строка 3. Открываем файл entry.pickle.
- Строка 4. Загружаем сериализованные данные в новую переменную
entry2
. - Строка 6. Python подтверждает, что эти два словаря(
entry
иentry2
) равны. В этой консоли мы создалиentry
с нуля, начиная с пустого словаря и вручную присваивая значения ключам. Мы сериализовали этот словарь и сохранили в файле entry.pickle. Теперь мы прочитали сериализованные данные из этого файла и создали идеальную копию исходной структуры данных. - Строка 8. Равенство не значит идентичность. Я сказал, что мы создали идеальную копию исходной структуры данных, и это правда. Но это всё же копия.
- Строка 10. По причинам, которые станут ясны позже в этой главе, я хочу указать, что значение ключа '
tags
' – это кортеж, и значение 'internal_id
' – это объектbytes
.
13.4 Использование pickle
без файлов
Пример из предыдущего раздела показал, как сериализовать объект Python непосредственно в файл на диске. Но что, если файл вам не нужен? Вы можете сериализовать в объект bytes
в памяти.
>>> shell
1
>>> b = pickle.dumps(entry) ①
>>> type(b) ②
<class 'bytes'>
>>> entry3 = pickle.loads(b) ③
>>> entry3 == entry ④
True
- Строка 3. Функция
pickle.dumps()
(обратите внимание на 's' в конце имени функции) выполняет такую же сериализацию, как и функцияpickle.dump()
. Но вместо того, чтобы принимать потоковый объект и записывать сериализованные данные на диск, она просто возвращает эти сериализованные данные - Строка 4. Поскольку протокол
pickle
использует двоичный формат данных, функцияpickle.dumps()
возвращает объект типаbytes
. - Строка 6. Функция
pickle.loads()
(снова обратите внимание на 's' в конце имени функции) выполняет такую же десериализацию, как и функцияpickle.load()
. Но вместо того, чтобы принимать потоковый объект и читать сериализованные данные из файла, она принимает объект типаbytes
, содержащий сериализованные данные, такие, какие возвращает функцияpickle.dumps()
. - Строка 7. Конечный результат такой же: идеальная копия исходного словаря.
13.5 Байты и строки снова поднимают свои уродливые головы
Протокол pickle
существует уже много лет, и он развивался по мере развития самого Python'а. Сейчас существует четыре разных версии протокола pickle
.
- В Python 1.x было две версии протокола
pickle
, текстовый формат (версия 0) и двоичный формат (версия 1). - Python 2.3 добавил новый протокол
pickle
(версия 2), чтобы поддерживать новый функционал объектов классов Python. У него двоичный формат. - Python 3.0 добавил еще один протокол
pickle
(версия 3) с полной поддержкой объектов типаbytes
и байтовых массивов. У него так же двоичный формат.
О, смотрите, разница между строками и байтами снова поднимает свою уродливую голову (если вы удивлены, то были недостаточно внимательны). На практике это значит, что в то время, как Python 3 может читать данные сохраненные при помощи протокола версии 2, Python 2 не может читать данные сохраненные при помощи протокола версии 3.
13.6 Отладка файлов pickle
Как выглядит протокол pickle
? Давайте ненадолго отложим консоль Python и посмотрим, что внутри файла entry.pickle, который мы создали. Для не вооруженного взгляда он выглядит как тарабарщина.
you@localhost:~/diveintopython3/examples$ ls -l entry.pickle
-rw-r--r-- 1 you you 358 Aug 3 13:34 entry.pickle
you@localhost:~/diveintopython3/examples$ cat entry.pickle
comments_linkqNXtagsqXdiveintopythonqXdocbookqXhtmlq?qX publishedq?
XlinkXJhttp://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition
q Xpublished_dateq
ctime
struct_time
?qRqXtitleqXDive into history, 2009 editionqu.
Не слишком-то полезно. Вы можете видеть строки, но остальные типы данных выглядят как непечатаемые (или, по крайней мере, как нечитаемые) символы. Поля даже не разделены хотя бы табуляциями или пробелами. Это не тот формат, который вы бы захотели отлаживать вручную.
>>> shell
1
>>> import pickletools
>>> with open('entry.pickle', 'rb') as f:
... pickletools.dis(f)
0: \x80 PROTO 3
2: } EMPTY_DICT
3: q BINPUT 0
5: ( MARK
6: X BINUNICODE 'published_date'
25: q BINPUT 1
27: c GLOBAL 'time struct_time'
45: q BINPUT 2
47: ( MARK
48: M BININT2 2009
51: K BININT1 3
53: K BININT1 27
55: K BININT1 22
57: K BININT1 20
59: K BININT1 42
61: K BININT1 4
63: K BININT1 86
65: J BININT -1
70: t TUPLE (MARK at 47)
71: q BINPUT 3
73: } EMPTY_DICT
74: q BINPUT 4
76: \x86 TUPLE2
77: q BINPUT 5
79: R REDUCE
80: q BINPUT 6
82: X BINUNICODE 'comments_link'
100: q BINPUT 7
102: N NONE
103: X BINUNICODE 'internal_id'
119: q BINPUT 8
121: C SHORT_BINBYTES 'ÞÕ´ø'
127: q BINPUT 9
129: X BINUNICODE 'tags'
138: q BINPUT 10
140: X BINUNICODE 'diveintopython'
159: q BINPUT 11
161: X BINUNICODE 'docbook'
173: q BINPUT 12
175: X BINUNICODE 'html'
184: q BINPUT 13
186: \x87 TUPLE3
187: q BINPUT 14
189: X BINUNICODE 'title'
199: q BINPUT 15
201: X BINUNICODE 'Dive into history, 2009 edition'
237: q BINPUT 16
239: X BINUNICODE 'article_link'
256: q BINPUT 17
258: X BINUNICODE 'http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition'
337: q BINPUT 18
339: X BINUNICODE 'published'
353: q BINPUT 19
355: \x88 NEWTRUE
356: u SETITEMS (MARK at 5)
357: . STOP
highest protocol among opcodes = 3
Самая интересная часть информации в этом листинге находится в последней строке потому, что она в себя включает версию протокола pickle
, при помощи которого данный файл был сохранен. Явного маркера протокола pickle
не существует. Чтобы определить, какую версию протокола использовали для сохранения pickle-файла, вам необходимо посмотреть на маркеры("opcodes
") внутри сохраненных данных и использовать жестко заданную информацию, какие маркеры добавлялись с каждой версией протокола pickle
. Функция pickletools.dis()
делает именно это, и она выводит в печать результат в последней строке листинга. Ниже показана функция, которая возвращает только номер версии, без вывода данных.
Скачать файл pickleversion.py.
import pickletools
def protocol_version(file_object):
maxproto = -1
for opcode, arg, pos in pickletools.genops(file_object):
maxproto = max(maxproto, opcode.proto)
return maxproto
А вот она же в действии:
>>> import pickleversion
>>> with open('entry.pickle', 'rb') as f:
... v = pickleversion.protocol_version(f)
>>> v
3
13.7 Сериализация объектов Python для чтения при помощи других языков программирования
Формат данных, используемый модулем pickle
, является Python-зависимым. Он не пытается быть совместимым с другими языками программирования. Если среди ваших требований есть совместимость с другими языками программирования, то вам следует присмотреться к другим форматам сериализации. Один из таких форматов – JSON. «JSON» – это аббревиатура от «JavaScript Object Notation», но не позволяйте названию ввести вас в заблуждение – JSON определенно был разработан для использования несколькими языками программирования.
Python 3 включает в стандартную библиотеку модуль json
. Как и модуль pickle
, модуль json
имеет функции для сериализации структур данных, сохранения сериализованных данных на диск, загрузки сериализованных данных с диска и десереализации данных обратно в новый объект Python. Но есть несколько важных отличий. Во-первых, формат данных JSON – текстовый, а не двоичный. RFC 4627 определяет формат JSON и то, как различные типы данных должны быть преобразованы в текст. Например, логическое значение сохраняется как пятисимвольная строка 'false' или четырехсимвольная строка 'true'. Все значения в JSON регистрозависимые.
Во-вторых, как и с любым текстовым форматом, существует проблема пробельных символов. JSON позволяет вставлять между значениями произвольное количество пробельных символов (табуляций, символов возврата каретки и перевода строки). Пробельные символы в нем «незначащие», что означает, что кодировщики JSON могут добавлять так много или так мало пробельных символов, как захотят, а декодировщики JSON будут игнорировать эти пробельные символы между значениями. Это позволяет нам использовать красивую печать (pretty-print) для вывода данных в формате JSON, красиво печатая вложенные значения с различными размерами отступов, чтобы всё это было можно читать даже в текстовом редакторе. У модуля json
в Python имеется такая опция красивой печати при кодировании данных.
В-третьих, существует многолетняя проблема с кодировкой символов. JSON кодирует значения как обычный текст, но, как вы знаете, такого понятия как «обычный текст» не существует. JSON должен сохраняться в кодировке Unicode (UTF-32, UTF-16, или, по умолчанию, UTF-8), а раздел 3 из RFC 4627 определяет то, как указать используемую кодировку.
13.8 Сохранение данных в файл JSON
JSON выглядит удивительно похоже на структуру данных, которую вы могли бы определить вручную в JavaScript. Это не случайно, вы действительно можете использовать функцию eval()
из JavaScript чтобы «декодировать» сериализованные в json данные (обычные предостережения ненадежных входных данных принимаются, но дело в том, что JSON это корректный JavaScript). По сути, JSON может быть уже хорошо знаком вам.
>>> shell
1
>>> basic_entry = {} ①
>>> basic_entry['id'] = 256
>>> basic_entry['title'] = 'Dive into history, 2009 edition'
>>> basic_entry['tags'] = ('diveintopython', 'docbook', 'html')
>>> basic_entry['published'] = True
>>> basic_entry['comments_link'] = None
>>> import json
>>> with open('basic.json', mode='w', encoding='utf-8') as f: ②
... json.dump(basic_entry, f) ③
- Строка 3. Вместо того чтобы использовать уже имеющуюся структуру данных
entry
, мы собираемся создать новую структуру. Позже в этой главе мы увидим, что случится, когда мы попробуем кодировать в JSON более сложную структуру данных. - Строка 10. JSON – это текстовый формат, что означает, что мы должны открыть файл в текстовом режиме и указать кодировку. Вы никогда не ошибетесь, используя UTF-8.
- Строка 11. Как и модуль
pickle
, модульjson
определяет функциюdump()
, которая принимает структуру данных Python и записываемый потоковый объект. Функцияdump()
сериализует структуру данных Python и записывает ее в этот потоковый объект. Выполняя это в оператореwith
, мы можем быть уверены, что, когда мы завершим работу с файлом, он будет корректно закрыт.
Ну и как выглядит результат сериализации в формате JSON?
you@localhost:~/diveintopython3/examples$ cat basic.json
{"published": true, "tags": ["diveintopython", "docbook", "html"], "comments_link": null,
"id": 256, "title": "Dive into history, 2009 edition"}
Это определенно намного более читаемо, чем файл pickle. Но JSON между значениями может содержать произвольное количество пробельных символов, и модуль json
предоставляет простой способ для создания еще более читаемого файла JSON.
>>> shell
1
>>> with open('basic-pretty.json', mode='w', encoding='utf-8') as f:
... json.dump(basic_entry, f, indent=2) ①
- Строка 4. Если вы передадите функции
json.dump()
параметрindent
, это сделает конечный файл JSON более читаемым, увеличив при этом размер файла. Параметрindent
– это целое число. 0 означает, «расположить каждое значение на отдельной строке». Число больше нуля означает, «расположить каждое значение на отдельной строке, и использовать это количество пробелов для отступов во вложенных структурах данных».
Результат будет следующим:
you@localhost:~/diveintopython3/examples$ cat basic-pretty.json
{
"published": true,
"tags": [
"diveintopython",
"docbook",
"html"
],
"comments_link": null,
"id": 256,
"title": "Dive into history, 2009 edition"
}
13.9 Соответствие типов данных Python в JSON
Поскольку JSON разрабатывался не только для Python, то в покрытии типов данных Python есть некоторые недочеты. Некоторые из них – это просто отличия в названиях типов, но есть два важных типа данных Python, которые полностью упущены из виду. Посмотрим, сможете ли вы заметить их:
Примечания | JSON | Python 3 |
---|---|---|
object | dictionary | |
array | list | |
string | string | |
integer | integer | |
real number | float | |
* | true | True |
* | false | False |
* | null | None |
* Все значения JSON чувствительны к регистру. |
Заметили что пропущено? Кортежи и байты! В JSON есть тип массив, который модуль json
ставит в соответствие типу список в Python, но там нет отдельного типа для «статичных массивов» (кортежей). В JSON есть хорошая поддержка строк, но нет поддержки объектов типа bytes
или массивов байт.
13.10 Сериализация типов данных, не поддерживаемых JSON
Даже если в JSON нет встроенной поддержки байтов, не значит, что вы не можете сериализовать объекты типа bytes
. Модуль json
предоставляет расширяющие хуки для кодирования и декодирования неизвестных типов данных. (Под «неизвестными» я имел в виду «не определенные в JSON». Очевидно, что модуль json
знает о массивах байт, но он ограничен спецификации JSON.) Если вы хотите закодировать bytes
или другие типы данных, которые JSON по умолчанию не поддерживает, вам необходимо предоставить специальные кодировщики и декодировщики для этих типов данных.
>>> shell
1
>>> entry ①
{'comments_link': None,
'internal_id': b'\xDE\xD5\xB4\xF8',
'title': 'Dive into history, 2009 edition',
'tags': ('diveintopython', 'docbook', 'html'),
'article_link': 'http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition',
'published_date': time.struct_time(tm_year=2009, tm_mon=3, tm_mday=27, tm_hour=22, tm_min=20, tm_sec=42, tm_wday=4, tm_yday=86, tm_isdst=-1),
'published': True}
>>> import json
>>> with open('entry.json', 'w', encoding='utf-8') as f: ②
... json.dump(entry, f) ③
...
Traceback (most recent call last):
File "<stdin>", line 5, in <module>
File "C:\Python31\lib\json\__init__.py", line 178, in dump
for chunk in iterable:
File "C:\Python31\lib\json\encoder.py", line 408, in _iterencode
for chunk in _iterencode_dict(o, _current_indent_level):
File "C:\Python31\lib\json\encoder.py", line 382, in _iterencode_dict
for chunk in chunks:
File "C:\Python31\lib\json\encoder.py", line 416, in _iterencode
o = _default(o)
File "C:\Python31\lib\json\encoder.py", line 170, in default
raise TypeError(repr(o) + " is not JSON serializable")
TypeError: b'\xDE\xD5\xB4\xF8' is not JSON serializable
- Строка 3. Хорошо, настало время вернуться к структуре данных
entry
. Там есть всё: логическое значение, значениеNone
, строка, кортеж строк, объектbytes
и структураtime
. - Строка 12. Я знаю, что говорил это ранее, но повторюсь еще раз: JSON – это текстовый формат. Открывайте файлы JSON всегда в текстовом режиме с кодировкой UTF-8.
- Строка 13. Что ж, это не хорошо. Что произошло?
А вот что: функция json.dump()
попробовала сериализовать объект bytes
b'\xDE\xD5\xB4\xF8'
, но ей это не удалось, потому что в JSON нет поддержки объектов bytes
. Однако, если сохранение таких объектов важно для вас, вы можете определить свой "миниформат сериализации".
Скачать файл customserializer.py.
def to_json(python_object): ①
if isinstance(python_object, bytes): ②
return {'__class__': 'bytes',
'__value__': list(python_object)} ③
raise TypeError(repr(python_object) + ' is not JSON serializable') ④
- Строка 1. Чтобы определить собственный "миниформат сериализации" для типов данных, не поддерживаемых JSON по умолчанию, просто определите функцию, которая принимает объект Python как параметр. Этот объект Python будет именно тем объектом, который функция
json.dump()
не сможет сериализовать сама – в данном случае, это объектbytes
b'\xDE\xD5\xB4\xF8'
. - Строка 2. Ваша специальная функция сериализации должна проверить тип объекта Python, который ей передала функция
json.dump()
. Это не строго обязательно, если ваша функция сериализует только один тип данных, но это делает понятным, какой случай покрывает данная функция, и делает более простым расширение, если позже вам понадобится сериализовать больше типов данных. - Строка 4. В данном случае я решил конвертировать объект
bytes
в словарь. Ключ__class__
будет содержать название исходного типа данных (как строку, 'bytes'), а ключ__value__
будет хранить само значение. Конечно, это не может быть объект типаbytes
, поэтому нужно преобразовать его во что-то сериализуемое в JSON. Объектbytes
– это просто последовательность целых чисел, где каждое число находится в диапазоне от 0 до 255. Мы можем использовать функциюlist()
, чтобы преобразовать объектbytes
в список целых чисел. Итак,b'\xDE\xD5\xB4\xF8'
становится[222, 213, 180, 248]
. (Проверьте! Это работает! Байт \xDE в шестнадцатеричной системе – это 222 в десятичной, \xD5 – это 213, и так далее.) - Строка 5. Это строка важна. Структура данных, которую вы сериализуете может содержать типы данных, которые не могут обработать ни встроенный сериализатор JSON, ни ваш специальный сериализатор. В этом случае, ваш обработчик должен выбросить исключение
TypeError
, чтобы функцияjson.dump()
узнала, что ваш сериализатор не смог распознать тип данных.
Всё, делать больше ничего не нужно. В действительности, эта специальная функция сериализации возвращает словарь Python, а не строку. Вы не пишите сериализацию в JSON полностью сами, вы просто выполняете преобразование в поддерживаемый тип данных. Функция json.dump()
сделает всё остальное.
>>> shell
1
>>> import customserializer ①
>>> with open('entry.json', 'w', encoding='utf-8') as f: ②
... json.dump(entry, f, default=customserializer.to_json) ③
...
Traceback (most recent call last):
File "<stdin>", line 9, in <module>
json.dump(entry, f, default=customserializer.to_json)
File "C:\Python31\lib\json\__init__.py", line 178, in dump
for chunk in iterable:
File "C:\Python31\lib\json\encoder.py", line 408, in _iterencode
for chunk in _iterencode_dict(o, _current_indent_level):
File "C:\Python31\lib\json\encoder.py", line 382, in _iterencode_dict
for chunk in chunks:
File "C:\Python31\lib\json\encoder.py", line 416, in _iterencode
o = _default(o)
File "/Users/pilgrim/diveintopython3/examples/customserializer.py", line 12, in to_json
raise TypeError(repr(python_object) + ' is not JSON serializable') ④
TypeError: time.struct_time(tm_year=2009, tm_mon=3, tm_mday=27, tm_hour=22, tm_min=20, tm_sec=42, tm_wday=4, tm_yday=86, tm_isdst=-1) is not JSON serializable
- Строка 3. Модуль
customserializer
– это то, где мы только что определили функциюto_json()
в предыдущем примере. - Строка 4. Текстовый режим, UTF-8, бла-бла-бла... (Вы забудете! Я сам иногда забываю! И всё работает замечательно, пока в один момент не сломается, и тогда оно начинает ломаться еще эффектнее.)
- Строка 5. Это важный фрагмент: чтобы встроить нашу специальную функцию преобразования в функцию
json.dump()
, передаем нашу функцию вjson.dump()
в параметреdefault
. (Ура, в Python всё является объектом!) - Строка 19. Хорошо, это действительно работает. Но посмотрите на исключение. Теперь функция
json.dump()
больше не жалуется, что не может сериализовать объектbytes
. Теперь она жалуется на совершенно другой объект:time.struct_time
.
Хоть получение другого исключения и не выглядит как прогресс, но на самом деле это он! Нужно просто внести еще одну корректировку, чтобы обрабатывался и этот тип.
import time
def to_json(python_object):
if isinstance(python_object, time.struct_time): ①
return {'__class__': 'time.asctime',
'__value__': time.asctime(python_object)} ②
if isinstance(python_object, bytes):
return {'__class__': 'bytes',
'__value__': list(python_object)}
raise TypeError(repr(python_object) + ' is not JSON serializable')
- Строка 4. Добавляя обработку в нашу существующую функцию
customserializer.to_json()
, мы должны проверить, является ли объект Python (с которым у функцииjson.dump()
проблемы) объектомtime.struct_time
. - Строка 6. Если это так, то выполняем нечто, похожее на преобразование, которое мы выполняли с объектом
bytes
: преобразуем объектtime.struct_time
в словарь, который содержит только сериализуемые в JSON типы данных. В данном случае, простейший способ преобразовать дату/время в сериализуемое в JSON значение – это преобразовать ее в строку с помощью функцииtime.asctime()
. Функцияtime.asctime()
преобразует ужасно выглядящую структуруtime.struct_time
в строку 'Fri Mar 27 22:20:42 2009'.
С этими двумя специальными преобразованиями, вся структура данных entry
должна сериализоваться в JSON без каких-либо проблем.
>>> shell
1
>>> with open('entry.json', 'w', encoding='utf-8') as f:
... json.dump(entry, f, default=customserializer.to_json)
...
you@localhost:~/diveintopython3/examples$ ls -l example.json
-rw-r--r-- 1 you you 391 Aug 3 13:34 entry.json
you@localhost:~/diveintopython3/examples$ cat example.json
{"published_date": {"__class__": "time.asctime", "__value__": "Fri Mar 27 22:20:42 2009"},
"comments_link": null, "internal_id": {"__class__": "bytes", "__value__": [222, 213, 180, 248]},
"tags": ["diveintopython", "docbook", "html"], "title": "Dive into history, 2009 edition",
"article_link": "http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition",
"published": true}
13.11 Загрузка данных из файла JSON
Как и в модуле pickle
, в модуле json
есть функция load()
, которая принимает потоковый объект, читает из него данные в формате JSON и создает новый объект Python, который будет копией структуры данных в JSON.
>>> shell
2
>>> del entry ①
>>> entry
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'entry' is not defined
>>> import json
>>> with open('entry.json', 'r', encoding='utf-8') as f:
... entry = json.load(f) ②
...
>>> entry ③
{'comments_link': None,
'internal_id': {'__class__': 'bytes', '__value__': [222, 213, 180, 248]},
'title': 'Dive into history, 2009 edition',
'tags': ['diveintopython', 'docbook', 'html'],
'article_link': 'http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition',
'published_date': {'__class__': 'time.asctime', '__value__': 'Fri Mar 27 22:20:42 2009'},
'published': True}
- Строка 3. Для демонстрации переключаемся на вторую консоль Python и удаляем структуру данных
entry
, которую создали ранее в этой главе при помощи модуляpickle
. - Строка 10. В простейшем случае, функция
json.load()
работает так же, как и функцияpickle.load()
. Передаем ей потоковый объект, а она возвращает новый объект Python. - Строка 12. У меня хорошие и плохие новости. Сначала хорошие: функция
json.load()
успешно прочитала файл entry.json, который мы создали в первой консоли Python, и создала новый объект Python, который содержит данные. А теперь плохие: она не воссоздала исходную структуру данныхentry
. Два значения ('internal_id
' и 'published_date
') были созданы как словари – а именно, как словари со значениями, совместимыми с JSON (именно их мы создали в функции преобразованияto_json()
).
Функция json.load()
ничего не знает ни о какой функции преобразования, которую мы могли передать в json.dump()
. Теперь нам нужно создать функцию, обратную to_json()
, – функцию, которая примет специально преобразованный объект JSON и преобразует его обратно в исходный тип данных Python.
# добавьте это в customserializer.py
def from_json(json_object): ①
if '__class__' in json_object: ②
if json_object['__class__'] == 'time.asctime':
return time.strptime(json_object['__value__']) ③
if json_object['__class__'] == 'bytes':
return bytes(json_object['__value__']) ④
return json_object
- Строка 2. Эта функция преобразования так же принимает один параметр и возвращает одно значение. Но параметр, который она принимает, – не строка, а объект Python – результат десереализации строки JSON в объект Python.
- Строка 3. Всё, что вам нужно, – это проверить, содержит ли данный объект ключ '
__class__
', который был создан функциейto_json()
. Если да, то значение этого ключа '__class__
', скажет нам, как декодировать это значение обратно в исходный тип данных Python. - Строка 5. Чтобы декодировать строку времени, возвращаемую функцией
time.asctime()
, мы используем функциюtime.strptime()
. Эта функция принимает форматированную строку даты/времени (в определяемом формате, но по умолчанию этот формат совпадает с форматом по умолчанию функцииtime.asctime()
) и возвращаетtime.struct_time
. - Строка 7. Для преобразования списка целых чисел обратно в объект
bytes
мы можем использовать функциюbytes()
.
Вот и всё. Всего два типа данных обрабатывались функцией to_json()
, и теперь эти типы данных обрабатываются функцией from_json()
. Ниже показан результат:
>>> shell
2
>>> import customserializer
>>> with open('entry.json', 'r', encoding='utf-8') as f:
... entry = json.load(f, object_hook=customserializer.from_json) ①
...
>>> entry ②
{'comments_link': None,
'internal_id': b'\xDE\xD5\xB4\xF8',
'title': 'Dive into history, 2009 edition',
'tags': ['diveintopython', 'docbook', 'html'],
'article_link': 'http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition',
'published_date': time.struct_time(tm_year=2009, tm_mon=3, tm_mday=27, tm_hour=22, tm_min=20, tm_sec=42, tm_wday=4, tm_yday=86, tm_isdst=-1),
'published': True}
- Строка 5. Чтобы добавить функцию
from_json()
в процесс десериализации, передаем ее функцииjson.load()
в параметреobject_hook
. Функции, которые принимают функции; как удобно! - Строка 7. Структура данных
entry
теперь содержит ключ 'internal_id
' со значением типаbytes
. Также она содержит ключ 'published_date
' со значением типаtime.struct_time
.
Хотя остался еще один глюк.
>>> shell
1
>>> import customserializer
>>> with open('entry.json', 'r', encoding='utf-8') as f:
... entry2 = json.load(f, object_hook=customserializer.from_json)
...
>>> entry2 == entry ①
False
>>> entry['tags'] ②
('diveintopython', 'docbook', 'html')
>>> entry2['tags'] ③
['diveintopython', 'docbook', 'html']
- Строка 7. Даже после встраивания функции
to_json()
в сериализацию и функцииfrom_json()
в десериализацию у нас всё еще нет идеальной копии исходной структуры данных. Почему? - Строка 9. В исходной структуре данных
entry
значение по ключу 'tags
' было кортежем из трех строк. - Строка 11. Но в воссозданной структуре данных
entry2
значение по ключу 'tags
' – это список из трех строк. JSON не видит разницы между кортежами и списками; в нем есть только один похожий на список тип данных, массив, и модульjson
при сериализации тихо преобразует и списки, и кортежи в массивы JSON. В большинстве случаев, вы можете игнорировать разницу между кортежами и списками, но об этом нужно помнить, когда работаете с модулемjson
.
13.12 Материалы для дальнейшего чтения
Много статей о модуле pickle
ссылаются на cPickle. В Python 2 существует две реализации модуля pickle
: одна написана на чистом Python, другая на C (но всё еще вызываемая из Python). В Python 3 эти два модуля были объединены, поэтому всегда следует использовать просто import pickle
. Вы можете найти эти статьи полезными, но игнорируйте устаревшую информацию о cPickle.
О модуле pickle
:
- pickle module
- pickle and cPickle — Python object serialization
- Using pickle
- Python persistence management
О формате JSON и модуле json
:
- json — JavaScript Object Notation Serializer
- JSON encoding and decoding with custom objects in Python
О расширяемости pickle
: