Nacházíte se zde: Domů ‣ Ponořme se do Pythonu 3 ‣
Úroveň obtížnosti: ♦♦♦♦♢
❝ Every Saturday since we’ve lived in this apartment, I have awakened at 6:15, poured myself a bowl of cereal, added
a quarter-cup of 2% milk, sat on this end of this couch, turned on BBC America, and watched Doctor Who. ❞
(Každou sobotu, od té doby co žiji v tomto bytě, jsem vstal v 6.15, nasypal do sebe misku cereálií, přidal jsem hrnek
2% mléka, sedl jsem si na tento konec této pohovky, zapnul jsem BBC America a díval jsem se na Doctor Who.)
— Sheldon, The Big Bang Theory
Myšlenka serializace vypadá na první pohled jednoduše. Máme datovou strukturu v paměti, kterou chceme uložit, znovu použít nebo zaslat někomu jinému. Jak bychom to udělali? Záleží to na tom, jak ji chceme uložit, jak ji chceme znovu použít a komu ji chceme poslat. Mnoho her umožňuje, abyste si při ukončení uložili stav a při příštím spuštění pokračovali od tohoto místa dál. (Ve skutečnosti to umožňuje i mnoho aplikací, které nemají s hrami nic společného.) V takovém případě musí být datová struktura, která zachycuje „váš dosavadní pokrok“, při ukončení uložena na disk a při opětném spuštění z disku načtena. Data jsou určena jen pro použití se stejným programem, který je vytvořil. Nikdy se neposílají po síti a nikdy je nečte nic jiného než program, který je vytvořil. To znamená, že záležitost součinnosti se omezuje pouze na to, aby byla následující verze programu schopna načíst data zapsaná předchozími verzemi.
Pro tyto případy se ideálně hodí modul pickle
. Je součástí pythonovské standardní knihovny, takže je kdykoliv k dispozici. Je rychlý. Jeho větší část je napsána v jazyce C, stejně jako vlastní interpret Pythonu. Dokáže uložit libovolně složité pythonovské datové struktury.
Co vlastně modul pickle
dokáže uložit?
bytes
, pole bajtů a None
.
A pokud se vám to zdá málo, modul pickle
je navíc rozšiřitelný. Pokud vás možnost rozšiřitelnosti zajímá, podívejte se na odkazy v podkapitole Přečtěte si na konci kapitoly.
Tato kapitola vypráví příběh s dvěma pythonovskými shelly. Všechny příklady v kapitole jsou částí jedné linie příběhu. Během předvádění modulů pickle
a json
budeme přecházet z jednoho pythonovského shellu do druhého.
Abychom oba od sebe poznali, otevřete jeden pythonovský shell a definujte následující proměnnou:
>>> shell = 1
Okno nechejte otevřené. Teď otevřete druhý pythonovský shell a definujte proměnnou:
>>> shell = 2
Během kapitoly budeme používat proměnnou shell
k indikaci toho, který pythonovský shell se u každého příkladu používá.
⁂
Modul pickle
pracuje s datovými strukturami. Jednu takovou si připravíme.
>>> 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)
pickle
předvést. Nestudujte uvedené hodnoty zbytečně podrobně.
time
definuje datovou strukturu (struct_time
), která se používá k reprezentaci času (s přesností na milisekundy), a funkce, které s touto strukturou manipulují. Funkce strptime()
přebírá formátovaný řetězec a převádí jej do podoby struct_time
. Tento řetězec je ve výchozím tvaru, ale můžete jej ovlivnit formátovacími značkami. Podrobnosti hledejte v dokumentaci k modulu time
.
Takže tu máme krásně vypadající pythonovský slovník. Uložme jej do souboru.
>>> shell ① 1 >>> import pickle >>> with open('entry.pickle', 'wb') as f: ② ... pickle.dump(entry, f) ③ ...
open()
. Režim souboru nastavíme na 'wb'
, abychom jej otevřeli pro zápis v binárním režimu. Zabalíme jej do příkazu with
, abychom zajistili, že se po dokončení prací sám zavře.
dump()
z modulu pickle
přebírá pythonovskou serializovatelnou datovou strukturu, serializuje ji do binárního podoby (je specifická pro Python a používá poslední verzi protokolu pro pickle) a uloží ji do otevřeného souboru.
Poslední věta je velmi důležitá.
pickle
přebírá pythonovskou datovou strukturu a uloží ji do souboru.
entry.pickle
, který jsme zrovna vytvořili, a udělali s ním něco rozumného v Perlu, v PHP, v Javě nebo v nějakém jiném jazyce.
pickle
nedokáže serializovat každou pythonovskou datovou strukturu. Pickle protokol se několikrát změnil s tím, jak byly do jazyka Python přidávány nové datové typy. Ale některá omezení přetrvávají.
pickle
používat poslední verze pickle protokolu. Tím je zajištěna maximální pružnost z hlediska typů serializovatelných dat, ale také to znamená, že výsledný soubor nebude čitelný staršími verzemi Pythonu, které poslední verzi pickle protokolu nepodporují.
⁂
Teď se přepneme do druhého pythonovského shellu — tj. do toho, ve kterém jsme nevytvářeli slovník 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}
entry.pickle
, který jsme vytvořili v pythonovském shellu č. 1. Modul pickle
používá binární datový formát, takže byste jej měli vždy otvírat v binárním režimu.
pickle.load()
přebírá objekt typu stream, čte z něj serializovaná data, vytváří nový pythonovský objekt, rekonstruuje v něm serializovaná data a nový pythonovský objekt vrací.
Kroky pickle.dump() / pickle.load()
vedou k vytvoření nové datové struktury, která se shoduje s původní datovou strukturou.
>>> 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'
entry.pickle
.
entry.pickle
. Teď jsme serializovaná data z uvedeného souboru načetli a vytvořili jsme perfektní repliku původní datové struktury.
'tags'
byla přiřazena hodnota v podobě n-tice a klíči 'internal_id'
byl přiřazen objekt typu bytes
.
⁂
Serializaci pythonovských objektů přímo do souboru na disk jsme si ukázali na příkladech v předchozí podkapitole. Ale co když soubor nechceme nebo nepotřebujeme? Serializaci můžeme provést také do objektu typu bytes
, který se nachází v paměti.
>>> shell 1 >>> b = pickle.dumps(entry) ① >>> type(b) ② <class 'bytes'> >>> entry3 = pickle.loads(b) ③ >>> entry3 == entry ④ True
pickle.dumps()
(všimněte si 's'
na konci jména funkce) provádí stejnou serializaci jako funkce pickle.dump()
. Ale nepřevezme objekt typu stream a serializovaná data nezapíše do souboru na disk. Místo toho serializovaná data jednoduše vrátí.
pickle.dumps()
objekt typu bytes
.
pickle.loads()
(opět si všimněte 's'
na konci jména funkce) provádí stejnou deserializaci jako funkce pickle.load()
. Místo čtení serializovaných dat ze souboru (přes objekt typu stream) přebírá objekt typu bytes
, který serializovaná data obsahuje — takový, jaký vrátila funkce pickle.dumps()
.
⁂
Pickle protokol se používá už celou řadu let a vyspíval spolu s dospíváním Pythonu. V současnosti existují čtyři různé verze pickle protokolu.
bytes
a pro pole bajtů. Jeho formát je binární.
Pozor, rozdíl mezi bajty a řetězci zase vystrkuje svou ošklivou hlavu. (Pokud jste dávali pozor, nejste překvapeni.) V praxi to znamená, že zatímco Python 3 umí číst data serializovaná protokolem verze 2, Python 2 neumí číst data „zapiklená“ protokolem verze 3.
⁂
Jak vlastně pickle protokol vypadá? Vyskočme na chvíli z pythonovského shellu a podívejme se na soubor entry.pickle
, který jsme vytvořili. Z prostého pohledu v tom vidíme převážně blábol.
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.
No, moc nám to tedy nepomohlo. Vidíme řetězce, ale ostatní datové typy končí jako netisknutelné (nebo přinejmenším nečitelné) znaky. Pole zjevně nejsou oddělena mezerami nebo tabulátory. Není to zrovna formát, který bychom chtěli analyzovat sami.
>>> 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
Nejzajímavější informaci v tomto reverzním překladu najdeme na posledním řádku. Obsahuje totiž verzi pickle protokolu, kterým byl tento soubor vytvořen. Pickle protokol neobsahuje žádnou explicitní značku, která by určovala verzi. Abychom verzi protokolu určili, musíme prohlížet značky („operační kódy“) uvnitř serializovaných dat a řídit se podle toho, který operační kód byl zaveden jakou verzí pickle protokolu. Přesně to dělá funkce pickletools.dis()
. Výsledek vytiskne na posledním řádku reverzního překladu. Tady máme funkci, která vrátí číslo verze, aniž by něco tiskla:
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
A tady ji vidíme v akci:
>>> import pickleversion >>> with open('entry.pickle', 'rb') as f: ... v = pickleversion.protocol_version(f) >>> v 3
⁂
Datový formát používaný modulem pickle
je specifický pro Python. Nijak se nepokouší o kompatibilitu s jinými programovacími jazyky. Pokud je vaším cílem mezijazyková kompatibilita, pak se musíte poohlédnout po jiných serializačních formátech. Jedním z nich je JSON. Zkratka „JSON“ znamená „JavaScript Object Notation“, ale nenechte se tím jménem zmást. JSON je explicitně navržen pro použití napříč různými programovacími jazyky.
V Pythonu 3 je modul json
součástí standardní knihovny. Modul json
má (stejně jako modul pickle
) funkce pro serializaci datových struktur, pro ukládání serializovaných dat na disk, pro načítání serializovaných dat z disku a pro deserializaci dat zpět do podoby nového pythonovského objektu. Ale najdeme zde také důležité odlišnosti. Ze všeho nejdřív uveďme, že datový formát JSON je textový a ne binární. Formát JSON a způsob kódování různých typů dat je definován v RFC 4627. Například booleovská hodnota je uložena buď jako pětiznakový řetězec 'false'
nebo jako čtyřznakový řetězec 'true'
. Všechny hodnoty používané v JSON jsou citlivé na velikost písmen.
Za druhé tu máme — jako u všech textových formátů — problém s bílými znaky (whitespace). JSON dovoluje, aby se mezi hodnotami vyskytovalo libovolné množství bílých znaků (mezery, tabulátory, návrat vozíku CR, přechod na nový řádek LF). Tyto bílé znaky jsou nevýznamné. To znamená, že kodéry JSON mohou přidat bílé znaky dle vlastního uvážení. Po dekodérech JSON se požaduje, aby bílé znaky mezi hodnotami ignorovaly. To umožňuje, aby byla JSON data „pěkně naformátována“ (pretty-print). Hodnoty mohou být pěkně vnořeny do jiných hodnot při použití různých úrovní odsazení, takže data budou dobře čitelná v textovém editoru nebo ve standardním prohlížeči. V pythonovském modulu json
najdeme volbu, která při procesu kódování zajistí „pěkné formátování“.
Za třetí tu máme přetrvávající problém s kódováním znaků. JSON kóduje hodnoty do podoby prostého textu, ale my už víme, že nic jako „prostý text“ neexistuje. JSON musí být uložen v kódování Unicode (v UTF-32, v UTF-16 nebo ve výchozím UTF-8). Sekce 3 dokumentu RFC 4627 definuje, jak máme říct, které kódování je použito.
⁂
JSON se nápadně podobá datovým strukturám, které byste mohli ručně definovat v JavaScriptu. Není to žádná náhoda. Ve skutečnosti můžete pro „dekódování“ dat serializovaných do JSON použít javascriptovou funkci eval()
. (Platí zde obvyklá výstraha o nedůvěryhodných zdrojích, ale věc se má tak, že JSON opravdu je platný JavaScript.) V tomto smyslu už se vám JSON může zdát důvěrně známý.
>>> 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) ③
json
(stejně jako modul pickle
) definuje funkci dump()
, která přebírá pythonovskou datovou strukturu a objekt typu stream připravený pro zápis. Funkce dump()
serializuje pythonovskou datovou strukturu a zapíše ji do objektu typu stream. Vložením volání do příkazu with
zajistíme, že po dokončení operace bude soubor korektně uzavřen.
Takže jak vlastně výsledek serializace do JSON vypadá?
you@localhost:~/diveintopython3/examples$ cat basic.json {"published": true, "tags": ["diveintopython", "docbook", "html"], "comments_link": null, "id": 256, "title": "Dive into history, 2009 edition"}
Tak tohle je určitě mnohem čitelnější než „zapiklený“ soubor. Navíc JSON může mezi hodnotami obsahovat libovolné bílé znaky a modul json
nabízí snadný způsob, jak toho využít. Díky tomu můžeme vytvořit ještě mnohem čitelnější JSON soubory.
>>> shell
1
>>> with open('basic-pretty.json', mode='w', encoding='utf-8') as f:
... json.dump(basic_entry, f, indent=2) ①
json.dump()
předáme parametr indent (tj. odsazení), může být výsledný JSON soubor mnohem čitelnější — za cenu zvětšení velikosti souboru. Parametr indent je celé číslo. 0 znamená „umísti každou hodnotu na zvláštní řádek“. Číslo větší než 0 znamená „umísti každou hodnotu na zvláštní řádek a použij tento počet mezer pro odsazování zanořených datových struktur“.
A takhle vypadá výsledek:
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" }
⁂
Protože JSON není určen pro Python, najdeme při zobrazování pythonovských datových typů určité nesrovnalosti. Některé z nich jsou jen rozdíly v názvech, ale dva důležité pythonovské datové typy v něm úplně chybí. Schválně, jestli si jich všimnete:
Poznámky | JSON | Python 3 |
---|---|---|
objekt | slovník | |
pole | seznam | |
řetězec | řetězec | |
integer | integer | |
reálné číslo | float | |
* | true
| True
|
* | false
| False
|
* | null
| None
|
* Všechny hodnoty používané v JSON jsou citlivé na velikost písmen. |
Všimli jste si, co chybí? N-tice a bajty! JSON definuje typ pole, které modul json
zobrazuje na pythonovský seznam, ale nedefinuje oddělený typ pro „zmrazená pole“ (n-tice). A ačkoliv JSON docela pěkně podporuje řetězce, nepodporuje objekty typu bytes
nebo pole bajtů.
⁂
I když JSON nemá žádnou zabudovanou podporu pro bajty, neznamená to, že bychom objekty typu bytes
nemohli serializovat. Modul json
poskytuje rozšiřující rozhraní (extensibility hooks) pro kódování a dekódování neznámých datových typů. (Slovem „neznámý“ rozumějme „nedefinovaný v JSON“. Modul json
zjevně pole bajtů zná, ale je svázán omezeními specifikace JSON.) Pokud chceme zakódovat bajty nebo jiné datové typy, které JSON v základu nepodporuje, musíme pro ně dodat uživatelské kodéry a dekodéry.
>>> 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
None
, řetězec, n-tici řetězců, objekt typu bytes
a strukturu time
.
Stalo se následující: funkce json.dump()
se pokusila o serializaci objektu typu bytes
s hodnotou b'\xDE\xD5\xB4\xF8'
, ale selhala, protože v JSON podpora objektů typu bytes
chybí. Pokud je ale pro nás ukládání bajtů důležité, můžeme si definovat náš vlastní „miniserializační formát“.
[stáhnout 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') ④
json.dump()
není schopna sama serializovat. V našem případě je to objekt typu bytes
s hodnotou b'\xDE\xD5\xB4\xF8'
.
json.dump()
. Pokud funkce serializuje jen jeden datový typ, není to nezbytně nutné. Na druhou stranu se tím vyjasňuje, čím se funkce zabývá. A pokud budeme později potřebovat přidat serializaci pro více datových typů, půjde to snadněji.
bytes
na slovník. Klíč __class__
bude obsahovat původní datový typ (v řetězcové podobě, 'bytes'
) a klíč __value__
bude obsahovat aktuální hodnotu. Nemůže to, samozřejmě, být objekt typu bytes
. Celý vtip spočívá v převodu na něco, co může být serializováno v JSON! Objekt typu bytes
je posloupností celých čísel, kde každé číslo nabývá hodnot z rozsahu 0–255. Pro převod objektu typu bytes
na seznam čísel můžeme použít funkci list()
. Takže z b'\xDE\xD5\xB4\xF8'
se stane [222, 213, 180, 248]
. (Počítejte! Funguje to! Bajt zapsaný šestnáctkově \xDE
je dekadicky 222, \xD5
je 213 a tak dále.)
TypeError
, aby se funkce json.dump()
dozvěděla, že náš uživatelský serializátor daný typ nezná.
A to je vše. Nemusíme dělat nic jiného. Konkrétně tato uživatelská serializační funkce vrací pythonovský slovník a ne řetězec. Nemusíme sami realizovat celou „serializaci do JSON“. Provedeme pouze část „konverze na podporovaný datový typ“. Funkce json.dump()
udělá zbytek.
>>> 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
patří modulu, ve kterém jsme (v předchozím příkladu) definovali funkci to_json()
.
json.dump()
, předáme ji při volání funkce json.dump()
jako hodnotu parametru default. (Hurá! V Pythonu je objektem všechno.)
json.dump()
už si nestěžuje na to, že není schopna serializovat objekt typu bytes
. Teď už si stěžuje na úplně jiný objekt — time.struct_time
.
Mohlo by se zdát, že výskyt jiné výjimky není známkou pokroku. Jenže on opravdu je známkou pokroku! Bude stačit jedno malé pošťouchnutí a překonáme i tohle.
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()
potřebujeme zkontrolovat, zda je pythonovský objekt (s kterým má funkce json.dump()
potíže) typu time.struct_time
.
bytes
. Objekt typu time.struct_time
převedeme na slovník, který bude obsahovat pouze hodnoty, které lze serializovat do JSON. V našem případě je nejsnadnější způsob převodu data a času na hodnotu serializovatelnou do JSON založen na převodu na řetězec pomocí funkce time.asctime()
. Funkce time.asctime()
převádí odporně vypadající time.struct_time
na řetězec 'Fri Mar 27 22:20:42 2009'
.
Při použití těchto dvou uživatelských konverzí proběhne serializace celé datové struktury entry do JSON bez dalších problémů.
>>> 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}
⁂
Modul json
obsahuje (stejně jako modul pickle
) funkci load()
, která přebírá objekt typu stream, čte z něj data v notaci JSON a vytváří nový pythonovský objekt, který odráží datovou strukturu 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}
pickle
.
json.load()
stejně jako funkce pickle.load()
. Předáme jí objekt typu stream a vrací nový pythonovský objekt.
json.load()
úspěšně přečetla soubor entry.json
, který jsme vytvořili v pythonovském shellu č. 1, a vytvořila nový pythonovský objekt, který data obsahuje. Teď ta špatná zpráva. Nevznikla tím původní datová struktura entry. Hodnoty 'internal_id'
a 'published_date'
byly vytvořeny jako slovníky. Jde konkrétně o slovníky obsahující hodnoty slučitelné s JSON, které jsme vytvořili převodní funkcí to_json()
.
Funkce json.load()
neví nic o konverzních funkcích, které jste mohli předat funkci json.dump()
. Potřebujeme vytvořit funkci, která je opakem k funkci to_json()
. Potřebujeme funkci, která převezme uživatelsky převedený objekt JSON a konvertuje jej zpět na původní pythonovský datový typ.
# do customserializer.py přidejte následující
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
'__class__'
, který vytvořila funkce to_json()
. Pokud tomu tak je, říká hodnota klíče '__class__'
, jak máme hodnotu dekódovat zpět na původní pythonovský datový typ.
time.asctime()
, použijeme funkci time.strptime()
. Tato funkce přebírá naformátovaný řetězec s datem a časem (v upravitelném formátu, ale s výchozím tvarem stejným, jaký používá funkce time.asctime()
) a vrací time.struct_time
.
bytes
můžeme použít funkci bytes()
.
A je to. Ve funkci to_json()
se upravovaly jen dva datové typy. Stejné datové typy jsme teď zpracovali funkcí from_json()
. A takhle vypadá výsledek:
>>> 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()
k deserializačnímu procesu připojíme tím, že ji předáme jako parametr object_hook funkci json.load()
. Funkce, která přebírá funkci. Jak šikovné!
'internal_id'
, jehož hodnotou je objekt typu bytes
. Obsahuje také klíč 'published_date'
, jehož hodnotou je objekt typu time.struct_time
.
Ale má to ještě jednu mouchu.
>>> 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()
k serializaci a připojení funkce from_json()
k deserializaci se nám stále nepodařilo vytvořit dokonalou repliku původní datové struktury. Proč tomu tak je?
'tags'
n-tice tří řetězců (tedy trojice řetězců).
'tags'
hodnotu seznamu těchto tří řetězců. JSON nedělá rozdíl mezi n-ticemi a seznamy. Zná jen jeden seznamu se podobající datový typ — typ pole. Modul json
během serializace potichu konvertuje jak n-tice, tak seznamy na pole v JSON. Při většině použití můžete rozdíl mezi n-ticemi a seznamy ignorovat. Ale pokud pracujete s modulem json
, měli byste na to myslet.
☞Řada článků o modulu
pickle
se odkazuje nacPickle
. V Pythonu 2 existovaly dvě implementace modulupickle
. Jedna byla napsána v Pythonu a druhá v jazyce C (ale dala se volat z Pythonu). V Pythonu 3 byly tyto moduly spojeny, takže pokaždé provádíme jenimport pickle
. Zmíněné články mohou být užitečné, ale informaci ocPickle
(která je nyní zastaralá) byste měli ignorovat.
O „piklení“ s modulem pickle
:
pickle
module
pickle
and cPickle
— Python object serialization
pickle
O JSON a o modulu json
:
json
— JavaScript Object Notation Serializer
O rozšiřitelnosti modulu pickle
:
© 2001–11 Mark Pilgrim