<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xml" href="/cjs/screen.xsl" media="screen"?>
<lecture>

<meta>
  <maintitle>Python</maintitle>
  <author>Jiří Znamenáček</author>
  <title>Serializace – „pickle“</title>
  <date>2011-04-07</date>
  <link><!--a href="http://vyuka.ookami.cz" rel="external">http://vyuka.ookami.cz</a--></link>
</meta>
<!--
  „“–
  ↵ aneb &#x21B5; aneb \r aneb CR aneb CarriageReturn
-->


<slide title="Úvod">

    <p>
        Modul <strong>pickle</strong> je jedním z prostředků vyhrazených pro perzistentní ukládání pythoních dat. To znamená, že data jím uložená mohou být např. k dispozici mezi dvěma časově vzdálenými běhy programu (a to i jiného).
    </p>
    <p>
        Umí zaserializovat (téměř libovolnou) pythonovskou strukturu pomocí příslušného <em>pickle</em>-protokolu a uložit ji jako <strong>binární</strong> objekt buď <em>do proměnné</em> nebo <em>do souboru</em>. A samozřejmě ji pak zase umí z proměnné/souboru načíst a rozserializovat zpátky do příslušné struktury.
    </p>
    <note>
        Nejčastější použití asi najde piklení do souboru, ale stejně tak dobře můžete posílat zapiklené struktury po síti nebo ukládat do databáze.
    </note>

</slide>
<slide title="Protokol">

  <p>
    Pickle-protokolu jsou dnes již čtyři verze:
  </p>
  <ul>
    <li>
        protokol <strong>0</strong> – původní <em>human-readable</em> textová verze; zpětně kompatibilní
    </li>
    <li>
        protokol <strong>1</strong> – původní binární serializace
    </li>
    <li>
        protokol <strong>2</strong> – od verze Python'u 2.3
    </li>
    <li>
        protokol <strong>3</strong> – od verze Python'u 3.0
    </li>
  </ul>
  <note>
    A další varování před verzí Python'u 3.0: Protokol 2 je v ní naimplementován jinak, než v pozdějších verzích řady 3.X.
  </note>
  
  <p>
    Pokud výslovně neřeknete jinak, používá se jako výchozí hodnota příslušející vaší verzi Python'u, takže pro Python 3.x je to verze protokolu 3.
  </p>

</slide>
<slide title="Metody modulu „pickle“">

  <p>
    Modul <code>pickle</code> umí piklit a rozpiklovávat v zásadě do/z dvou různých míst – paměti nebo proudu (<em>stream</em>u):
  </p>
  <ul>
    <li>
        do/z <strong>paměti</strong>:
        <ul>
            <li>
              <code>po = pickle.dumps(o)</code> – zapiklí objekt <em>o</em> do proměnné <em>po</em> typu <i>bytes()</i>
            </li>
            <li>
              <code>o = pickle.loads(po)</code> – rozpiklí bajt-objekt <em>po</em> do datové struktury <em>o</em>
            </li>
        </ul>
    </li>
    <note>
        Všimněte si koncového <em>s</em> u metod pro piklení přes paměť.
    </note>
    <li>
        do/z <strong>proudu</strong>:
        <ul>
            <li>
              <code>pickle.dump(o, file)</code> – zapiklí objekt <em>o</em> do binárního souboru <em>file</em>
            </li>
            <li>
              <code>o = pickle.load(file)</code> – rozpiklí do objektu <em>o</em> zapiklenou strukturu v binárním souboru <em>file</em>
            </li>
        </ul>
    </li>
  </ul>
  <notes>
    <note>
        <em>file</em> je objekt typu <em>stream</em>, tj. <code>file = open('soubor', 'rwb')</code>, nikoli cesta k souboru.
    </note>
    <note>
        <em>dump</em>-metody mají několik dalších nepovinných parametrů, ale pokud nebudete sdílet zapiklené struktury mezi různými verzemi Python'u, může vám to být celkem jedno.
    </note>
  </notes>

</slide>
<slide title="Příklad – data">

  <p>
    Zkusme zapiklit jednoduchý slovník, který obsahuje pouze data:
  </p>
  <example layout="vertical">
    <program src="_files/test1_in.py" lang="python"/>
    <out src="_files/test1.pickle" lang="text"/>
  </example>
  <note>
    Tady se binární formát piklícího protokolu přeloží do textu i s novými řádky, ve skutečnosti samozřejmě takhle pozalamovaný není.
  </note>
  
  <p>
    A teď si ho zase rozpikleme:
  </p>
  <example layout="vertical">
    <in src="_files/test1.pickle" lang="text"/>
    <program src="_files/test1_out.py" lang="python"/>
    <out src="_files/test1_out.out" lang="text"/>
  </example>
  <p>
    Vidíme, že na načtené datové struktuře jsou normálně k dispozici všechny její atributy.
  </p>

</slide>
<slide title="Co lze zapiklit">

  <p>
    Zaserializovat je možné následující typy:
  </p>
  <ul>
    <li>
        <code>None</code>, <code>True</code>, <code>False</code>
    </li>
    <li>
        celá, reálná a komplexní čísla
    </li>
    <li>
        řetězce (unicodové), bajtové řetězce a bajtová pole
    </li>
    <li>
        n-tice, seznamy, množiny a slovníky, pokud obsahují piklitelné typy
    </li>
    <li>
        funkce definované na globální úrovni modulu
    </li>
    <li>
        vestavěné funkce definované na globální úrovni modulu
    </li>
    <li>
        třídy definované na globální úrovni modulu
    </li>
    <li>
        instance tříd, jejichž atributy <code>__dict__</code> a <code>__setstate__()</code> jsou piklitelné
    </li>
  </ul>
  
  <p>
    Přitom:
  </p>
  <ul>
    <li>
        Pokus o zapiklení nepiklitelného objektu vyhodí výjimku <em>PicklingError</em>, blíže neurčený počet bajtů však mezitím už může být zapsán do otevřeného proudu. Podobně při pokusu o piklení velmi rekurzivních dat můžete narazit na hranice rekurze (výjimka <em>RuntimeError</em>).
    </li>
    <li>
        Piklení probíhá na úrovni <em>plně kvalifikovaných jmen</em>, nikoli příslušných zdrojových kódů. Tzn. že funkce a třídy (včetně uživatelem definovaných) jsou zapikleny pod svým jménem a jménem rodičovského modulu. Při odpiklení pak musí být příslušný modul a v něm příslušná funkce či třída k dispozici.
    </li>
    <li>
        Ze tříd jsou piklena pouze data instancí. To proto, aby byl zapiklený objekt použitelný i tehdy, pokud základní třídu nějak (rozumně) upravíme.
    </li>
  </ul>

</slide>
<slide title="Příklad – funkce">

  <p>
    Když náš datový slovník doplníme o odkaz na funkci (tedy referenci), zapiklí se pouze tento odkaz..
  </p>
  <example layout="vertical">
    <program src="_files/test2_in.py" lang="python"/>
    <out src="_files/test2.pickle" lang="text"/>
  </example>
  
  <p>
    ..což znamená, že při odpiklovávání musí příslušná funkce existovat (i když bude dělat něco úplně jiného):
  </p>
  <example layout="vertical">
    <in src="_files/test2.pickle" lang="text"/>
    <program src="_files/test2_out.py" lang="python"/>
    <out src="_files/test2_out.out" lang="text"/>
  </example>
  <p>
    Druhou možností by bylo zapiklit funkci jako řetězec a při rozpiklování ji převést na funkční tvar pomocí <em>eval()</em>, ale...
  </p>

</slide>
<slide title="K (ne)bezpečnosti piklení">

    <p>
        Piklící formát není nijak zabezpečen proti nebezpečným datům =>
    </p>
    <h1 class="security">Nikdy neodpiklovávejte cizí data!</h1>
    <p>
        Odpiklená data jsou totiž převedena do bajtkódu a vzápětí vykonána (aby mohly být příslušné datové a programové struktury k dispozici), je tudíž <strong>velkým bezpečnostním rizikem</strong> bezhlavě odpiklovávat všechno, co se vám dostane pod ruku!
    </p>
    
    <p>
        Musíte-li pracovat s (nejen cizími) zapiklenými daty, mohou se vám hodit následujícíc dvě věci:
    </p>
    <ul>
        <li>
            Úpravou <em>Unpickler.find_class()</em> můžete omezit, které globální funkce a třídy bude povoleno odpiklit.
        </li>
        <li>
            Nástroje v modulu <code>pickletools</code> (viz <a href="?slajd=8">následující slajd</a>) umožňují prozkoumat „vnitřnosti“ zapiklených dat bez nebezpečí spouštění vytvořeného bajtkódu.
        </li>
    </ul>

</slide>
<slide title="Modul „pickletools“">

    <p class="enumerate">
        Modul <em>pickletools</em> toho umí více (mimo jiné optimalizovat zapiklená data), ale na tomto místě se o něm zmiňujeme především proto, že umožňuje prozkoumat „assembler“ piklicího protokolu zkoumaného souboru, aniž by při tom jako <code>pickle.load()</code> (či <code>loads()</code>) získaný pythoní bajtkód rovnou i vykonal.
    </p>
    <p>
        Programátor tak dostává do ruky nástroj, který mu umožňuje prozkoumat zapiklená data tak říkajíc „offline“, aniž by si jimi případně kompromitoval vlastní systém.
    </p>
    
    <p class="enumerate">
        V programátorském rozhraní modulu jsou k dispozici následující metody:
    </p>
    <ul>
        <li>
            <code>dis(pickle, out=None, memo=None, indentlevel=4, annotate=0)</code> – vrací symbolickou disasemblaci (do operačních kódů piklicího protokolu) vstupního <em>pickle</em>-souboru;
        </li>
        <li>
            <code>genops(pickle)</code> – vrací iterátor přes jednotlivé operační kódy vstupního <em>pickle</em>-souboru;
        </li>
        <li>
            <code>optimize(picklestring)</code> – vrací optimalizovanou verzi vstupního <em>pickle</em>-řetězce.
            <note>Zřejmě alespoň některá z verzí piklicího protokolu nezvládá přímo poskytnout optimalizovaný výstup a je možné odstranit přebytečné příkazy <code>PUT</code>.</note>
        </li>
    </ul>

</slide>
<slide title="Modul „pickletools“ – příkazová řádka">

    <p>
        Od verze Python'u 3.2 umožňuje modul <code>pickletools</code> i své přímé volání z příkazové řádky:
    </p>
    <example lang="bash">
        python -m pickletools [přepínače] soubor [soubor…]
    </example>
    <p>
        Dostupné přepínače jsou:
    </p>
    <ul>
        <li>
            <code>-a</code>, <code>--annotate</code><br/>
            doplní každý operační kód popisem jeho funkce
        </li>
        <li>
            <code>-o</code>, <code>--output=&lt;file></code><br/>
            jméno souboru pro zápis výstupu příkazu
            <note>Zdá se, že modul má zatím při použití tohoto přepínače problémy s výstupem pro ne-ASCII kódovaná data a struktury.</note>
        </li>
        <li>
            <code>-l</code>, <code>--indentlevel=&lt;num></code><br/>
            počet mezer použitých při odsazování úrovní výstupu
        </li>
        <li>
            <code>-m</code>, <code>--memo</code><br/>
            <!-- When multiple objects are disassembled, preserve memo between disassemblies. -->
            pro vícero překládaných objektů se pokusí udržet stejnou interní mapovací tabulku
            <note>Tohle je asi zrovna užitečnější spíš z programového API než tady.</note>
        </li>
        <li>
            <code>-p</code>, <code>--preamble=&lt;preamble></code><br/>
            při výpisu výstupů pro vícero vstupních souborů doplní před každý jednotlivý výpis uvedený text (ve výchozím nastavení jméno zpracovávaného souboru)
        </li>
    </ul>

</slide>
<slide title="Modul „pickletools“ – příklad (bez anotací)">

    <example layout="vertical">
        <cmd>python -m pickletools pt/test.pickle</cmd>
        <in lang="python">
            slovník = {
                'seznam': [1, 2, 3, range(3)],
                12: "Ahoj, světe!",
            }
        </in>
        <out src="_files/pt/test_plain.out" lang="text"/>
    </example>

</slide>
<slide title="Modul „pickletools“ – příklad (s anotacemi)">

    <example layout="vertical">
        <cmd>python -m pickletools -a pt/test.pickle</cmd>
        <in lang="python">
            slovník = {
                'seznam': [1, 2, 3, range(3)],
                12: "Ahoj, světe!",
            }
        </in>
        <out src="_files/pt/test_annotated.out" lang="text"/>
    </example>

</slide>
<slide title="Poznámka – protokol 0">

  <p>
    Pro zajímavost zkusme ještě zapiklit slovník z předchozích slajdů, ale tentokrát v nejstarším ASCII-protokolu <code>0</code>:
  </p>
  <example layout="vertical">
    <program src="_files/test0_in.py" lang="python"/>
    <out src="_files/test0.pickle" lang="text"/>
  </example>
  <note>
    Tady jsou nové řádky ve výpisu správně, protože formát <em>protokolu 0</em> je textový.
  </note>

</slide>


</lecture>
