__toc__

Згоди ідея серіалізації виглядає простою. В нас є структура даних в пам'яті яку ми хочемо зберегти, перевикористати чи відправити комусь іншому. Як це можна зробити? Ну, все залежить від того як ви хочете її зберігати, як ви хочете її повторно використовувати, і кому ви хочете її відправити. Багато ігор дозволяють вам зберігати свій прогрес при завершенні гри, і продовжувати звідки почали, коли ви запускаєте гру наступного разу. (Ну і звісно так вміють багато програм окрім ігор.) В даному випадку, структура даних що зберігає ваш поточний прогрес повинна записатись на диск коли ви виходите з програми, а потім завантажитись в пам'ять коли ви запускаєте програму знову. Такі дані призначені для використання тою ж програмою що їх створила, не пересилаються через мережу і ніколи не читаються іншими програмами. Тому проблеми сумісності обмежуються гарантуванням того що наступні версії програми будуть здатні читати дані записані попередніми версіями.

Для подібних випадків ідеальним є модуль pickle. Він є частиною стандартної бібліотеки мови Python, тому завжди доступний. Він швидкий, основна його частина написана на C, як і сам інтерпретатор мови. Він може зберігати досить складні структури даних мови Python.

Що може зберігати модуль pickle?

Всі стандартні типи даних що підтримує Python: булевий, цілий, числа з плаваючою крапкою, комплексні числа, рядки, байтові об'єкти, та None. Списки, кортежі, словники і множини що містять будь-які комбінації стандартних типів даних. Списки, кортежі, словники і множини що містять будь-яку послідовність списків, кортежів, словників і множин що містять будь-яку послідовність стандартних типів даних (і так далі, до максимального рівня вкладеності що підтримує PythonНасправді рівень вкладеності не обмежується глибиною стеку. Обмежується лише глибина рекурсії функцій що працюють з такими структурами даних але їх не обов'язково робити рекурсивними.). Функції, класи та екземпляри класів (з кількома застереженнями).

Якщо цього для вас недостатньо, модуль pickle ще й розширюваний. Якщо ви зацікавлені в розширенні, перегляньте посилання в параграфі Для подальшого читання в кінці розділу.

Коротке зауваження про приклади цього розділу Цей розділ розповідає історію з двома оболонками інтерпретатора. Всі приклади в цьому розділі є частиною єдиної сюжетної арки. Вас проситимуть перемикатись між двома оболонками поки я демонструватиму вам модулі pickle та json.

Щоб не заплутатись, запустіть одну сесію інтерпретатора Python і створіть наступну змінну:

>>> shell = 1

Тримайте це вікно відкритим. Тепер відкрийте ще одне, і опишіть таку змінну:

>>> shell = 2

Протягом цього розділу я використовуватиму змінну shell щоб позначити яка з оболонок використовується в кожному прикладі.

Збереження даних в файл Piсkle Модуль 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

Ідея тут полягає в тому щоб створити словник що може містити щось корисне, наприклад елемент фіду Atom. Але я також хочу щоб він містив кілька різних типів даних, щоб показати що може модуль pickle. Не треба вишукувати в цих значеннях забагато змісту.

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

Модуль 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, серіалізує її в двійковий формат використовуючи останню версію протоколу pickle мови Python, і зберігає її в відкритий файл.

Останнє речення було досить важливим.

Модуль pickle приймає структуру даних мови Python і зберігає її в файл. Щоб зробити це він серіалізує структуру даних використовуючи формат який називається "протокол pickle". Протокол pickle специфічний для мови Python, немає гарантії міжмовної сумісності. Швидше за все ви не зможете взяти щойностворений файл entry.pickle і зробити з ним щось корисне за допомогою Perl, PHP, Java чи іншої мови. Не кожна структура мови Python може бути серіалізованою модулем pickle. Протокол pickle змінювався кілька разів коли в мові Python з'являлись нові типи даних, але досі існують певні обмеження. Через ці зміни немає гарантії сумісності цих файлів між різними версіями мови Python. Новіші версії мови підтримують старі формати серіалізації, але старіші версії не підтримують нові формати (бо в них немає відповідних нових типів даних). Якщо ви не вкажете інше, всі функції модуля pickle використовуватимуть останню версію протоколу. Це гарантує вам максимальну гнучкість при виборі даних які можна серіалізувати, але також означає що файл не читатиметься в старих версіях мови Python, які не підтримують останню версію протоколу. Останньою версією протоколу pickle є двійковий формат. Переконайтесь що ви відкриваєте файли в двійковому режимі, інакше дані будуть пошкоджені при записі.

Завантаження даних з файлу pickle Тепер переключіться в другий інтерпретатор Python - в той в якому ми не створювали словник entry.

>>> shell 2 >>> entry Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'entry' is not defined

Як бачимо в інтерпретаторі №2 немає змінної entry. Ми описали змінну entry в першій оболонці, але це зовсім інше середовище з власним станом.

>>> import pickle >>> with open('entry.pickle', 'rb') as f: ... entry = pickle.load(f) ...

Відкрийте файл entry.pickle який ми створили в інтерпретаторі №1. Модуль pickle використовує двійковий формат даних, тому ми повинні завжди відкривати файли pickle в двійковому режимі.

Функція pickle.load() приймає потоковий об'єкт, читає серіалізовані дані з потоку, створює новий об'єкт мови Python, відтворює серіалізовані дані в цьому об'єкті, і повертає його як результат.

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

Тепер змінна entry - це словник з вже знайомими нам ключами та значеннями.

Послідовність pickle.dump() / pickle.load() в результаті дає нам нову структуру даних яка дорівнює оригінальній структурі.

Тепер давайте знову повернемось в оболонку №1.

>>> shell 1 >>> with open('entry.pickle', 'rb') as f: ... entry2 = pickle.load(f) ...

Давайте відкриємо файл entry.pickle і завантажимо серіалізовані дані в нову змінну, entry2.

>>> entry2 == entry True

Python підтверджує що два словники, entry та entry2 рівні. В цьому інтерпретаторі ми створили entry з нуля, почавши з порожнього словника, і вручну присвоюючи значення відповідним ключам. Ми серіалізували цей словник і зберегли його в файлі entry.pickle. Зараз ми прочитали серіалізовані дані з того файлу і створили ідеальну копію оригінальної структури даних.

>>> entry2 is entry False

Рівність це не те саме що ідентичність. Я сказав що ми створили ідеальну копію оригінальної структури даних, і це правда. Але це все одно копія.

>>> entry2['tags'] ('diveintopython', 'docbook', 'html') >>> entry2['internal_id'] b'\xDE\xD5\xB4\xF8'

З причин які стануть зрозумілими далі в цьому розділі, я хочу вказати на те що значення ключа 'tags' - кортеж, а значення ключа 'internal_id' - байтовий об'єкт.

Серіалізація без файлу Приклади в попередній секції показали нам як серіалізувати об'єкт прямо в файл на диску. Але що якщо ми не хочемо, чи не потребуємо файла? Також можна серіалізувати в байтовий об'єкт в пам'яті.

>>> shell 1 >>> b = pickle.dumps(entry)

Функція pickle.dumps() (зауважте 's' наприкінці імені) виконує таку ж серіалізацію як і pickle.dump(). Але замість того щоб прийняти потоковий об'єкт і записати серіалізовані дані на диск, вона просто повертає серіалізовані дані.

>>> type(b) <class 'bytes'>

Так як протокол pickle використовує двійковий формат даних, функція pickle.dumps повертає об'єкт типу bytes.

>>> entry3 = pickle.loads(b)

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

>>> entry3 == entry True

Кінцевий результат такий самий - ідеальна копія оригінального словника.

Байти та рядки показують свою потворну голову знову Протокол pickle існував протягом багатьох років, і розвивався разом з мовою Python. Зараз існує чотири різні версії протоколу pickle.

Python 1.x мав два протоколи, текстовий формат ("версія 0") та двійковий формат ("версія 1"). В Python 2.3 з'явився новий протокол pickle ("версія 2") призначений для роботи з новою функціональністю класів в Python. Це двійковий формат. В Python 3.0 з'явився інший протокол pickle ("версія 3") з підтримкою байтових об'єктів та байтових масивів. Він теж двійкового формату.

Ой, гляньте, відмінність між байтами та рядками знову показує свою потворну голову. (Якщо вас це здивувало, значить ви недостатньо уважно читаєте). На практиці це означає що хоча Python 3 може читати дані записані протоколом версії 2, Python 2 не може читати дані записані протоколом версії 3.

Дослідження файлів Pickle Як виглядає протокол pickle? Давайте на хвилину вийдемо з інтерпретатора і подивимось на створений нами файл 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