Погружение в Python 3 (Пилгрим)/Сериализация объектов Python

Материал из Викитеки — свободной библиотеки
Перейти к: навигация, поиск

Погружение в Python 3 — Сериализация объектов Python
автор Марк Пилгрим, пер. Shchemelevev
Язык оригинала: английский. Название в оригинале: Dive into Python 3. — См. Погружение в Python 3 (Пилгрим). Источник: Mark Pilgrim. Dive into Python 3. — 2009. — ISBN 978-1430224150; diveintopython3.org


Погружение[править]

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

Для таких случаев модуль pickle идеален. Это часть стандартной библиотеки Python, поэтому он всегда доступен. Он быстрый, большая часть написана на C, как и сам интерпретатор Python. Он может сохранять совершенно произвольные комплексные структуры данных Python.

Что может сохранять модуль pickle?

  • Все встроенные типы данных Python: тип boolean, Integer, числа с плавающей точкой, комплексные числа, строки, объекты bytes, массивы байт, и None.
  • Списки, кортежи, словари и множества, содержащие любую комбинацию встроенных типов данных
  • Списки, кортежи, словари и множества, содержащие любую комбинацию списков, кортежей, словарей и множеств содержащий любую комбинацию встроенных типов данных (и так далее, вплоть до максимального уровня вложенности, который поддерживает Python).
  • Функции, классы и экземпляры классов (с caveats).

Если для вас этого мало, то модуль pickle еще и расширяем. Если вам интересна эта возможность, то смотрите ссылки в разделе «Дальнейшее чтение» в конце этой главы.

Маленькая заметка о примерах в этой главе.[править]

Эта часть повествует о двух Python консолях. Все примеры в этой главе — часть одной большей истории. Вам нужно будет переключаться назад и вперед между двумя консолями для демонстрации модулей pickle и json.

Для того чтобы не запутаться откройте консоль Python и определите следующую переменную:

>>> shell = 1

Оставьте это окно открытым. И откройте еще одну консоль Python и определите следующую переменную:

>>> shell = 2

В этой главе я буду использовать переменную shell для того чтобы показать какую именно консоль Python я использую в каждом примере.

Сохранение данных в файл 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)

① Все дальнейшее происходит в консоли Python #1.

② Идея в том чтобы создать словарь, который будет представлять что-нибудь полезное, например элемент рассылки Atom. Также я хочу быть уверенным, что он содержит несколько разных типов данных, чтобы раскрыть возможности модуля pickle. Не вчитывайтесь слишком сильно в эти переменные.

③ Модуль time содержит структуру данных (struct_time) для представления момента времени (вплоть до миллисекунд) и функции для работы с этими структурами. Функция strptime() принимает на вход форматированную строку и преобразует ее в struct_time. Эта строка в стандартном формате, но вы можете контролировать ее при помощи кодов форматирования. Для более подробного описания загляните в модуль time.

Теперь у нас есть замечательный словарь. Давайте сохраним его в файл.

>>> shell                                    ①
1
>>> import pickle
>>> with open('entry.pickle', 'wb') as f:    ②
...     pickle.dump(entry, f)                ③
...

① Мы все еще в первой консоли

② Используйте функцию open() для того чтобы открыть файл. Установим режим работы с файлом в 'wb' для того чтобы открыть файл для записи в двоичном режиме. Обернем его в конструкцию with для того чтобы быть уверенным в том что файл закроется автоматически, когда вы завершите работу с ним.

③ Функция dump() модуля pickle принимает сериализуемую структуру данных Python, сериализует ее в двоичный, Python-зависимый формат использует последнюю версию протокола pickle и сохраняет ее в открытый файл.

Последнее предложение было очень важным.

  • Протокол pickle зависит от Python; здесь нет гарантий совместимости с другими языками. Вы возможно не сможете взять entry.pickle файл, который только что сделали и как — либо с пользой его использовать при помощи Perl, PHP, Java или любого другого языка программирования
  • Не всякая структура данных Python может быть сериализована модулем Pickle. Протокол pickle менялся несколько раз с добавлением новых типов данных в язык Python, и все еще у него есть ограничения.
  • Как результат, нет гарантии совместимости между разными версиями Python. Новые версии Python поддерживают старые форматы сериализации, но старые версии Python не поддерживают новые форматы (поскольку не поддерживают новые форматы данных)
  • Пока вы не укажете иное, функции модуля pickle будут использовать последнюю версию протокола pickle. Это сделано для уверенности в том, что вы имеете наибольшую гибкость в типах данных, которые вы можете сериализовать, но это также значит, что результирующий файл будет невозможно прочитать при помощи старых версий Python, которые не поддерживают последнюю версию протокола pickle.
  • Последняя версия протокола pickle это двоичный формат. Убедитесь, что открываете файлы pickle в двоичном режиме, или данные будут повреждены при записи.

Загрузка данных из файла 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}

① Это вторая консоль Python

② Здесь не определена переменная entry. Вы определяли переменную entry в первой консоли Python, но это полностью отличное окружение со своим собственным состоянием.

③ Откроем entry.pickle файл, который вы создали в первой консоли Python. Модуль pickle использует двоичный формат представления данных, поэтому вам всегда нужно открывать файл в двоичном режиме.

④ Функция pickle.load() принимает на вход поток, читает сериализованные данные из потока, создает новый объект Python, восстанавливает сериализованные данные в новый объект Python, и возвращает новый объект Python.

⑤ Теперь переменная 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'

① Переключитесь обратно в первую консоль Python.

② Откройте entry.pickle файл

③ Загрузите сериализованные данные в новую переменную entry2

④ Python подтверждает, что эти два словаря(entry и entry2) эквивалентны. В этой консоли вы создали entry с нуля, начиная с пустого словаря вручную присваивая значения ключам. Вы сериализовали этот словарь и сохранили в файле entry.pickle. Теперь вы считали сериализованные данные из этого фала и создали совершенную копию оригинальной структуры.

⑤ Эквивалентность не значит идентичности. Я сказал, что вы создали _идеальную копию_ оригинальной структуры данных, и это правда. Но это все же копия.

⑥ По причинам которые станут ясны в дальнейшем, я хочу указать, что значения ключа 'tags' это кортеж, и значение 'internal_id' это объект bytes.

Start hand.svg

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

Используем Pickle без файлов[править]

Пример из предыдущей секции показал как сериализовать объект напрямую в файл на диске. Но что если он вам не нужен или вы не хотели использовать файл? Вы можете сериализовать в объект bytes в памяти.

>>> shell
1
>>> b = pickle.dumps(entry)     ①
>>> type(b)                     ②
<class 'bytes'>
>>> entry3 = pickle.loads(b)    ③
>>> entry3 == entry             ④
True

① Функция pickle.dumps() (обратите внимание на 's' в конце имени функции) делает ту же самую сериализацию что и функция pickle.dump(). Вместо того чтобы принимать на вход поток и писать сериализованные данные на диск, она просто возвращает сериализованные данные

② Поскольку протокол pickle использует двоичный формат данных, функция pickle.dumps() возвращает объект типа bytes.

③ Функция pickle.loads() (снова заметьте 's' в конце имени функции) делает ту же самую десериализацию что и функция pickle.load(). Но вместо того чтобы принимать на вход поток и читать сериализованные данные из файла, она принимает на вход объект типа bytes содержащий сериализованные данные, такие как возвращаемые функцией pickle.dumps()

④ Конечный результат таков же: идеальная копия оригинального словаря.

Байты и строки снова вздымают свои уродливые головы[править]

Протокол pickle существует уже много лет, и он развивался вместе с тем как развивался сам Python. Сейчас существует четыре различных версии протокола pickle.

  • Python 1.x породил две версии протокола, основанный на тексте формат (версия 0) и двоичный формат (версия 1)
  • Python 2.3 ввел новый протокол pickle(версия 2) для того чтобы поддерживать новый функционал в классах Python. Он двоичный.
  • Python 3.0 ввел еще один протокол pickle(версия 3) с полной поддержкой объектов типа bytes и массивов байт. Он так же двоичный.

Вау смотрите, разница между строками и байтами снова вздымает свою уродливую голову. (Если вы удивлены, вы не уделяли достаточно внимания.) На практике это значит, что в то время, как Python 3 может читать данные сохраненные при помощи протокола версии 2, Python 2 не может читать данные сохраненные при помощи протокола версии 3.

Отладка файлов 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, вам необходимо заглянуть в маркеры(«opcodes») внутри сохраненных данных и использовать вшитую информацию о том какие маркеры были введены, в какой версии протокола Pickle. Функция pickletools.dis() делает именно это, и она печатает результат в последней строке дизассемблированного вывода. Вот функция, которая возвращает только номер версии, без вывода данных:

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

Сериализация объектов 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 определяет то, как указать используемую кодировку.

Сохранение данных в файл 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)                              ③

① Мы собираемся создать новую структуру данных вместо того чтобы использовать уже имеющуюся структуру данных entry. Позже в этой главе мы увидим что случится, когда мы попробуем кодировать более общую структуру данных в JSON.

② JSON это текстовый формат, это значит, что вы должны открыть файл в текстовом режиме и указать кодировку. Вы никогда не ошибетесь, используя UTF-8.

③ Как и модуль 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)                            ①

① Если вы передадите параметр ident функции Json.dump() она сделает результирующий файл json более читаемым в ущерб размеру файла. Параметр ident это целое число. 0 значит «расположить каждое значение на отдельной строке». Число больше 0 значит «расположить каждое значение на отдельной строке, и использовать number пробелов для отступов во вложенных структурах данных».

И вот результат:

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"
}

Соответствие типов данных 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
* Текст ячейки Текст ячейки
  • - Все переменные в JavaScript регистрозависимые

Вы заметили что потеряно? Кортежи и байты! В JSON есть тип - массив, который модуль JSON ставит в соответствие типу список в Python, но там нет отдельного типа для "статичных массивов» (кортежей). И также в JSON есть хорошая поддержка строк, но нет поддержки объектов типа bytes или массивов байт.

Сериализация типов данных не поддерживаемых JSON[править]

То что в JSON нет встроенной поддержки типа bytes, не значит что вы не сможете сериализовать объекты типа 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

① Хорошо, настало время вновь обратиться к структуре данных entry. Там есть все: логические значения, пустое значение, строка, кортеж строк, объект типа bytes, и структура хранящая время.

② Я знаю, что говорил это ранее, но повторюсь еще раз: json это текстовый формат. Всегда открывайте файлы json в текстовом режиме с кодировкой utf-8.

③Чтож... _ЭТО_ не хорошо. Что произошло?

А вот что: функция json.dump() попробовала сериализовать объект bytes b'\xDE\xD5\xB4\xF8', но ей не удалось, потому что в json нет поддержки объектов bytes. Однако, если сохранение таких объектов важно для вас, вы можете определить свой "мини формат сериализации".

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')  ④

① Чтобы определить свой собственный "мини формат сериализации" для тех типов данных, что json не поддерживает из коробки, просто определите функцию, которая принимает объект Python как параметр. Этот объект Python будет именно тем объектом, который функция json.dump() не сможет сериализовать сама - в данном случае это объект bytes b'\xDE\xD5\xB4\xF8'

② Вашей специфичной функции сериализации следует проверять тип объектов Python, которые передала ей функция json.dump(). Это не обязательно, если ваша функция сериализует только один тип данных, но это делает кристально ясным какой случай покрывает данная функция, и делает более простым улучшение функции, если вам понадобится сериализовать больше типов данных позже

③ В данном случае я решил конвертировать объект bytes в словарь. Ключ __class__ будет содержать название оригинального типа данных, а ключ __value__ будет хранить само значение. Конечно, это не может быть объекты типа bytes, поэтому нужно преобразовать его во что-нибудь сериализуемое при помощи json. Объекты типа bytes это просто последовательность чисел, каждое число будет где-то от 0 до 255. Мы можем использовать функцию list() чтобы преобразовать объект bytes в список чисел. Итак b'\xDE\xD5\xB4\xF8' становится [222, 213, 180, 248]. (Посчитайте! Это работает! Байт \xDE в шестнадцатеричной системе это 222 в десятичной, \xD5 это 213, и так далее.)

④ Это строка важна. Структура данных, которую вы сериализуете может содержать типы данных которых нет в json, и которые не обрабатывает ваша функция. В таком случае, ваш обработчик должен raise ошибку 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

① Модуль customserializer, это то где вы только что определили функцию to_json() в предыдущем примере

② Текстовый режим, utf-8, тра-ля-ля. (Вы забудете! Я иногда забываю! И все работает замечательно, пока в один момент не сломается, и тогда оно начинает ломаться еще театральнее)

③ Это важный кусок: чтобы встроить вашу функцию обработчик преобразования в функцию json.dump() передайте вашу функцию в json.dump() в параметре default. (Ура, все в Python - объект!)

④ Замечательно, это и правда работает. Но посмотрите на исключение. Теперь функция 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')

① Добавляя в уже существующую функцию customserializer.to_json() мы должны проверить, что объект Python(с которым у функции json.dump() проблемы) на самом деле time.struct_time.

② Если так, мы сделаем нечто похожее на конвертацию, что мы делали с объектом bytes: преобразуем объект time.struct_time в словарь который содержит только те типы данных что можно сериализовать в json. В данном случае, простейший путь преобразовать дату в значение которое можно сериализовать в json это преобразовать ее к строке при помощи функции time.asctime(). Функция time.asctime() преобразует отвратительно выглядящую time.struct_time в строку 'Fri Mar 27 22:20:42 2009'.

С этими двумя особыми преобразованиями, структура данных entry должна сериализовать полность без каких либо проблем.

>>> 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}

Загрузка данных из файла 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}

① Для демонстрации переключитесь во вторую консоль Python и удалите структуру данных entry которую вы создали ранее в этой главе при помощи модуля Pickle.

② В простейшем случае функция json.load() работает так же как и функция pickle.load(). Вы передаете ей объект потока, а получаете в результате новый объект Python.

③ У меня есть хорошие и плохие новости. Хорошие новости в том, что функция json.load() успешно прочитала файл entry.json, который вы создали в первой консоли Python и создала новый объект Python, который содержит данные. А теперь плохие новости: она не воссоздала оригинальную структуру данных entry. Два значения 'internal_id' и 'published_date' были созданы как словари - а именно, как словари со значениями которые совместимы с json (именно их вы создали в функции преобразования to_json() )

Функция json.load() ничего не знает о функции преобразования, которую вы могли передать в json.dump(). Теперь вам нужно создать функцию, обратную to_json() — функцию, которая примет выборочно преобразованный json объект и преобразует его обратно в оригинальный объект Python.

# add this to 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

① Эта функция преобразования так же принимает один параметр и возвращает одно значение. Но параметр который она принимает - не строка, это объект Python - результат десереализации строки JSON, в которую был преобразован объект Python.

② Все что вам нужно, так это проверить, содержит ли данный объект ключ '__class__', который создала функция to_json(). Если так, то значение найденное по этому ключу, расскажет вам, как декодировать этот объект обратно в оригинальный объект Python

③ Чтобы декодировать строку времени возвращаемую функцией time.asctime() вам нужно использовать функцию time.strptime(). Эта функция принимает параметром форматированную строку времени(в определяемом формате, но по умолчанию этот формат совпадает с форматом time.asctime()) и возвращает time.struct_time

④ Для преобразования списка чисел обратно в объекты 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}

① Чтобы встроить функцию from_json() в процесс десериализации, передайте ее в параметре object_hook в вызове функции json.load(). Функции которые принимают функции, как удобно!

② Структура данных 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']

① Даже после встраивания функции to_json() в сериализацию и функции from_json() в десериализацию мы все еще не получили полную копию оригинальной структуры данных. Почему нет?

② В оригинальной структуре данных entry значение по ключу 'tags' было кортежем строк.

③ Но в воссозданной структуре данных entry2 значение по ключу 'tags' - это список строк. JSON не видит различий между кортежами и списками, в нем есть только один похожий на список тип данных, массив, и модуль json по-тихому преобразует и списки и кортежи в массивы json во время сериализации. Для большинства случаев, вы можете проигнорировать различие между списками и кортежами, но это нужно помнить, когда работаешь с модулем json.

Материалы для дальнейшего чтения[править]