<?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>Obsluha externích procesů – „subprocess“</title>
  <date>2018-03-05 (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 <em>subprocess</em> je dnes preferovaný (a podstatně bezpečnější) způsob práce s externími programy. Od Python'u 3.5 se jedná především o funkci <a href="?slajd=2"><strong><em>run()</em></strong></a>, ve starších verzích nebo pro speciálnější použití pak o obecnou třídu <a href="?slajd=9"><strong><em>Popen()</em></strong></a>.
  </p>
  <p>
    Dále máme k dispozici několik užitečných konstant (<code>subprocess.PIPE</code>, <code>subprocess.STDOUT</code>, <code>subprocess.DEVNULL</code>) a také přehršel dalších pomocných funkcí (dnes už převážně nedoporučovaných).
  </p>
  <notes>
    <note>
        Dostupnost různých metod se liší platformu od platformy.
    </note>
    <note>
        Některé z metod mohou vyvolat <em>deadlock</em>.
    </note>
  </notes>
  <p>
    PS: Dříve se pro dosažení podobného efektu používala především funkce <em>os.system()</em> a armáda funkcí <em>os.spawn*()</em> (případně i <em>os.exec*()</em>, ty ovšem kompletně nahrazovaly aktuální proces volaným a nevracely se z něj).
  </p>

</slide>
<slide title="I. „run()“">

  <p>
    Dnes preferovaná metoda spouštění externích procesů (a získávání jejich výsledků) je pomocí funkce <strong><em>run()</em></strong>, která má v nejobvyklejších případech následující argumenty:
  </p>
  <pre>subprocess.run(args, *, stdin=None, input=None, stdout=None, stderr=None, shell=False,
               cwd=None, timeout=None, check=False, encoding=None, errors=None)  </pre>
  <p>
    Tato funkce zavolá externí proces a čeká na jeho ukončení. Po návratu do volajícího prostředí vrátí instanci třídy <em>CompletedProcess</em> naplněnou návratovými hodnotami z proběhlého externího procesu.
  </p>
  <p>
    PS: Ve skutečnosti jsou parametry funkce <em>run()</em> až na tři (<em>input</em>, <em>timeout</em>, <em>check</em>) stejné jako u <em>Popen()</em> a také se volají jejich přímým předáním na ni.
  </p>

</slide>
<slide title="Základní použití">

  <p>
    Nejjednodušší způsob použití představuje přímé zavolání externího programu bez jakýchkoli parametrů..
  </p>
  <example lang="python">
    import subprocess

    # zavolej příkaz 'ls'
    subprocess.run("ls")
  </example>
  <p>
    ..případně s parametry:
  </p>
  <example lang="python">
    import subprocess

    # zavolej příkaz 'ls -al'
    args = ['ls', '-al']
    subprocess.run(args)
  </example>
  <p>
    Výstup programu se v tomto případě „vysype“ tam, kam má Python zrovna přístup, nejčastěji tedy na konzoli.
  </p>
  <p>
    PS: Mimochodem na první pohled zdánlivě nešikovné uvádění parametrů jako seznamu řetězců má tu výhodu, že se vyhneme většině trablů spojených se správným iskejpováním vstupu na příkazové řádce.
  </p>

</slide>
<slide title="„stdout“ – přesměrování výstupu">

  <p>
    Výstup externího programu si můžeme snadno přesměrovat z výchozího standardního výstupu třebas do souboru pomocí atributu <strong><em>stdout</em></strong>:
  </p>
  <example layout="vertical">
    <program src="_files/execution/process+.1.py" lang="python"/>
    <out lang="text">
      A: CompletedProcess(args='ls', returncode=0)
      B: CompletedProcess(args=['ls', '-al'], returncode=0)
    </out>
    <out src="_files/execution/process+.1.out" lang="text"/>
  </example>

</slide>
<slide title="„input“ – vlastní vstup (1)">

  <p>
    Vstup pro externí program můžeme zařídit pomocí specifického parametru <strong><em>input=b''</em></strong>:
  </p>
  <example layout="vertical">
    <program src="_files/execution/process+.2a.py" lang="python"/>
    <out src="_files/execution/process+.2a.out" lang="text"/>
  </example>
  <note>
      Ve skutečnosti je vstup přesměrován na parametr <em>stdin</em> metody <em>Popen()</em> (nedají se proto použít najednou!). V našem příkladu je jím text <em>hello\nhow\nare\nyou</em>, zadaný ovšem jako binární řetězec.
  </note>

</slide>
<slide title="„input“ – vlastní vstup (2)">

  <p>
    Python 3.6 s sebou přinesl velmi pohodlné zjednodušení – nastavení parametru <strong><em>encoding</em></strong> způsobí změnu vyhodnocování proudů <em>stdin</em>, <em>stdout</em> a <em>stderr</em> z bajtových řetězců na příslušné textové:
  </p>
  <example layout="vertical">
    <program src="_files/execution/process+.2b.py" lang="python"/>
    <out src="_files/execution/process+.2b.out" lang="text"/>
  </example>
  <p>
      PS: Kromě toho můžete pomocí parametru <a href="/materialy/python/files/streams.xml?slajd=8"><strong><em>errors</em></strong></a> nastavit i vlastní obsluhu chyb vzniklých při (de)kódování takto vzniklých řetězcových dat. V podstatě pro překlad z bajtů na řetězce stačí, aby byl nastavený alespoň jeden z těchto parametrů.
  </p>

</slide>
<slide title="Objekt vráceného stavu „CompletedProcess“">

  <p>
    Návratový objekt ukončeného externího procesu má několik užitečných vlastností:
  </p>
  <example layout="vertical">
    <program src="_files/execution/process+.3.py" lang="python"/>
    <out src="_files/execution/process+.3.out" lang="text"/>
  </example>
  <notes>
    <note>
      Parametr <code>shell=True</code> je zde nastaven proto, aby se vstupní řetězec <code>"ls; exit 1"</code> správně vyhodnotil – bez něj se Python přímo pokusí zavolat zadaný řetězec, což neprojde, zatímco s ním Python spustí shell (na Linuxu typicky <em>/bin/sh</em>), v jehož kontextu kód zavolá.
    </note>
    <note class="security">
      V souvislosti s tím je asi jasné, že pustit <code>shell=True</code> (a <code>os.system()</code>) na neošetřený uživatelský vstup, je tak z hlediska bezpečnosti jedna z nejhorších věcí, co vůbec můžete v Python'u udělat… Pro podobné případy nezapomeňte alespoň vstup <a href="https://docs.python.org/3/library/shlex.html#shlex.quote" class="external">náležitě odiskejpovat</a> pomocí <code>shlex.quote()</code>.
    </note>
  </notes>

</slide>
<slide title="„check=True“ – zachycení chyby">

  <p>
    Případná chyba ve volání externího procesu se tedy projeví především v nastavení parametru <code>CompletedProcess.returncode</code>. Chceme-li na ni nějak zareagovat, musíme nastavit parametr volání <strong><em>check=True</em></strong>, což způsobí vyhození výjimky:
  </p>
  <example layout="vertical">
    <program src="_files/execution/process+.4.py" lang="python"/>
    <out src="_files/execution/process+.4.out" lang="text"/>
  </example>

</slide>
<slide title="„stderr“ – odchycení chybového výstupu">

  <p>
    Pokud nechceme (nebo naopak chceme), aby se případné chyby z externího procesu objevily mezi jeho normálním výstupem, stačí přesměrovat chybový výstup pomocí parametru <strong><em>stderr</em></strong>:
  </p>
  <example layout="vertical">
    <program src="_files/execution/process+.5.py" lang="python"/>
    <out src="_files/execution/process+.5.out" lang="text"/>
  </example>
  <note>
    V zájmu zjednodušení jsem zde přesměroval standardní výstup do chybového, který jsem následně nastavením <code>stderr=subprocess.PIPE</code> zachytil, abych si ho mohl přečíst v návratovém objektu.
  </note>

</slide>
<slide title="„timeout“">

  <p>
    Pro případ, že by se volání externího procesu „zadrhlo“, je k dispozici omezovač v podobě atributu <strong><em>timeout=SEKUND</em></strong>, který zajistí vyhození výjimky <code>subprocess.TimeoutExpired</code>:
  </p>
  <example layout="vertical">
    <in src="_files/execution/timeout.py" lang="python"/>
    <program src="_files/execution/process+.6.py" lang="python"/>
    <out src="_files/execution/process+.6.out" lang="text"/>
  </example>

</slide>
<slide title="II. „Popen()“">

  <p>
    Ve skutečnosti za veškerou funkcionalitou stojí úplně základní třída <strong><em>Popen</em></strong>, která – až na <a href="?slajd=2">zmíněné tři parametry</a> – vlastně zpracovává volání funkce <em>run()</em>:
  </p>
  <pre>subprocess.Popen(args, bufsize=0, executable=None, stdin=None, stdout=None, stderr=None, 
                 preexec_fn=None, close_fds=False, shell=False, cwd=None, env=None, 
                 universal_newlines=False, startupinfo=None, creationflags=0, 
                 restore_signals=True, start_new_session=False, pass_fds=(), 
                 *, encoding=None, errors=None)  </pre>
  <p>
    Až do Python'u 3.4 to byl i preferovaný způsob volání externích procesů, nicméně dnes je tu především pro ty, kterým z nějakého důvodu nestačí <em>run()</em>. Oproti němu se liší především ve způsobu, jakým se zahajuje zpracování externího procesu a jak se mu předávají parametry.
  </p>

</slide>
<slide title="Parametry">

  <p>
    Volání externích procesů je dosti flexibilní – s výhodou můžeme použít zvláště následující:
  </p>
  <ul>
    <li>
        Parametry <code>stdin</code>, <code>stdout</code> a <code>stderr</code>, které slouží k přesměrování vstupu, výstupu a chybového výstupu volaného procesu.
    </li>
    <li>
        Parametr <code>cwd</code> slouží ke změně adresáře, v němž dojde k zavolání externího procesu.
        <note>
            Cesta k procesu nemůže být zadána relativně vůči tomuto adresáři.
        </note>
    </li>
    <li>
        Parametr <code>env</code> slouží k modifikaci proměnných prostředí pro potřeby spouštěného externího procesu.
    </li>
    <li>
        Obsluhu <em>standardního proudu</em> <code>subprocess.PIPE</code> zajišťuje metoda <code>communicate([input=None])</code>. Jejím úkolem je posílat data do <em>stdin</em> (v tom případě vyžaduje nastavení <code>stdin=subprocess.PIPE</code>) a číst je ze <em>stdout</em> a <em>stderr</em> (čeká přitom na ukončení volaného procesu).
        <notes>
            <note>
                Její nepovinný parametr je ve výchozím nastavení <code>None</code> (tj. žádná vstupní data), jinak to musí být bajtový řetězec. (Což je v ostrém kontrastu s Pythonem 2.x – tam to byl řetězec. Změna je to ale pochopitelná, protože Python 3.x narozdíl od Python'u 2.x striktně rozlišuje mezi binárními 8-bitovými daty a řetězci.)
            </note>
            <note>
                Stejně tak dobře ale nemusíte přes <code>communicate()</code> posílat nic a vstup do programu zařídit například ze souboru pomocí <code>stdin=open()</code> a podobně.
            </note>
        </notes>
    </li>
    <!--li>
    </li-->
  </ul>
  <p class="security">
    PS: Samozřejmě nezapomeňte na číhající <a href="?slajd=7">bezpečností problém s parametrem <code>shell</code></a>!
  </p>

</slide>
<slide title="Vlastní vstup">

  <p>
    Vyvolání externího programu se vstupem ze standardního proudu a výstupem do souboru:
  </p>
  <example layout="vertical">
    <program src="_files/execution/process.1.py" lang="python"/>
    <out src="_files/execution/process.1.out" lang="text"/>
  </example>
  <notes>
    <note>
        Voláme tedy externí program <em>sort</em>.
    </note>
    <note>
        Vstup je pomocí parametru <em>stdin=subprocess.PIPE</em> přesměrován ze standardního proudu, je jím tedy text <em>hello\nhow\nare\nyou</em>, zde zadaný ve výchozím nastavení jako bajtový objekt.
    </note>
    <note>
        Výstup je směřován do souboru <em>process.1.out</em>.
    </note>
  </notes>

</slide>
<slide title="Přesměrování vstupu i výstupu">

  <p>
    Složitější varianta předchozího – standardní i chybový výstup přesměrovány také na standardní proud:
  </p>
  <example layout="vertical">
    <program src="_files/execution/process.2.py" lang="python"/>
    <out src="_files/execution/process.2.out" lang="text"/>
  </example>

</slide>
<slide title="Odchycení chyby I">

  <p>
    Příklad zachycení chybového hlášení:
  </p>
  <example layout="vertical">
    <in src="_files/execution/error.py" lang="python"/>
    <program src="_files/execution/process.3.py" lang="python"/>
    <out src="_files/execution/process.3.out" lang="text"/>
  </example>
  <note>
    Při použití speciální hodnoty <em>stderr=subprocess.STDOUT</em> by byl chybový výstup přesměrován na stejné „zařízení“ jako <em>stdout</em> a uvedené chybové hlášení by tak bylo součástí proměnné <em>output</em>.
  </note>

</slide>
<slide title="Odchycení chyby II">

  <p>
    Návratový kód ukončivšího se programu se pochopitelně schovává ve vlastnosti <strong><em>returncode</em></strong> volaného procesu:
  </p>
  <example layout="vertical">
    <in src="_files/execution/error.py" lang="python"/>
    <program src="_files/execution/process.4.py" lang="python"/>
    <out src="_files/execution/process.4.out" lang="text"/>
  </example>
  <note>
    Až na výpis návratového kódu je program jinak úplně stejný jako předchozí.
  </note>

</slide>


</lecture>
