<?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>Funkce vyššího řádu</title>
  <date>2024-11-15</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>
        Jelikož v Python'u hrají funkce velmi důležitou roli, nepřekvapí, že v rámci knihovního modulu <em>functools</em> je k dispozici armáda metod pro funkce tzv. <strong>vyššího řádu</strong> (<em>higher order functions</em>) – funkce, které operují na funkcích a (případně) vrací další funkce.
    </p>
    <note>
        A protože v Python'u je funkce (přinejmenším) všechno, co implementuje magickou metodu <code>__call__()</code>, tak to nemusí být jen funkce v klasickém smyslu slova.
    </note>

</slide>
<slide title="„wraps()“ – kopie otisku funkce">

    <p>
        U <a href="decorators.xml">dekorátorů</a> jsme si říkali, že odekorovaná funkce je (či bývá) kompletně nahrazena uzávěrovou funkcí vrácenou z dekorátoru. Což má tu nepříjemnou vlastnost, že když se někdo začne šťourat v jejích vlastnostech, tak zjistí, že je to úplně jiná funkce.
    </p>
    <p>
        Snažit se přepsat potřebné vlastnosti dekorované funkce je otravné a navíc zbytečné, protože uvedenou práci za nás udělá dekorátor (sic! :-) <code>functools.wraps</code>:
    </p>
    <example lang="python">
        from functools import wraps
         
        def dekorátor(funkce):
            @wraps(funkce)
            def _dekorátor(*args, **kwargs):
                ...
            return _dekorátor
    </example>
    <p>
        PS: <code>@wraps</code> je příjemná dekorátorová zkratka pro funkci vyššího řádu <a href="https://docs.python.org/3/library/functools.html#functools.update_wrapper" class="external"><code>update_wrapper()</code></a>.
    </p>

</slide>
<slide title="„partial()“ – částečné vyhodnocení některých argumentů">

    <p>
        Představte si, že ve svém programu používáte nějakou funkci s jistým přednastavením některých parametrů tak často, že už vám jejich neustálé přepisování začne lézt krkem. Nemusí, protože od toho je tu funkce <code>functools.partial()</code>:
    </p>
    <example lang="python">
        from functools import partial
        
        print_bez_odřádkování = partial(print, end='')
        print_do_souboru = partial(print, file=STREAM)
        open_unicode = partial(open, encoding='utf-8')
    </example>
    <p>
        Navzdory příkladům výše to funguje i pro nepojmenované parametry, jak ukazuje příklad přímo z dokumentace:
    </p>
    <example lang="python">
        >>> basetwo = partial(int, base=2)
        >>> basetwo.__doc__ = 'Convert base 2 string to an int.'
        >>> basetwo('10010')
        18
    </example>
    <p>
        PS: Jelikož metody objektů jsou také funkce, jen v trochu jiném kontextu, existuje pro ně obdoba této funkce v podobě <code>functools.partialmethod</code>.
    </p>

</slide>
<slide title="„reduce()“ – redukce iterátorů na jednu hodnotu">

    <p>
        Nepotřebujete-li znát všechny mezivýsledky postupné redukce iterovatelného objektu předloženou <em>binární funkcí</em> (což zařídí <a href="/materialy/python/iterators/itertools.xml?slajd=9"><code>itertools.accumulate()</code></a>), můžete použít funkci <code>functools.reduce()</code>:
    </p>
    <example lang="python">
        >>> from functools import reduce

        # Vlastně provede ((((5 + 2) + 4) + 3) + 6):
        >>> reduce(lambda x, y: x + y, [5, 2, 4, 3, 6])
        20
    </example>
    <p>
        Jejím prvním parametrem je tedy binární funkce (funkce o dvou parametrech mezi kterými implementuje <em>binární operaci</em>) a druhým pak příslušný iterovatelný objekt. Případný třetí parametr pak představuje počáteční (inicializační) hodnotu pro operaci a je předřazen prvkům zpracovávaného iterátoru:
    </p>
    <example lang="python">
        >>> from functools import reduce

        # Vlastně provede (((((1 + 5) + 2) + 4) + 3) + 6):
        >>> reduce(lambda x, y: x + y, [5, 2, 4, 3, 6], 1)
        21
    </example>
    <note>
        Pokud inicializační parametr není přítomen a vstupní sekvence má právě délku 1, vrací se celkově právě jen tento první prvek.
    </note>

</slide>
<slide title="„singledispatch()“ – přetěžování v prvním argumentu">

    <p>
        Jelikož <strong>dekorátory jsou mocné</strong>, přinejmenším od doby, kdy si Python hraje na typy (konkrétně tedy od verze 3.4), je možné pythoní funkce svým způsobem i <strong>přetěžovat</strong>, byť pouze <strong>na základě prvního argumentu</strong>.
    </p>
    <p>
        Uvedené téma je poněkud obsáhlejší a proto je zpracováno v samostatné přednášce o <a href="generic.xml">generických funkcích</a>.
    </p>

</slide>
<slide title="„lru_cache()“ – podpora pro memoizaci">

    <p>
        Dekorátor <code>lru_cache()</code> si ukládá slovník argumentů dekorované funkce a jim odpovídajících výsledků, ale pouze do jisté hloubky (LRU = <em>least recently used</em>; ve výchozím nastavení 128). To pochopitelně značně zrychluje opětovná volání funkce pro stejné argumenty, protože se už nemusí znovu počítat nebo stahovat z internetu a podobně. Vzhledem k tomu <strong>má</strong> ale pochopitelně <strong>smysl dekorovat pouze funkce</strong>, které:
    </p>
    <ul>
        <li>
            opravdu budete často během běhu programu volat se stejnými parametry;
        </li>
        <li>
            argumenty jsou hešovatelné a zadávané pokaždé ve stejném pořadí (stávají se totiž klíči slovníku);
        </li>
        <li>
            nemají vedlejší efekty (těžko kešovat něco, co nezávisí pouze na svých argumentech nebo nevrací na daný dotaz pokaždé totéž).
            <note>
                Třebas <code>time()</code> nebo <code>random()</code> jsou krásný protipříklad.
            </note>
        </li>
    </ul>
    <p>
        Zatímco rekurzivní funkce bez vedlejších efektů jsou tedy naprosto jasný kandidát na použití keše, nemá naopak vůbec smysl kešovat generátory nebo asynchronní funkce.
    </p>
    <p>
        PS: Mezipaměť užívaná tímto dekorátorem je <em>threadsafe</em>, takže zůstává konzistentní, i když je volána z různých vláken ve stejnou dobu.
    </p>

</slide>
<slide title="„cache()“ ==  „lru_cache(maxsize=None)“">

    <p>
        V některých případech má smysl LRU vypnout a kešovat všechna volání dekorované funkce, například u hluboce rekurzivních funkcí. To zařizuje volání ve tvaru <code>lru_cache(maxsize=None)</code>. Protože je to užitečné a v některých scénářích často užívané, Python 3.9 na to zavedl zkratku <code>@cache</code>:
    </p>
    <example lang="python" src="_files/memoization.py" />
    <p>
        Dekorátor <code>@cache</code> je pro uvedená použití rychlejší (a také méně náročný) než <code>@lru_cache</code>, protože nemusí řešit odstraňování hodnot, které překročily nastavený limit, z mezipaměti.
    </p>

</slide>
<slide title="„totalordering()“ – porovnávání instancí tříd">

    <p>
        Mají-li instance tříd mezi sebou býti porovnatelné, je třeba na třídě nadefinovat odpovídající <a href="/materialy/python/sorting/advanced.xml?slajd=6">porovnávací magické metody</a>. Přitom mají-li být porovnávací operace navzájem konzistentní, je většinu z nich možno odvodit pomocí jiných.
    </p>
    <p>
        A právě to dokáže <strong>třídní dekorátor <code>totalordering()</code></strong> – z definice jedné jediné z metod pro operace <code>&lt;</code>, <code>&lt;=</code>, <code>&gt;</code> a <code>&gt;=</code> (metody <code>__lt__()</code>, <code>__le__()</code>, <code>__gt__()</code> a <code>__ge__()</code>) odvodit zbývající. Pokud je na třídě ideálně nadefinována i metoda pro rovnost <code>==</code> (<code>__eq__()</code>), odvodí i konzistentní nerovnost <code>!=</code> (metoda <code>__ne__()</code>). Příklad je k vidění <a href="/materialy/python/sorting/advanced.xml?slajd=7">u přednášky na porovnávání</a> v Python'u.
    </p>
    <note>
        PS: Dopočítané operátory sice šetří práci a zpřehledňují kód, nicméně jsou ale pochopitelně složitější a pomalejší než odpovídající ručně napsané.
    </note>

</slide>
<slide title="PS: Varianty pro metody tříd">

    <p>
        Je třeba zmínit, že některé z dekorátorů existují i ve variantách pro metody tříd, konkrétně <code>@cached_property</code>, <code>@partialmethod</code> a <code>@singledispatchmethod</code>.
    </p>
    <note>
        Jelikož metody tříd jsou v Python'u navrženy <a href="/materialy/python/objects/basics.xml?slajd=9">nepříliš šťastně</a> a hlavně jejich správnou funkcionalitu zajišťují <a href="/materialy/python/objects/oop.xml?slajd=13">odpovídající dekorátory</a>, musí s nimi tyto dekorátory pochopitelně spolupracovat, aby celou mašinérii tříd nerozbořily.
    </note>
    <p>
        Podrobnosti zde ale uvádět nebudu a zájemce odkazuji na <a href="https://docs.python.org/3/library/functools.html" class="external">oficiální dokumentaci k modulu <em>functools</em></a>.
    </p>

</slide>


</lecture>
