Nacházíte se zde: Domů Ponořme se do Pythonu 3

Úroveň obtížnosti: ♦♦♦♦♢

Serializace pythonovských objektů

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

 

Ponořme se

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?

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.

Stručná poznámka k příkladům v této kapitole

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á.

Uložení dat do „pickle-souboru“

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)
  1. Budeme pracovat v pythonovském shellu č. 1.
  2. Základní myšlenka spočívá ve vytvoření pythonovského slovníku, který reprezentuje něco užitečného, jako například záznam v Atom feed. Ale současně by měl obsahovat několik různých typů dat, abychom mohli modul pickle předvést. Nestudujte uvedené hodnoty zbytečně podrobně.
  3. Modul 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)                
... 
  1. Pořád se nacházíme v pythonovském shellu č. 1.
  2. K otevření souboru použijeme funkci 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.
  3. Funkce 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á.

Načítání dat z „pickle souboru“

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}
  1. Tohle je pythonovský shell č. 2.
  2. Není zde definována žádná proměnná entry. Proměnnou entry jsme definovali v pythonovském shellu č. 1, ale ten se nachází v úplně jiném prostředí a udržuje svůj vlastní stav.
  3. Otevřeme soubor 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.
  4. Funkce 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í.
  5. Nyní proměnná entry obsahuje slovník s důvěrně známými klíči a hodnotami.

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'
  1. Přepneme se zpět do pythonovského shellu č. 1.
  2. Otevřeme soubor entry.pickle.
  3. Načteme serializovaná data do nové proměnné entry2.
  4. Python potvrzuje, že se slovníky entry a entry2 shodují. V tomto shellu jsme strukturu entry vybudovali od základů. Začali jsme prázdným slovníkem a ručně jsme jednotlivým klíčům přiřadili určité hodnoty. Slovník jsme serializovali a uložili do souboru entry.pickle. Teď jsme serializovaná data z uvedeného souboru načetli a vytvořili jsme perfektní repliku původní datové struktury.
  5. Shodnost ale nezaměňujme za totožnost. Řekl jsem, že jsme vytvořili perfektní repliku původní datové struktury, což je pravda. Ale pořád je to jen kopie.
  6. Z důvodů, které budou objasněny v této kapitole později, chci upozornit na to, že klíči 'tags' byla přiřazena hodnota v podobě n-tice a klíči 'internal_id' byl přiřazen objekt typu bytes.

„Piklení“ bez souboru

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
  1. Funkce 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í.
  2. A protože pickle protokol používá binární datový formát, vrátí funkce pickle.dumps() objekt typu bytes.
  3. Funkce 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().
  4. Konečný výsledek je stejný: perfektní replika původního slovníku.

Bajty a řetězce znovu zvedají své ošklivé hlavy

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.

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.

Ladění „pickle souborů“

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:

[stáhnout 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

A tady ji vidíme v akci:

>>> import pickleversion
>>> with open('entry.pickle', 'rb') as f:
...     v = pickleversion.protocol_version(f)
>>> v
3

Serializace pythonovských objektů pro čtení z jiných jazyků

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.

Uložení dat do JSON souboru

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)                              
  1. Místo znovupoužití existující datové struktury entry si teď vytvoříme novou datovou strukturu. Později si v této kapitole ukážeme, co se stane, když se do JSON pokusíme zakódovat složitější datovou strukturu.
  2. JSON je textový formát, což znamená, že soubor musíme otevřít v textovém režimu a musíme určit znakové kódování. Nikdy neuděláte chybu, když použijete UTF-8.
  3. Modul 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)                            
  1. Pokud funkci 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"
}

Zobrazení pythonovských datových typů do JSON

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ů.

Serializace datových typů, které JSON nepodporuje

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
  1. Nastal čas k tomu, abychom se znovu podívali na datovou strukturu entry. Obsahuje následující: booleovskou hodnotu, hodnotu None, řetězec, n-tici řetězců, objekt typu bytes a strukturu time.
  2. Já vím. Říkal jsem to už dříve, ale stojí to za zopakování: JSON je textový formát. JSON soubory se musí otvírat vždy v textovém režimu a se znakovým kódováním UTF-8.
  3. Hmm, tohle není dobré. Co se to vlastně stalo?

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')  
  1. Abychom definovali vlastní „miniserializační formát“ pro datový typ, který JSON přirozeně nepodporuje, musíme definovat funkci, která přebírá pythonovský objekt jako parametr. Tímto pythonovským objektem bude skutečný objekt, který funkce json.dump() není schopna sama serializovat. V našem případě je to objekt typu bytes s hodnotou b'\xDE\xD5\xB4\xF8'.
  2. Naše uživatelská serializační funkce by měla zkontrolovat typ pythonovského objektu, který jí předala funkce 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.
  3. V tomto případě jsem se rozhodl převést objekt typu 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.)
  4. Tento řádek je důležitý. Datová struktura, kterou serializujete, může obsahovat typy, které nejsou ani zabudované do serializátoru JSON a nezvládne je ani náš uživatelský serializátor. V takovém případě musí náš uživatelský serializátor vyvolat výjimku 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
  1. Jméno customserializer patří modulu, ve kterém jsme (v předchozím příkladu) definovali funkci to_json().
  2. Textový režim, kódování UTF-8 atd., atd. (Jednou na to zapomenete! Já na to taky občas zapomenu! A všechno bude fungovat správně až do chvíle, kdy se to pokazí. Ale pak se to pokazí se vší parádou.)
  3. Tohle je důležitá část. Abychom navěsili svou převodní funkci na funkci json.dump(), předáme ji při volání funkce json.dump() jako hodnotu parametru default. (Hurá! V Pythonu je objektem všechno.)
  4. No dobrá, ono to všechno nefunguje. Ale podívejte se na výjimku. Funkce 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')
  1. Při rozšiřování existující funkce customserializer.to_json() potřebujeme zkontrolovat, zda je pythonovský objekt (s kterým má funkce json.dump() potíže) typu time.struct_time.
  2. Pokud tomu tak je, uděláme podobný převod jako v případě objektu typu 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}

Načítání dat z JSON souboru

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}
  1. Pro demonstrační účely se přepneme do pythonovského shellu č. 2 a zrušíme tam datovou strukturu entry, kterou jsme v této kapitole vytvořili dříve, použitím modulu pickle.
  2. V nejjednodušším případě pracuje funkce json.load() stejně jako funkce pickle.load(). Předáme jí objekt typu stream a vrací nový pythonovský objekt.
  3. Mám pro vás dobrou a špatnou zprávu. Nejdříve tu dobrou. Funkce 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
  1. Tato převodní funkce také přebírá jeden parametr a vrací jednu hodnotu. Ale parametrem není řetězec. Je jím pythonovský objekt, který je výsledkem deserializace řetězce v notaci JSON do pythonovského objektu.
  2. Potřebujeme pouze zkontrolovat, zda tento objekt obsahuje klíč '__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.
  3. K dekódování řetězce s časem, který vrátila funkce 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.
  4. Pro převod seznamu celých čísel na objekt typu 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}
  1. Funkci 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é!
  2. Datová struktura entry teď obsahuje klíč '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']
  1. Dokonce ani po připojení funkce 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?
  2. V původní datové struktuře entry byla hodnotou klíče 'tags' n-tice tří řetězců (tedy trojice řetězců).
  3. Ale v datové struktuře entry2, kterou jsme dostali převodem tam a zase zpět, má klíč '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.

Přečtěte si

Řada článků o modulu pickle se odkazuje na cPickle. V Pythonu 2 existovaly dvě implementace modulu pickle. 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 jen import pickle. Zmíněné články mohou být užitečné, ale informaci o cPickle (která je nyní zastaralá) byste měli ignorovat.

O „piklení“ s modulem pickle:

O JSON a o modulu json:

O rozšiřitelnosti modulu pickle:

© 2001–11 Mark Pilgrim