Глава 13. Сериализация объектов Python

Добавлено 14 июля 2020 в 21:24

Содержание главы

Погружение

На первый взгляд, идея сериализации проста. У вас есть структура данных в памяти, которую вы хотите сохранить, использовать повторно, или кому-либо отправить. Как бы вы это сделали? Это зависит от того, как вы хотите ее сохранить, как вы хотите повторно ее использовать, и кому вы хотите ее отправить. Многие игры позволяют вам сохранять ваш прогресс перед выходом, а при следующем запуске возобновлять игру с того же места (на самом деле, многие неигровые приложения также позволяют это делать). В этом случае, структура, которая хранит ваш прогресс в игре, должна быть сохранена на диске, когда вы выходите из игры, и загружена с диска, когда вы ее снова запускаете. Данные предназначены только для использования той же программой, что и создала их, они никогда не посылаются по сети и никогда не читаются ничем, кроме создавшей их программы. Поэтому проблемы совместимости ограничены тем, чтобы более поздние версии программы могли читать данные, созданные ранними версиями.

Для таких случаев идеально подходит модуль 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. Строка 1. Всё дальнейшее происходит в консоли Python #1.
  2. Строка 3. Идея в том, чтобы создать словарь, который будет представлять что-нибудь полезное, например, элемент entry в рассылке Atom. Но, чтобы раскрыть возможности модуля pickle, я хочу убедиться, что он содержит несколько разных типов данных. Не вчитывайтесь слишком сильно в эти переменные.
  3. Строка 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. Строка 1. Мы всё еще в первой консоли
  2. Строка 4. Чтобы открыть файл, используем функцию open(). Чтобы открыть файл для записи в двоичном режиме, установим режим работы с файлом в 'wb'. Обернем это всё в оператор with, чтобы быть уверенными, что, когда мы завершим с ним работу, файл закроется автоматически.
  3. Строка 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. Строка 1. Это вторая консоль Python.
  2. Строка 3. Здесь переменная entry не определена. Мы определили переменную entry в первой консоли Python, но это полностью другое окружение со своим собственным состоянием.
  3. Строка 8. Откроем файл entry.pickle, который мы создали в первой консоли Python. Модуль pickle использует двоичный формат данных, поэтому открывать pickle-файлы мы всегда должны в двоичном режиме.
  4. Строка 9. Функция pickle.load() принимает потоковый объект, читает сериализованные данные из потока, создает новый объект Python, восстанавливает сериализованные данные в этот новый объект Python, и возвращает этот новый объект Python.
  5. Строка 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. Строка 1. Переключаемся обратно в первую консоль Python.
  2. Строка 3. Открываем файл entry.pickle.
  3. Строка 4. Загружаем сериализованные данные в новую переменную entry2.
  4. Строка 6. Python подтверждает, что эти два словаря(entry и entry2) равны. В этой консоли мы создали entry с нуля, начиная с пустого словаря и вручную присваивая значения ключам. Мы сериализовали этот словарь и сохранили в файле entry.pickle. Теперь мы прочитали сериализованные данные из этого файла и создали идеальную копию исходной структуры данных.
  5. Строка 8. Равенство не значит идентичность. Я сказал, что мы создали идеальную копию исходной структуры данных, и это правда. Но это всё же копия.
  6. Строка 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
  1. Строка 3. Функция pickle.dumps() (обратите внимание на 's' в конце имени функции) выполняет такую же сериализацию, как и функция pickle.dump(). Но вместо того, чтобы принимать потоковый объект и записывать сериализованные данные на диск, она просто возвращает эти сериализованные данные
  2. Строка 4. Поскольку протокол pickle использует двоичный формат данных, функция pickle.dumps() возвращает объект типа bytes.
  3. Строка 6. Функция pickle.loads() (снова обратите внимание на 's' в конце имени функции) выполняет такую же десериализацию, как и функция pickle.load(). Но вместо того, чтобы принимать потоковый объект и читать сериализованные данные из файла, она принимает объект типа bytes, содержащий сериализованные данные, такие, какие возвращает функция pickle.dumps().
  4. Строка 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)                              ③
  1. Строка 3. Вместо того чтобы использовать уже имеющуюся структуру данных entry, мы собираемся создать новую структуру. Позже в этой главе мы увидим, что случится, когда мы попробуем кодировать в JSON более сложную структуру данных.
  2. Строка 10. JSON – это текстовый формат, что означает, что мы должны открыть файл в текстовом режиме и указать кодировку. Вы никогда не ошибетесь, используя UTF-8.
  3. Строка 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)                            ①
  1. Строка 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, которые полностью упущены из виду. Посмотрим, сможете ли вы заметить их:

ПримечанияJSONPython 3
objectdictionary
arraylist
stringstring
integerinteger
real numberfloat
*trueTrue
*falseFalse
*nullNone
* Все значения 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
  1. Строка 3. Хорошо, настало время вернуться к структуре данных entry. Там есть всё: логическое значение, значение None, строка, кортеж строк, объект bytes и структура time.
  2. Строка 12. Я знаю, что говорил это ранее, но повторюсь еще раз: JSON – это текстовый формат. Открывайте файлы JSON всегда в текстовом режиме с кодировкой UTF-8.
  3. Строка 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. Строка 1. Чтобы определить собственный "миниформат сериализации" для типов данных, не поддерживаемых JSON по умолчанию, просто определите функцию, которая принимает объект Python как параметр. Этот объект Python будет именно тем объектом, который функция json.dump() не сможет сериализовать сама – в данном случае, это объект bytes  b'\xDE\xD5\xB4\xF8'.
  2. Строка 2. Ваша специальная функция сериализации должна проверить тип объекта Python, который ей передала функция json.dump(). Это не строго обязательно, если ваша функция сериализует только один тип данных, но это делает понятным, какой случай покрывает данная функция, и делает более простым расширение, если позже вам понадобится сериализовать больше типов данных.
  3. Строка 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, и так далее.)
  4. Строка 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
  1. Строка 3. Модуль customserializer – это то, где мы только что определили функцию to_json() в предыдущем примере.
  2. Строка 4. Текстовый режим, UTF-8, бла-бла-бла... (Вы забудете! Я сам иногда забываю! И всё работает замечательно, пока в один момент не сломается, и тогда оно начинает ломаться еще эффектнее.)
  3. Строка 5. Это важный фрагмент: чтобы встроить нашу специальную функцию преобразования в функцию json.dump(), передаем нашу функцию в json.dump() в параметре default. (Ура, в Python всё является объектом!)
  4. Строка 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')
  1. Строка 4. Добавляя обработку в нашу существующую функцию customserializer.to_json(), мы должны проверить, является ли объект Python (с которым у функции json.dump() проблемы) объектом time.struct_time.
  2. Строка 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}
  1. Строка 3. Для демонстрации переключаемся на вторую консоль Python и удаляем структуру данных entry, которую создали ранее в этой главе при помощи модуля pickle.
  2. Строка 10. В простейшем случае, функция json.load() работает так же, как и функция pickle.load(). Передаем ей потоковый объект, а она возвращает новый объект Python.
  3. Строка 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
  1. Строка 2. Эта функция преобразования так же принимает один параметр и возвращает одно значение. Но параметр, который она принимает, – не строка, а объект Python – результат десереализации строки JSON в объект Python.
  2. Строка 3. Всё, что вам нужно, – это проверить, содержит ли данный объект ключ '__class__', который был создан функцией to_json(). Если да, то значение этого ключа '__class__', скажет нам, как декодировать это значение обратно в исходный тип данных Python.
  3. Строка 5. Чтобы декодировать строку времени, возвращаемую функцией time.asctime(), мы используем функцию time.strptime(). Эта функция принимает форматированную строку даты/времени (в определяемом формате, но по умолчанию этот формат совпадает с форматом по умолчанию функции time.asctime()) и возвращает time.struct_time.
  4. Строка 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}
  1. Строка 5. Чтобы добавить функцию from_json() в процесс десериализации, передаем ее функции json.load() в параметре object_hook. Функции, которые принимают функции; как удобно!
  2. Строка 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']
  1. Строка 7. Даже после встраивания функции to_json() в сериализацию и функции from_json() в десериализацию у нас всё еще нет идеальной копии исходной структуры данных. Почему?
  2. Строка 9. В исходной структуре данных entry значение по ключу 'tags' было кортежем из трех строк.
  3. Строка 11. Но в воссозданной структуре данных entry2 значение по ключу 'tags' – это список из трех строк. JSON не видит разницы между кортежами и списками; в нем есть только один похожий на список тип данных, массив, и модуль json при сериализации тихо преобразует и списки, и кортежи в массивы JSON. В большинстве случаев, вы можете игнорировать разницу между кортежами и списками, но об этом нужно помнить, когда работаете с модулем json.

13.12 Материалы для дальнейшего чтения

Много статей о модуле pickle ссылаются на cPickle. В Python 2 существует две реализации модуля pickle: одна написана на чистом Python, другая на C (но всё еще вызываемая из Python). В Python 3 эти два модуля были объединены, поэтому всегда следует использовать просто import pickle. Вы можете найти эти статьи полезными, но игнорируйте устаревшую информацию о cPickle.

О модуле pickle:

О формате JSON и модуле json:

О расширяемости pickle:

Источник:

  • Mark Pilgrim. Dive Into Python 3

Теги

JSONpicklePythonВысокоуровневые языки программированияОбучениеСериализация/десериализацияЯзыки программирования

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.