Létající cirkus (14)

12. 7. 2002
Doba čtení: 9 minut

Sdílet

Dnes konečně plním slib z konce minulého dílu našeho seriálu, dnešní díl tedy bude věnován perzistentním objektům. Povíme si něco o modulech marshal, pickle a shelve.

Serializace

Nejeden programátor jistě zažil potřebu ukládat objekty určitého typu například do souboru a posléze je opět z tohoto souboru přečíst zpět. V dobách kralování jazyků Pascal nebo Basic (na nichž většina současných programátorů začínala) si tuto problematiku řešil každý po svém. Většinou ale bylo možné do jednoho souboru ukládat jen jeden typ dat. S příchodem interpretovaných jazyků a novějších postupů se začaly objevovat prostředky, které umožňují ukládat do souboru data různého typu a zpětně je z tohoto souboru přečíst. Při tomto procesu je třeba zajistit jednotný formát dat, čili u dat musí být uveden jejich typ a další informace potřebné pro zkrekonstruování objektu. Proces převodu objektu do „jednotného“ formátu se nazývá serializace. Serializace se používá velice často. První způsob použití, který napadne téměř každého, může být ukládání dokumentu do souboru (každý objekt v dokumentu se jednoduše převede do jednotného tvaru a v tomto tvaru se objekty postupně zapíší do souboru, odkud je pak aplikace přečte zpět ve stejném pořadí, v jakém je tam zapsala). Serializaci provádí také jakýkoli mechanismus pro vzdálené volání metod (například CORBA, která serializuje argumenty a návratové hodnoty před jejich přenosem mezi aplikacemi). Obecně lze serializovaná data přenést do jiného prostředí (jiný proces klidně i na jiném počítači), kde se z nich obnoví původní objekt. Přenos se může dít za pomoci soketů, rour, souborů, případně dalších mechanismů.

Na závěr je třeba poznamenat, že serializace není totéž co mechanismus perzistentních objektů. Pro to, abychom mohli perzistentní objekty používat, by bylo potřeba vyřešit otázku pojmenování objektů a konkurenčního přístupu k objektům. Nicméně samotné ukládání dat perzistentního objektu by se řešilo za použití serializace. Ve zbytku tohoto článku se tedy podíváme na moduly určené pro serializaci objektů.

Marshal

Modul marshal je nejjednodušším modulem pro serializaci objektů. Jméno marshal je převzato z jazyka Modula-3, který pojmem marshaling nazývá proces konverze dat z interního do externího tvaru. Unmarshaling je pak proces opačný. Modul marshal používá především interpret Pythonu pro ukládání zkompilovaných .pyc souborů. Interní formát NENÍ dokumentovaný, protože verzi od verze interpretu se mění. Nelze proto spoléhat na jeho zpětnou kompatibilitu. Nicméně je nezávislý na platformně, a tudíž soubory zapsané na jednom počítači můžete používat i na jiném používajícím tutéž verzi Pythonu (z toho vyplývá jedna užitečná vlastnost – .pyc soubory mohou být sdíleny počítači v síti).

Serializaci je možné většinou uplatnit na omezenou množinu datových typů. Modul marshal dokáže ukládat pouze ty typy, které jsou „nezávislé“ na prostředí interpretu. Jsou to tedy None, číselné typy, řetězce (včetně Unicode) a tuple, seznamy a asociativní pole, pokud tyto složené typy obsahují pouze typy, které dokáže marshal zpracovat. Posledním typem, kvůli kterému tento modul existuje především, je kódový objekt (tj. objekt, který obsahuje spustitelný kód).

Je třeba připomenout, že marshal je navržen pouze pro potřeby interpretu a my si ho vysvětlujeme pouze jako jednu z možných variant. Pro plnohodnotné používání tu je modul pickle.

Modul marshal exportuje čtyři užitečné funkce: dump(), load(), dumps() a loads(). Funkce dump přejímá dva argumenty, první je objekt, který má být uložen do souboru, a druhý je souborový objekt, kam bude serializovaný objekt uložen. Funkce load() zcela překvapivě načte zpět ze souboru, který jí formou objektu předáme jako jediný argument, jeden objekt, jenž pak vrátí. Funkce dumps(), která přejímá jediný argument – objekt k serializaci, vrátí takový řetězec, jaký by byl zapsán do souboru funkcí dump(). A nakonec loads() zrekonstruuje z řetězce, který jí předáme jako argument, původní objekt:

>>> import marshal
>>> obj1 = (None, 1, 2, 3)
>>> obj2 = ['jedna', 'dva', 'atd.']
>>> f = open('/tmp/marshal', 'wb')
>>> marshal.dump(obj1, f)

>>> marshal.dump(obj2, f)
>>> f.close()

Tento příklad ukazuje, jak uložit objekty do souboru /tmp/marshal. Ten je otevřen pro zápis a v binárním módu. Unixy nerozlišují mezi binárním a textovým módem, ve Woknech byste si ale s textovým módem ani neškrtli. Nyní si tyto objekty zrekonstruujeme (nevěřící Tomášové si mohou znovu spustit interpret):

>>> import marshal
>>> f = open('/tmp/marshal', 'rb')
>>> obj1 = marshal.load(f)
>>> obj2 = marshal.load(f)

>>> f.close()
>>> print obj1
(None, 1, 2, 3)
>>> print obj2
['jedna', 'dva', 'atd.']

Pickle

Jak již bylo řečeno v předchozích odstavcích, marshal je modul určený interpretu, proto zároveň s ním existuje ještě jeden modul – pickle. Ten je napsán v Pythonu a umožňuje mnohem lépe řídit proces serializace objektů. Tento objekt již zaručuje zpětnou kompatibilitu a dokáže serializovat i instance uživatelských tříd a dokonce i třídy a funkce samotné.

Abychom předešli omylům – serializování tříd a funkcí neukládá žádný kód, modul pickle si pouze uloží jméno modulu a jméno třídy. Při načtení pak vrátí třídu (funkci) pouze podle těchto informací, přičemž nezáleží na tom, jestli jde stále o jednu a tu samou třídu. Neukládání kódu je bezpečnostní opatření, modul pickle se takto (zčásti) vyhnul nebezpečí zneužití pomocí trojských koní.

Modul pickle při vícenásobné serializaci jednoho objektu pouze uloží odkaz na posledně serializovaný objekt. Proto je třeba dávat pozor na změny objektů mezi jednotlivými serializacemi, pokud k nim dojde, uloží modul pickle do souboru (řetězce) odkaz na nezměněný objekt a změna nebude zaznamenána. Nicméně i toto chování lze obejít (viz dokumentace jazyka).

Základní rozhraní modulu pickle je shodné s modulem marshal. Opět zde najdeme čtveřici funkcí dump(), load(), dumps(), loads(). Navíc zde najdeme funkce Pickler() a Unpickler(), které přejímají argument typu souborový objekt a vrátí objekt, který podporuje metody dump(), resp. load(). Tímto můžeme zkrátit zápis, chceme-li do jednoho souboru uložit více objektů:

>>> import pickle
>>> obj1 = (None, 1, 2, 3)
>>> obj2 = ['jedna', 'dva', 'atd.']
>>> f = open('/tmp/pickle', 'wb')
>>> p = pickle.Pickler(f)
>>> p.dump(obj1)
>>> p.dump(obj2)

>>> f.close()

>>> import pickle
>>> f = open('/tmp/pickle', 'rb')
>>> u = pickle.Unpickler(f)
>>> obj1 = u.load()
>>> obj2 = u.load()
>>> f.close()
>>> print obj1
(None, 1, 2, 3)
>>> print obj2
['jedna', 'dva', 'atd.']

Nyní je ten pravý čas zmínit se ještě o jedné důležité věci: existují dva moduly umožňující používat pickle rozhraní – Pythonovský modul pickle a céčkový modul cPickle. Modul cPickle je mnohonásobně rychlejší, ale nelze jej rozšiřovat, zatímco v modulu pickle jsou funkce Pickler() a Unpickler() implementovány jako třídy a není problém od nich odvodit vlastní třídu. Nicméně výsledná data po serializaci objektu nají u obou modulů stejný formát, takže není problém moduly zaměňovat (samozřejmě pokud neupravujeme chování při serializaci).

Nyní si ukážeme, jak postupovat, pokud potřebujeme serializovat instanci vlastní třídy. Nejprve je třeba vědět, že modul pickle při obnově instance třídy standardně NEVOLÁ konstruktor této třídy (jen pro zajímavost: modul při obnově nejprve vytvoří instanci třídy, která nemá konstruktor, a poté změní třídu této instance na danou třídu přiřazením hodnoty atributu __class__).

Přeje-li si programátor před obnovením instance zavolat konstruktor její třídy, musí instance podporovat metodu __getinitargs__(), která musí vrátit tuple prvků, jež budou použity jako argumenty předané konstruktoru při obnově třídy. Metoda __getinitargs__() je volána PŘED serializací instance a jí vrácené argumenty jsou uloženy zároveň se serializovanými daty instance.

Další užitečné metody, které může instance implementovat, je dvojice __getstate__() a __setstate__(). První je volána při serializaci instance a vrací objekt, obsahující interní stav instance. Tento objekt je následně serializován. Při obnově instance je tento objekt obnoven a předán metodě __setstate__(), která podle něj obnoví vnitřní stav instance. Metoda __setstate__() může být vynechána, pak __getstate__() musí vrátit asociativní pole, podle nějž se obnoví atribut __dict__ této instance.

Nepodporuje-li instance ani metodu __getinitargs__(), ani __getstate__(), bude se serializovat pouze obsah atributu __dict__. Zde si ukážeme jednoduchou třídu, která implementuje frontu FIFO a zároveň podporuje serializaci, přičemž vnitřní stav beze změny uloží jako seznam prvků (protože první prvek je na posledním místě, musí se při naplnění fronty po obnovení obrátit pořadí prvků v seznamu), délka fronty se zase uchovává jako argument pro konstruktor a vrací ho metoda __getinitargs__:

class Fronta:
    __safe_for_unpickling__ = 1

    def __init__(self, delka):
        print 'Vytvarim Frontu o delce %d' % delka
        self.delka = delka
        self.buffer = []

    def push(self, prvek):
        print 'Ukladam prvek "%s"' % prvek
        if len(self.buffer) < self.delka:
            self.buffer.insert(0, prvek)
        else:
            raise RuntimeError, 'buffer overflow'

    def pop(self):
        if len(self.buffer) > 0:
            return self.buffer.pop()
        else:
            raise RuntimeError, 'buffer underflow'

    def __getinitargs__(self):
        return (self.delka, )

    def __getstate__(self):
        return self.buffer

    def __setstate__(self, stav):
        print 'Naplnuji frontu'
        stav.reverse()
        for prvek in stav:
            self.push(prvek)

Nyní již můžeme vytvořit instanci této třídy a uložíme jí za pomoci serializace do řetězce:

>>> fronta1 = Fronta(5)
Vytvarim Frontu o delce 5
>>> fronta1.push(1)
Ukladam prvek "1"
>>> fronta1.push('Ahoj')
Ukladam prvek "Ahoj"
>>> import pickle
>>> retezec = pickle.dumps(fronta1)
>>> retezec
"(I5\ni__main__\nFronta\np1\n(lp2\nS'Ahoj'\np3\naI1\nab."

>>> fronta2 = pickle.loads(retezec)
Vytvarim Frontu o delce 5
Naplnuji frontu
Ukladam prvek "1"
Ukladam prvek "Ahoj"

Jak vidíme z následujících řádků, obsah bufferů obou front je stejný!

>>> fronta1.buffer
['Ahoj', 1]
>>> fronta2.buffer
['Ahoj', 1]

Modul pickle byl navržen i s ohledem na bezpečnost. Proto nelze přímo obnovit instanci libovolné třídy. Třídy, o kterých víme, že je budeme serializovat a poté obnovovat, musíme nejprve „označit“ pomocí atributu __safe_for_un­pickling__ (který musí být pravdivou logickou hodnotou).

Shelve

Modul shelve je mezistupňem mezi serializací a opravdovými perzistentními objekty. Řeší totiž otázku pojmenování objektů (konkurenční přístup ale stále není možný, konkrétně – vícenásobné čtení je podporováno, čtení a zápis zároveň ale neprojde).

Modul shelve používá pro ukládání dbm databázi, která se použije, je závislé na platformně a dostupných modulech. Modul shelve obsahuje funkci open(), která přejímá jeden argument – jméno souboru pro dbm databázi (tedy bez přípony) – a vrátí objekt, který má podobné rozhraní jako asociativního pole. Jako klíče se mohou ovšem používat pouze řetězce, hodnotami pak mohou být libovolné objekty, které se podle potřeby serializují a obnovují zpět (pomocí modulu pickle). Nakonec se databáze uzavře voláním metody close():

>>> import shelve
>>> d = shelve.open('/tmp/shelve')
>>> d['cislo'] = 1
>>> d['retezec'] = 'AHOJ'
>>> d['seznam'] = ['jedna', 1, 1.0]
>>> d.close()

Nyní můžeme interpret uzavřít a spustit znova:

bitcoin_skoleni

>>> import shelve
>>> d = shelve.open('/tmp/shelve')
>>> d['cislo']
1
>>> d['retezec']
'AHOJ'
>>> d['seznam']
['jedna', 1, 1.0]

Stejně tak můžete používat i další metody dbm databází jako keys(), get() nebo has_key(). Pro více informací o všech dnes probíraných modulech prosím nahlédněte do dokumentace jazyka Python.

Příště

Protože jsme se dnes zmínili o tom, že serializovaná data mohou být poslána napříč sítí, v příštím dílu se podíváme na komunikaci mezi procesy – tedy na roury a sokety.