<?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>Práce se soubory I</title>
  <date>2011-10-27</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>
    Se soubory v Python'u se na první pohled pracuje velmi podobně, jako kdekoliv jinde – soubor z daného umístění otevřeme, pracujeme s jeho obsahem a na konci ho zase spořádaně uzavřeme:
  </p>
  <example lang="python">
f = open('cesta/k/souboru', mode='r', encoding='utf-8')
...
f.close()
  </example>
  <p>
    Výchozí mód je <code>r</code> (tedy textový soubor otevřený pro čtení) a protože je pozičně na druhém místě, zdánlivě by pro textové soubory tedy stačilo psát <code>open('CESTA', 'r')</code> nebo rovnou <code>open('CESTA')</code>. Problém je v tom, že <em>encoding</em> je až <strong>třetí</strong> pojmenovaný parametr (druhým je nastavení mezipaměti <em>buffering</em>) a výchozí kódování je závislé na systému, takže <strong><code>open()</code> bez parametru <em>encoding</em> je v podstatě nepoužitelný (rozhodně nepřenosný)</strong>.
  </p>

</slide>
<slide title="Operační kontext „with“">

  <p>
    Protože cestou se může stát mnoho nepředvídaného, je dobré se pojistit a soubor pro případ takových nenadálých událostí stejně pěkně zavřít.
  </p>
  <note>
    Soubor se snažíme <em>čistě uzavřít</em> nejen proto, aby nezůstal „viset“ v nějakém nedefinovaném stavu, ale hlavně proto, že veškerá práce s ním je „bafrovaná“ (<em>buffered</em>), takže se snadno stane, že při nekorektním uzavření se část dat třeba vůbec nezapíše, protože zůstala viset připravená v mezipaměti (<em>buffer</em>).
  </note>
  <p>
    Po staru to člověk dělal soustavou bloků <code>try-finally</code>, ale v Python'u 3 je mnohem lepší použít příkaz <code>with</code> pro vytvoření příslušného operačního kontextu (<em>runtime context</em>):
  </p>
  <example lang="python">
with open('cesta/k/souboru', encoding='utf-8') as f:
    BLOK
  </example>
  <p>
    Uvnitř bloku máme pod identifikátorem <code>f</code> k dispozici otevřený <em>stream</em> (aneb „proud dat“) a můžeme s ním úplně normálně pracovat. Jakmile kód v bloku proběhne, ať už úspěšně nebo neúspěšně, tak je stream automaticky uzavřen (a nemusíme se o to tudíž starat sami).
  </p>
  <note>
    Technicky vzato objekt typu stream zde funguje jako „správce kontextu“ (<em>context manager</em>) – při vstupu do bloku je zavolána jeho „magická“ metoda <code>__enter__()</code>, při opuštění bloku pak <code>__exit__()</code>. Blíže viz <a class="external" href="http://diveintopython3.net/special-method-names.html#context-managers">B.11</a>.
  </note>

</slide>
<slide title="Vlastnosti proudu">
  
  <p>
    Otevřený soubor (tedy <em>stream</em>, resp. „proud dat“; zde pro případ <strong>textového souboru</strong>) má mnoho zajímavých vlastností:
  </p>
  <example lang="python">
>>> f = open('cestina.txt', encoding='utf-8')

>>> f
&lt;_io.TextIOWrapper name='cestina.txt' encoding='utf-8'>

>>> dir(f)
['_CHUNK_SIZE', '__class__', '__delattr__', '__doc__', '__enter__', '__eq__', 
 '__exit__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', 
 '__init__', '__iter__', '__le__', '__lt__', '__ne__', '__new__', '__next__', 
 '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', 
 '__str__', '__subclasshook__', '_checkClosed', '_checkReadable', 
 '_checkSeekable', '_checkWritable', 'buffer', 'close', 'closed', 'detach', 
 'encoding', 'errors', 'fileno', 'flush', 'isatty', 'line_buffering', 'name', 
 'newlines', 'read', 'readable', 'readline', 'readlines', 'seek', 'seekable', 
 'tell', 'truncate', 'writable', 'write', 'writelines']
  </example>
  <p>
    Na mnohé o souboru se můžeme ptát:
  </p>
  <example lang="python">
>>> f.name       # jméno souboru
'cestina.txt'
>>> f.encoding   # kódování souboru
'utf-8'
>>> f.mode       # způsob přístupu k souboru (zde r = „pouze pro čtení“)
'r'

>>> f.readable()   # Je proud možno číst?
True
>>> f.writable()   # Je do souboru možno zapisovat?
False

# Podporuje proud náhodný přístup? Aneb..
# ..Je možno použít metody seek(), tell() a truncate()?
>>> f.seekable()
True
  </example>

  <p>
    Všimněte si, že (dosud) otevřený soubor se patřičně hlásí jako nezavřený a naopak:
  </p>
  <example lang="python">
>>> f.closed
False
>>> f.close()
>>> f.closed
True
  </example>

</slide>
<slide title="Konstruktor „open()“">

  <p>
    Plný konstruktor pro otevření souboru je nepřekvapivě dosti komplikovaný:
  </p>
  <example lang="python">
      open(
          SOUBOR
          [, mode='r']
          [, buffering=-1]
          [, encoding=None]
          [, errors=None]
          [, newline=None]
          [, closefd=True]
          [, opener=None]
      )
  </example>
  <p>
    Teoreticky jediným povinným parametrem je sice pouze cesta k souboru (která možná trošku překvapivě <a href="https://docs.python.org/3/glossary.html#term-path-like-object" class="external">nemusí být pouze řetězcem</a>), se kterým chceme pracovat, ale jelikož výchozím módem je textový pro čtení, máme problém s přenositelností kódování<sup>*</sup>.
  </p>
  <note>
    <sup>*</sup> Ne, UTF-8 stále není automatickým výchozím kódováním na všech operačních systémech :-(
  </note>
  <p>
    Další parametry pak jsou:
  </p>
  <ul>
    <li>
      <em>mode</em> – <a href="?slajd=5">způsob otevření souboru</a> pro práci s ním;
    </li>
    <li>
      <em>buffering</em> – <a href="?slajd=13">nastavení práce s mezipamětí</a>;
    </li>
    <li>
      <em>encoding</em> – algoritmus kódování <strong>textového souboru</strong>;
      <note>
        Není-li zadáno, zjistí se aktuální automaticky pomocí <code>locale.getpreferredencoding(False)</code>.
      </note>
    </li>
    <li>
      <em>errors</em> – varianta <a href="texts.xml?slajd=11">řešení chyb při dekódování</a> <strong>textových souborů</strong>;
    </li>
    <li>
      <em>newline</em> – velmi zajímavý parametr umožňující <a href="texts.xml?slajd=12">změnu chování vůči koncům řádek</a> v <strong>textových souborech</strong>.
    </li>
  </ul>
  <p>
    Parametr <em>closefd</em> se může uplatnit v situaci, kdy cesta k souboru není zadána řetězcem, a parametr <em>opener</em> pak umožňuje i použít vlastní volatelný objekt pro otevření požadovaného cíle. Pro obé konzultujte prosím přímo <a href="https://docs.python.org/3/library/functions.html#open" class="external">oficiální dokumentaci</a>.
  </p>

</slide>
<slide title="Módy otevření souboru I">
  
  <p class="enumerate">
    <strong>Textový soubor</strong> (volitelně identifikovaný příznakem <code>t</code>) můžeme otevřít v několika módech:
  </p>
  <ul>
    <li>
        <strong>r</strong> – soubor je otevřen pouze pro čtení
    </li>
    <li>
        <strong>w</strong> – soubor je otevřen pro zápis (již existující neprázdný soubor bude smazán)
    </li>
    <li>
        <strong>x</strong> – soubor je exkluzivně vytvořen (tzn. že přístup selže s výjimkou <code>FileExistsError</code>, pokud soubor již existuje) a otevřen pro zápis
    </li>
    <li>
        <strong>a</strong> – soubor je otevřen pro přidávání (<strong>zapsaná data budou přidána na konec</strong>)
    </li>
    <li>
        <strong>r+</strong> – soubor je otevřen pro čtení i zápis (čili musí již existovat a popisovač je nastaven na jeho začátek)
    </li>
    <li>
        <strong>w+</strong> – soubor je otevřen pro čtení i zápis (již existující neprázdný soubor bude smazán)
    </li>
    <li>
        <strong>a+</strong> – soubor je otevřen pro čtení i přidávání (přitom čtecí a zapisovací ukazatel jsou na sobě nezávislé; sice na začátku míří oba na konec souboru, ale čtecí můžete např. pomocí <code>seek()</code> přesunout, zatímco <strong>zápis probíhá vždy na konci</strong>)
    </li>
  </ul>
  <note>
    Nezadáme-li mód otevření souboru, je výchozí hodnotou <code>tr</code>, to jest <em>textový soubor, pouze čtení</em>.
  </note>
  
  <p class="enumerate">
    Pro případ <strong>binárního souboru</strong> kombinujeme výše uvedené módy s příznakem <code>b</code> (tedy např. <code>br</code> otevře binární soubor pro čtení).
  </p>

</slide>
<slide title="Módy otevření souboru II">

    <p>
        Nejen mně přijdou módy otevření souboru poněkud zmatené. <a href="https://stackoverflow.com/a/30566011" class="external">Andrzej Pronobis / Renato Byrro</a> vytvořili rozhodovací diagram, který – pokud jsem se úplně neztratil v testování, což by mě ani trochu nepřekvapilo ^_^ – odpovídá realitě a je docela přehledný:
    </p>
    <!--img src="_files/ExWNT-white-bg.png" width="1182" height="702" alt="módy otevření souboru" /-->
    <img src="_files/ExWNT-white-bg.png" width="800" alt="módy otevření souboru" />
    <p>
        PS: Mód <em>x</em> patří do větve <em>writing</em> – je to tak trochu „nafintěné“ <em>w</em>. Mód <em>x+</em> můžete také použít, ale vzhledem k povaze věci se chová úplně stejně jako <em>x</em>.
    </p>

</slide>
<slide title="Čtení z proudu">

  <p>
    Z otevřeného proudu (<em>stream</em>u) je možno číst data několika různými způsoby. Základní představuje metoda <code>read()</code>:
  </p>
  <ul>
    <li>
        načtení celého proudu najednou:<br/>
        <code>content = stream.read()</code>
        <note>
          Pro binární stream je volání <code>read()</code> bez parametrů (resp. s <code>-1</code>) v podstatě ekvivalentní zavolání metody <code>readall()</code>.
        </note>
    </li>
    <li>
        načtení daného počtu <strong>znaků</strong> (pro případ <em>textového souboru</em>) nebo <strong>bajtů</strong> (pro případ <em>binárního souboru</em>):<br/>
        <code>part = stream.read(ZNAKŮ|BAJTŮ)</code>
    </li>
  </ul>
  <example layout="horizontal">
    <in src="_files/cestina.txt" lang="text"/>
    <program src="_files/cestina.py" lang="python"/>
    <out src="_files/cestina.out" lang="text"/>
  </example>
  <note>
    Shodou okolností má tato oblíbená česká testovací věta právě 39 znaků :-)
  </note>

</slide>
<slide title="Zápis do proudu">

    <p>
        Podobně jako u čtení z proudu máme pro zápis k dispozici především základní metodu:
    </p>
    <ul>
        <li>
            <code>stream.write(ŘETĚZEC|BAJTY)</code>
        </li>
    </ul>
    <p>
        Tato metoda provede zapsání daných <strong>znaků</strong> (pro případ <em>textového souboru</em>) nebo <strong>bajtů</strong> (pro případ <em>binárního souboru</em>):
    </p>
    <example lang="python">
        # otevřme soubor v textovém módu a kódování UTF-8 pro zápis (vlastně „přepis“)
        >>> f = open('test', 'w', encoding='utf-8')
        # metoda write() vrací počet zapsaných znaků (jsme v módu 't')
        >>> f.write('Ahoj, světe!')
        12
        >>> f.write('Jak se máš?')
        11
        >>> f.close()

        # podívejme se, co jsme zapsali
        >>> f = open('test', 'r', encoding='utf-8')
        >>> f.read()
        'Ahoj, světe!Jak se máš?'
        >>> f.close()
    </example>

</slide>
<slide title="Znaky vs bajty I">

    <p>
        Základní třídou pro vstup a výstup je třída <code>IOBase</code>. Z ní dědí jak třída <code>TextIOBase</code> pro práci s textovými soubory, tak třída <code>RawIOBase</code> pro práci se soubory binárními. 
    </p>
    <p>
        Přitom mezi textovými a binárními soubory jsou dva, a to zcela zásadní, rozdíly:
    </p>
    <ol>
        <li>
            <em>Binární</em> soubory jsou čteny (a zapisovány) <strong>po bajtech</strong>, <em>textové</em> jsou zpracovávány <strong>po znacích</strong> (přičemž jeden znak zabírá podle použitého kódování místo jednoho či několika bajtů).
        </li>
        <li>
            V <em>textových souborech</em> je automaticky prováděna <strong>konverze konců řádků</strong> různých platforem (<code>\n</code> na Unixu a MacOS X+, <code>\r\n</code> na Windows, <code>\r</code> na MacOS 9-) na jednotné pracovní <code>\n</code> (konverze je samozřejmě obousměrná), v <em>binárních</em> pochopitelně nikoli.
        </li>
    </ol>

</slide>
<slide title="Znaky vs bajty II">

  <p>
    Pamatovat na rozdíl mezi <em>bajty</em> a <em>znaky</em> je přitom skutečně důležité:
  </p>
  <example lang="python">
# otevřme textový soubor v kódování UTF-8 
>>> f = open('japonstina.txt', encoding='utf-8')

>>> f.tell()   # s čertvě otevřeným souborem stojíme na začátku proudu
0
>>> f.read()   # načtěmež celý proud
'狼.cz'
>>> f.tell()   # jsme na jeho konci – v bajtech je delší, než ve znacích!
6

>>> f.seek(0)   # vraťme se na začátek
0
>>> f.tell()
0

>>> f.read(1)   # přečtěmež jeden znak..
'狼'
>>> f.tell()    # ..ale posunuli jsme se o tři bajty dále!
3

>>> f.read(1)   # '.' je ve spodní polovině ASCII-tabulky, takže načtení
                #   jednoho znaku..
'.'
>>> f.tell()    # ..nás posune pouze o jeden bajt dále
4

>>> f.close()
  </example>

</slide>
<slide title="Znaky vs bajty III">

  <p>
    Asi už tušíte, do jakých nesnází se dostanete, pokud se pokusíte na <em>textovém</em> proudu posouvat začátky čtení dalšího znaku do nesmyslných míst:
  </p>
  <example lang="python">
>>> f = open('japonstina.txt', encoding='utf-8')
>>> f.tell()
0

>>> f.seek(1)   # posuňme se o jeden bajt po streamu dopředu..
1
>>> f.tell()
1
>>> f.read(1)   # ..a pokusme se nyní načíst jeden znak
Traceback (most recent call last):
  File "&lt;stdin>", line 1, in &lt;module>
  File "/usr/lib/python3.1/codecs.py", line 300, in decode
    (result, consumed) = self._buffer_decode(data, self.errors, final)
UnicodeDecodeError: 'utf8' codec can't decode byte 0x8b in position 0: 
        unexpected code byte
  </example>
  <note>
    Chybové hlášení zalomeno pro potřeby zobrazení. Proč k němu vůbec došlo, viz <a href="/materialy/text/encoding.xml?slajd=9">algoritmus UTF-8</a>.
  </note>
  
  <p>
    Kdybyste si neměli pamatovat nic jiného: <strong>Nikdy nemíchejte binární a textové proudy dohromady!</strong> ^_^
  </p>

</slide>
<slide title="Mezipaměť („buffer“) I">

  <p>
    Zakončeme náš úvod do práce se soubory jednou velmi důležitou poznámkou:
  </p>
  <blockquote>
    Z důvodů vnitřní implementace a optimalizace práce s proudy (<em>stream</em>) probíhá veškerá komunikace přes tzv. <strong>mezipaměť</strong> (<em>buffer</em>), odkud jsou data přesouvána až podle požadavků systému, není-li programem vynuceno jinak.
  </blockquote>
  <p>
    V praxi to například v případě zápisu znamená, že data nemusí být před vyžádaným uzavřením proudu vůbec skutečně zapsána!
  </p>
  <p>
    Někdy (podle způsobu práce i často) se nám může hodit vnutit systému zápis připravených dat dříve, než to zařídí uzavření proudu zavoláním jeho metody <em>close()</em> nebo ukončením práce v bloku <em>with</em>. A právě tuto činnost má na starosti metoda proudu <strong>flush()</strong>.
  </p>
  <note>
    Uvedené se mimochodem týká třeba i výpisu znaků na konzoli při použití zdánlivě obyčejné a bezproblémové funkce <em>print()</em>! Pokud byste na to někdy narazili, tak funkce <em>print()</em> ve výchozím nastavení tiskne na zařízení <code>sys.stdout</code>, což je – také ve výchozím nastavení – právě konzole. Vynucení výpisu dosud nabafrovaných dat pak zařídí podle verze Python'u buď nastavení stejnojmenného parametru přímo na funkci <em>print()</em> (Python 3.3+)..
    <example lang="python">
      print(…, flush=True)
    </example>
    ..nebo zavolání metody <em>flush()</em> (pro Python 3.2-):
    <example lang="python">
      import sys
      print(…)
      sys.stdout.flush()
    </example>
  </note>

</slide>
<slide title="Mezipaměť („buffer“) II">

    <p>
        Jak jsme zmiňovali už <a href="?slajd=1">na prvním slajdu</a>, historická pozice kódování textového souboru až jako třetího pojmenovaného parametru plus neexistující konsenzus pro výchozí kódování nám ztěžují práci se soubory. A tím parametrem, kvůli kterému musíme vždy vypisovat <em>encoding=</em> je právě parametr řídící způsob práce s mezipamětí: <strong><code>buffering</code></strong>
    </p>
    
    <p class="enumerate">
        Jeho možná nastavení jsou následující:
    </p>
    <ul>
        <li>
            <strong>0</strong> – v binárním módu vypíná bafrování
        </li>
        <li>
            <strong>1</strong> – v textovém módu přepíná bafr na řádkový
        </li>
        <li>
            <strong>&gt;1</strong> – nastavuje příslušnou velikost bafru
        </li>
    </ul>
    
    <p class="enumerate">
        Není-li atribut <em>buffering=</em> uveden (a má tak výchozí hodnotu -1), nastoupí heuristika:
    </p>
    <ul>
        <li>
            u <em>„interaktivních“ textových souborů</em> (tedy těch, pro něž metoda <em>stream</em>u <code>isatty()</code> vrací <code>True</code>) je bafrování nastaveno na řádkové;
        </li>
        <li>
            u <em>binárních</em> a ostatních <em>textových</em> souborů se Python pokusí zjistit velikost systémových bloků (typicky skončí s číslem 4096 nebo 8192).
        </li>
    </ul>

</slide>


</lecture>
