V této lekci se nebude věnovat žádné externí knihovně. Místo toho se seznámíme s jednou vlastností Pythonu, kterou knihovny často využívají, a která obvykle vypadá trochu magicky.
Touto vlastností jsou dekorátory.
Dekorátory se hodí tehdy, když potřebujeme upravit chování nějaké funkce, ale nechceme ji přímo upravovat.
Předpokládáme základní znalost Pythonu. Měli byste mít počítač s nainstalovaným interpretem jazyka Python ve verzi aspoň 3.6. Pro začátek si také vytvořte nové virtuální prostředí. Bude se nám hodit, pokud v něm bude nainstalovaná knihovna requests.
Co je to vlastně dekorátor? Dekorátor je vlastně jenom funkce, která dostane jeden argument a vrátí jednu hodnotu. Je ale trochu speciální v tom, že jak argument, tak návratová hodnota jsou zase jiné funkce.
Funkcím, které operují nad jinými funkcemi, říkáme funkce vyššího řádu.
Použití dekorátorů v kódu už jsme viděli při používání knihovny click
. Vypadá zhruba takto:
@dekorator
def funkce():
pass
Tento zápis se zavináčem je jenom syntaktický cukr. Usnadňuje nám zápis, ale
chová se přesně stejně jako následující kód, na kterém je lépe vidět, že
dekorator
je funkce:
def funkce():
pass
funkce = dekorator(funkce)
Na řádku za zavináčem může být libovolný výraz, který po vyhodnocení vrátí funkci, která má požadované rozhraní.
Jak už při programování bývá zvykem, náš první dekorátor nás pozdraví.
Začneme s jednoduchým programem, který definuje funkci pro pozdrav a zavolá ji.
def ahoj():
print("Ahoj")
if __name__ == "__main__":
ahoj()
Do tohoto programu bychom rádi přidali další pozdravy, a zavolali je všechny.
def ahoj():
print("Ahoj")
def nazdar():
print("Nazdar")
if __name__ == "__main__":
ahoj()
nazdar()
Tento přístup ale povede k tomu, že by na konci byl dlouhý seznam pozdravů. Můžeme si funkce rovnou uložit do seznamu a potom přes něj jenom iterovat.
def ahoj():
print("Ahoj")
def nazdar():
print("Nazdar")
if __name__ == "__main__":
funkce = [ahoj, nazdar]
for f in funkce:
f()
A jako poslední krok přidáme dekorátor, který nám bude funkce rovnou přidávat do seznamu.
funkce = []
def pridej_pozdrav(func):
funkce.append(func)
return funkce
@pridej_pozdrav
def ahoj():
print("Ahoj")
@pridej_pozdrav
def nazdar():
print("Nazdar")
if __name__ == "__main__":
for f in funkce:
f()
Zkuste přidat ještě jeden pozdrav.
V tomto příkladu jde o docela zbytečné použití dekorátorů. Ukazuje ale
praktický způsob, jak řešit registraci funkcí. Stejné řešení používá
například knihovna flask
pro definování webových služeb nebo click
pro
vytváření příkazů pro terminál.
Podívejme se třeba na tuto na pohled nevinnou funkci. Počítá, jak vypadá n-té číslo ve Fibonacciho posloupnosti. Funguje docela pěkně, pokud jí nezadáme jako argument příliš velké číslo. Na autorově počítači příliš velká čísla začínají kolem 35.
def fib(x):
"""Spočítá x-té číslo ve Fibonacciho posloupnosti."""
if x <= 1:
return x
return fib(x - 1) + fib(x - 2)
Napíšeme si jednoduchý dekorátor, který nám bude vypisovat informace o tom, co se ve funkci děje.
def co_se_deje(func):
print("Aplikuju dekorátor")
return func
@co_se_deje
def fib(x):
"""Spočítá x-té číslo ve Fibonacciho posloupnosti."""
if x <= 1:
return x
return fib(x - 1) + fib(x - 2)
if __name__ == "__main__":
print(fib(4))
Tento dekorátor funkci nijak nemění. Akorát nám oznámí, že byl aplikovaný. V těle dekorátoru ale můžeme nadefinovat novou funkci a vrátit ji.
Zkusme si to:
def co_se_deje(func):
def nahradni_funkce(x):
return "Spočítej si to sám!"
return nahradni_funkce
Nebo můžeme vrátit funkci, která akorát zavolá tu původní.
def co_se_deje(func):
def nahradni_funkce(x):
return func(x)
return nahradni_funkce
Pojďme vracenou funkci rozšířit tak, aby vypisovala informace o tom, co dělá.
def co_se_deje(func):
def nahradni_funkce(x):
print(f"Voláme {func.__name__}({x})")
return func(x)
return nahradni_funkce
Úkol: upravte dekorátor tak, aby vypisoval i vypočítanou hodnotu.
Tento dekorátor není úplně praktický. Pokud toho vypíše trochu víc, tak už se v tom logu nikdo nevyzná. Myšlenka jako taková ovšem není úplně špatná. Kdyby třeba dekorátor počítal, kolikrát se funkce spustí, a jak dlouho obvykle trvá, mohl by nám pomoct najít místa pro optimalizaci.
Zkuste si v interaktivní konzoli Pythonu spustit následující příklad:
>>> help(print)
Help on built-in function print in module builtins:
print(...)
print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=False)
Prints the values to a stream, or to sys.stdout by default.
Optional keyword arguments:
file: a file-like object (stream); defaults to the current sys.stdout.
sep: string inserted between values, default a space.
end: string appended after the last value, default a newline.
flush: whether to forcibly flush the stream.
Dostaneme krátkou nápovědu o tom, jak používat funkci print
.
Odkud se tato nápověda bere? Z dokumentačního komentáře. Takže bychom měli
dostat pěknou nápovědu třeba i pro naši známou funkci fib
.
>>> from fib import fib
>>> help(fib)
Help on function nahradni_funkce in module fib:
nahradni_funkce(x)
>>>
Něco je špatně. Protože jsme původní implementaci funkce fib
pomocí
dekorátoru nahradili naší pomocnou funkcí, komentář se cestou ztratil. Mohli
bychom přidat dokumentační komentář k náhradní funkci, ale přece nebudeme
stejný kód kopírovat dvakrát.
Standardní knihovna má naštěstí možnost, jak to snadno opravit. V modulu
functools
je definovaný dekorátor wraps
, který umí zkopírovat dokumentační
komentář a jméno z jedné funkce do druhé.
import functools
def co_se_deje(func):
@functools.wraps(func)
def nahradni_funkce(x):
pass
>>> from fib import fib
>>> help(fib)
Help on function fib in module fib:
fib(x)
Spočítá x-té číslo ve Fibonacciho posloupnosti.
>>>
Při psaní dekorátorů je dobré myslet na to, jak moc univerzální by měly být.
Například náš co_se_deje
momentálně funguje pouze pro funkce, které mají
jeden argument.
To je ale docela hloupé omezení. Stejně dobře bychom mohli chtít sledovat volání jiné funkce, která má třeba argumentů víc.
Pokud dekorátor nepotřebuje vědět nic o argumentech funkce, je docela praktické jej nadefinovat tak, aby byly prostě všechny předal dál, ať už jich je kolik chce.
To můžeme udělat následovně:
def co_se_deje(func):
@functools.wraps(func)
def nahradni_funkce(*args, **kwargs):
print(f"Voláme {func.__name__}{args}")
vysledek = func(*args, **kwargs)
print(f"Výsledek {func.__name__}{args} = {vysledek}")
return vysledek
return nahradni_funkce
Do n-tice args
posbíráme všechny poziční argumenty, do slovníku kwargs
všechny pojmenované argumenty. A při volání dekorované funkce je všechny zase
předáme dál.
Ve výstupu teď používáme pouze poziční argumenty. Přidání těch pojmenovaných je cvičení pro čtenáře.
Pokud náš program musí pracovat s nějakou externí službou nebo systémem, může se stát, že komunikace mezi nimi nebude vždy bezproblémová. Pěkný příklad je třeba stahování webové stránky se špatným připojením. S tím z Pythonu nic udělat nemůžeme.
Můžeme ale zkusit požadavek zopakovat, pokud poznáme, že je to typ chyby, kde opakování může pomoct.
Začneme s jednoduchým programem, který udělá HTTP požadavek.
def stahni():
"""Stáhne stránku a něco s ní udělá."""
print("Stahuju stránku")
odpoved = requests.get("https://httpbin.org/status/200,400,500")
print(f"Dostali jsme {odpoved.status_code}")
odpoved.raise_for_status()
return "OK"
if __name__ == "__main__":
stahni()
Použitá stránka náhodně odpoví jedním z vyjmenovaných kódu, takže ve dvou třetinách případů bychom měli dostat chybu. Pokud požadavek zkusíme zopakovat, máme dobrou šanci, že to projde.
Začneme s jednoduchým dekorátorem, který jenom zavolá funkci.
def opakuj_pri_neuspechu(func):
"""Pokud volání funkce vyhodí výjimku, budeme ji ignorovat a zkusíme funkci
zavolat znovu.
"""
@functools.wraps(func)
def nahradni_funkce(*args, **kwargs):
return func(*args, **kwargs)
return nahradni_funkce
@opakuj_pri_neuspechu()
def stahni():
pass
Co bude dělat naše náhradní funkce. Donekonečna bude zkoušet zavolat
dekorovanou funkci. Pokud se to podaří, vrátí její výsledek. Pokud dostaneme
výjimku requests.exceptions.HTTPError
, chvilku počkáme, a půjdeme na další
pokus.
import functools
import time
import requests
def opakuj_pri_neuspechu(func):
"""Pokud volání funkce vyhodí výjimku, budeme ji ignorovat a zkusíme funkci
zavolat znovu.
"""
@functools.wraps(func)
def nahradni_funkce(*args, **kwargs):
while True:
try:
return func(*args, **kwargs)
except requests.exceptions.HTTPError:
print("Chyba, zkusíme to znovu")
time.sleep(1)
return nahradni_funkce
Teď by program měl vypisovat, že se snaží stránku stáhnout několikrát, a opakovat to tak dlouho, dokud se to nepodaří.
Co když ale potřebujeme opakování pokusů na více místech, ale chceme reagovat na jiné výjimky?
Mohli bychom si nadefinovat nový dekorátor pro každý typ podmínky, kterou chceme zachycovat. To zní jako hodně práce a duplicitního kódu.
Místo toho můžeme dekorátor upravit tak, aby přijímal argumenty, a pak mu řekneme, kterou výjimku ošetřovat.
Výraz za @
musí při vyhodnocení vždy vracet funkci, která se chová jako
dekorátor. Takže musíme přidat jednu vrstvu do našich vnořených funkcí.
Funkce opakuj_pri_neuspechu
je vlastně továrna na dekorátory. Vždy, když ji
zavoláme, vrátí nám funkci, která se chová podle našich potřeb a funguje jako
dekorátor.
import functools
import time
import requests
def opakuj_pri_neuspechu(vyjimka):
def dekorator(func):
@functools.wraps(func)
def nahradni_funkce(*args, **kwargs):
while True:
try:
return func(*args, **kwargs)
except vyjimka:
print("Chyba, zkusíme to znovu")
time.sleep(1)
return nahradni_funkce
return dekorator
@opakuj_pri_neuspechu(requests.exceptions.HTTPError)
def stahni():
pass
{ "data": { "sessionMaterial": { "id": "session-material:2019/brno-jaro-knihovny:dekoratory:0", "title": "Dekorátory", "html": "\n \n \n\n <h1>Dekorátory</h1>\n<h2>Co je cílem tohoto cvičení?</h2>\n<p>V této lekci se nebude věnovat žádné externí knihovně. Místo toho se seznámíme\ns jednou vlastností Pythonu, kterou knihovny často využívají, a která obvykle\nvypadá trochu magicky.</p>\n<p>Touto vlastností jsou <em>dekorátory</em>.</p>\n<p><em>Dekorátory</em> se hodí tehdy, když potřebujeme upravit chování nějaké funkce, ale\nnechceme ji přímo upravovat.</p>\n<h2>Předpoklady</h2>\n<p>Předpokládáme základní znalost Pythonu. Měli byste mít počítač s nainstalovaným\ninterpretem jazyka Python ve verzi aspoň 3.6. Pro začátek si také vytvořte nové\nvirtuální prostředí. Bude se nám hodit, pokud v něm bude nainstalovaná knihovna\n<em>requests</em>.</p>\n<h2>Teorie do začátku</h2>\n<p>Co je to vlastně dekorátor? Dekorátor je vlastně jenom funkce, která dostane\njeden argument a vrátí jednu hodnotu. Je ale trochu speciální v tom, že jak\nargument, tak návratová hodnota jsou zase jiné funkce.</p>\n<div class=\"admonition note\"><p>Funkcím, které operují nad jinými funkcemi, říkáme <em>funkce vyššího řádu</em>.</p>\n</div><p>Použití dekorátorů v kódu už jsme viděli při používání knihovny <code>click</code>. Vypadá zhruba takto:</p>\n<div class=\"highlight\"><pre><span></span><span class=\"nd\">@dekorator</span>\n<span class=\"k\">def</span> <span class=\"nf\">funkce</span><span class=\"p\">():</span>\n <span class=\"k\">pass</span>\n</pre></div><p>Tento zápis se zavináčem je jenom <em>syntaktický cukr</em>. Usnadňuje nám zápis, ale\nchová se přesně stejně jako následující kód, na kterém je lépe vidět, že\n<code>dekorator</code> je funkce:</p>\n<div class=\"highlight\"><pre><span></span><span class=\"k\">def</span> <span class=\"nf\">funkce</span><span class=\"p\">():</span>\n <span class=\"k\">pass</span>\n<span class=\"n\">funkce</span> <span class=\"o\">=</span> <span class=\"n\">dekorator</span><span class=\"p\">(</span><span class=\"n\">funkce</span><span class=\"p\">)</span>\n</pre></div><p>Na řádku za zavináčem může být libovolný výraz, který po vyhodnocení vrátí\nfunkci, která má požadované rozhraní.</p>\n<h2>Přiklad 0 – registrace funkcí</h2>\n<p>Jak už při programování bývá zvykem, náš první dekorátor nás pozdraví.</p>\n<p>Začneme s jednoduchým programem, který definuje funkci pro pozdrav a zavolá ji.</p>\n<div class=\"highlight\"><pre><span></span><span class=\"k\">def</span> <span class=\"nf\">ahoj</span><span class=\"p\">():</span>\n <span class=\"k\">print</span><span class=\"p\">(</span><span class=\"s2\">"Ahoj"</span><span class=\"p\">)</span>\n\n\n<span class=\"k\">if</span> <span class=\"vm\">__name__</span> <span class=\"o\">==</span> <span class=\"s2\">"__main__"</span><span class=\"p\">:</span>\n <span class=\"n\">ahoj</span><span class=\"p\">()</span>\n</pre></div><p>Do tohoto programu bychom rádi přidali další pozdravy, a zavolali je všechny.</p>\n<div class=\"highlight\"><pre><span></span><span class=\"k\">def</span> <span class=\"nf\">ahoj</span><span class=\"p\">():</span>\n <span class=\"k\">print</span><span class=\"p\">(</span><span class=\"s2\">"Ahoj"</span><span class=\"p\">)</span>\n\n\n<span class=\"k\">def</span> <span class=\"nf\">nazdar</span><span class=\"p\">():</span>\n <span class=\"k\">print</span><span class=\"p\">(</span><span class=\"s2\">"Nazdar"</span><span class=\"p\">)</span>\n\n\n<span class=\"k\">if</span> <span class=\"vm\">__name__</span> <span class=\"o\">==</span> <span class=\"s2\">"__main__"</span><span class=\"p\">:</span>\n <span class=\"n\">ahoj</span><span class=\"p\">()</span>\n <span class=\"n\">nazdar</span><span class=\"p\">()</span>\n</pre></div><p>Tento přístup ale povede k tomu, že by na konci byl dlouhý seznam pozdravů.\nMůžeme si funkce rovnou uložit do seznamu a potom přes něj jenom iterovat.</p>\n<div class=\"highlight\"><pre><span></span><span class=\"k\">def</span> <span class=\"nf\">ahoj</span><span class=\"p\">():</span>\n <span class=\"k\">print</span><span class=\"p\">(</span><span class=\"s2\">"Ahoj"</span><span class=\"p\">)</span>\n\n\n<span class=\"k\">def</span> <span class=\"nf\">nazdar</span><span class=\"p\">():</span>\n <span class=\"k\">print</span><span class=\"p\">(</span><span class=\"s2\">"Nazdar"</span><span class=\"p\">)</span>\n\n\n<span class=\"k\">if</span> <span class=\"vm\">__name__</span> <span class=\"o\">==</span> <span class=\"s2\">"__main__"</span><span class=\"p\">:</span>\n <span class=\"n\">funkce</span> <span class=\"o\">=</span> <span class=\"p\">[</span><span class=\"n\">ahoj</span><span class=\"p\">,</span> <span class=\"n\">nazdar</span><span class=\"p\">]</span>\n <span class=\"k\">for</span> <span class=\"n\">f</span> <span class=\"ow\">in</span> <span class=\"n\">funkce</span><span class=\"p\">:</span>\n <span class=\"n\">f</span><span class=\"p\">()</span>\n</pre></div><p>A jako poslední krok přidáme dekorátor, který nám bude funkce rovnou přidávat\ndo seznamu.</p>\n<div class=\"highlight\"><pre><span></span><span class=\"n\">funkce</span> <span class=\"o\">=</span> <span class=\"p\">[]</span>\n\n\n<span class=\"k\">def</span> <span class=\"nf\">pridej_pozdrav</span><span class=\"p\">(</span><span class=\"n\">func</span><span class=\"p\">):</span>\n <span class=\"n\">funkce</span><span class=\"o\">.</span><span class=\"n\">append</span><span class=\"p\">(</span><span class=\"n\">func</span><span class=\"p\">)</span>\n <span class=\"k\">return</span> <span class=\"n\">funkce</span>\n\n\n<span class=\"nd\">@pridej_pozdrav</span>\n<span class=\"k\">def</span> <span class=\"nf\">ahoj</span><span class=\"p\">():</span>\n <span class=\"k\">print</span><span class=\"p\">(</span><span class=\"s2\">"Ahoj"</span><span class=\"p\">)</span>\n\n\n<span class=\"nd\">@pridej_pozdrav</span>\n<span class=\"k\">def</span> <span class=\"nf\">nazdar</span><span class=\"p\">():</span>\n <span class=\"k\">print</span><span class=\"p\">(</span><span class=\"s2\">"Nazdar"</span><span class=\"p\">)</span>\n\n\n<span class=\"k\">if</span> <span class=\"vm\">__name__</span> <span class=\"o\">==</span> <span class=\"s2\">"__main__"</span><span class=\"p\">:</span>\n <span class=\"k\">for</span> <span class=\"n\">f</span> <span class=\"ow\">in</span> <span class=\"n\">funkce</span><span class=\"p\">:</span>\n <span class=\"n\">f</span><span class=\"p\">()</span>\n</pre></div><p>Zkuste přidat ještě jeden pozdrav.</p>\n<div class=\"admonition note\"><p>V tomto příkladu jde o docela zbytečné použití dekorátorů. Ukazuje ale\npraktický způsob, jak řešit registraci funkcí. Stejné řešení používá\nnapříklad knihovna <code>flask</code> pro definování webových služeb nebo <code>click</code> pro\nvytváření příkazů pro terminál.</p>\n</div><h2>Příklad 1 – trasování volání funkcí</h2>\n<p>Podívejme se třeba na tuto na pohled nevinnou funkci. Počítá, jak vypadá <em>n</em>-té\nčíslo ve Fibonacciho posloupnosti. Funguje docela pěkně, pokud jí nezadáme jako\nargument příliš velké číslo. Na autorově počítači příliš velká čísla začínají\nkolem 35.</p>\n<div class=\"highlight\"><pre><span></span><span class=\"k\">def</span> <span class=\"nf\">fib</span><span class=\"p\">(</span><span class=\"n\">x</span><span class=\"p\">):</span>\n <span class=\"sd\">"""Spočítá x-té číslo ve Fibonacciho posloupnosti."""</span>\n <span class=\"k\">if</span> <span class=\"n\">x</span> <span class=\"o\"><=</span> <span class=\"mi\">1</span><span class=\"p\">:</span>\n <span class=\"k\">return</span> <span class=\"n\">x</span>\n <span class=\"k\">return</span> <span class=\"n\">fib</span><span class=\"p\">(</span><span class=\"n\">x</span> <span class=\"o\">-</span> <span class=\"mi\">1</span><span class=\"p\">)</span> <span class=\"o\">+</span> <span class=\"n\">fib</span><span class=\"p\">(</span><span class=\"n\">x</span> <span class=\"o\">-</span> <span class=\"mi\">2</span><span class=\"p\">)</span>\n</pre></div><p>Napíšeme si jednoduchý dekorátor, který nám bude vypisovat informace o tom, co\nse ve funkci děje.</p>\n<div class=\"highlight\"><pre><span></span><span class=\"k\">def</span> <span class=\"nf\">co_se_deje</span><span class=\"p\">(</span><span class=\"n\">func</span><span class=\"p\">):</span>\n <span class=\"k\">print</span><span class=\"p\">(</span><span class=\"s2\">"Aplikuju dekorátor"</span><span class=\"p\">)</span>\n <span class=\"k\">return</span> <span class=\"n\">func</span>\n\n\n<span class=\"nd\">@co_se_deje</span>\n<span class=\"k\">def</span> <span class=\"nf\">fib</span><span class=\"p\">(</span><span class=\"n\">x</span><span class=\"p\">):</span>\n <span class=\"sd\">"""Spočítá x-té číslo ve Fibonacciho posloupnosti."""</span>\n <span class=\"k\">if</span> <span class=\"n\">x</span> <span class=\"o\"><=</span> <span class=\"mi\">1</span><span class=\"p\">:</span>\n <span class=\"k\">return</span> <span class=\"n\">x</span>\n <span class=\"k\">return</span> <span class=\"n\">fib</span><span class=\"p\">(</span><span class=\"n\">x</span> <span class=\"o\">-</span> <span class=\"mi\">1</span><span class=\"p\">)</span> <span class=\"o\">+</span> <span class=\"n\">fib</span><span class=\"p\">(</span><span class=\"n\">x</span> <span class=\"o\">-</span> <span class=\"mi\">2</span><span class=\"p\">)</span>\n\n<span class=\"k\">if</span> <span class=\"vm\">__name__</span> <span class=\"o\">==</span> <span class=\"s2\">"__main__"</span><span class=\"p\">:</span>\n <span class=\"k\">print</span><span class=\"p\">(</span><span class=\"n\">fib</span><span class=\"p\">(</span><span class=\"mi\">4</span><span class=\"p\">))</span>\n</pre></div><p>Tento dekorátor funkci nijak nemění. Akorát nám oznámí, že byl aplikovaný. V\ntěle dekorátoru ale můžeme nadefinovat novou funkci a vrátit ji.</p>\n<p>Zkusme si to:</p>\n<div class=\"highlight\"><pre><span></span><span class=\"k\">def</span> <span class=\"nf\">co_se_deje</span><span class=\"p\">(</span><span class=\"n\">func</span><span class=\"p\">):</span>\n <span class=\"k\">def</span> <span class=\"nf\">nahradni_funkce</span><span class=\"p\">(</span><span class=\"n\">x</span><span class=\"p\">):</span>\n <span class=\"k\">return</span> <span class=\"s2\">"Spočítej si to sám!"</span>\n\n <span class=\"k\">return</span> <span class=\"n\">nahradni_funkce</span>\n</pre></div><p>Nebo můžeme vrátit funkci, která akorát zavolá tu původní.</p>\n<div class=\"highlight\"><pre><span></span><span class=\"k\">def</span> <span class=\"nf\">co_se_deje</span><span class=\"p\">(</span><span class=\"n\">func</span><span class=\"p\">):</span>\n <span class=\"k\">def</span> <span class=\"nf\">nahradni_funkce</span><span class=\"p\">(</span><span class=\"n\">x</span><span class=\"p\">):</span>\n <span class=\"k\">return</span> <span class=\"n\">func</span><span class=\"p\">(</span><span class=\"n\">x</span><span class=\"p\">)</span>\n\n <span class=\"k\">return</span> <span class=\"n\">nahradni_funkce</span>\n</pre></div><p>Pojďme vracenou funkci rozšířit tak, aby vypisovala informace o tom, co dělá.</p>\n<div class=\"highlight\"><pre><span></span><span class=\"k\">def</span> <span class=\"nf\">co_se_deje</span><span class=\"p\">(</span><span class=\"n\">func</span><span class=\"p\">):</span>\n <span class=\"k\">def</span> <span class=\"nf\">nahradni_funkce</span><span class=\"p\">(</span><span class=\"n\">x</span><span class=\"p\">):</span>\n <span class=\"k\">print</span><span class=\"p\">(</span><span class=\"n\">f</span><span class=\"s2\">"Voláme {func.__name__}({x})"</span><span class=\"p\">)</span>\n <span class=\"k\">return</span> <span class=\"n\">func</span><span class=\"p\">(</span><span class=\"n\">x</span><span class=\"p\">)</span>\n\n <span class=\"k\">return</span> <span class=\"n\">nahradni_funkce</span>\n</pre></div><p>Úkol: upravte dekorátor tak, aby vypisoval i vypočítanou hodnotu.</p>\n<div class=\"solution\" id=\"solution-0\">\n <h3>Řešení</h3>\n <div class=\"solution-cover\">\n <a href=\"/2019/brno-jaro-knihovny/beginners/decorators/index/solutions/0/\"><span class=\"link-text\">Ukázat řešení</span></a>\n </div>\n <div class=\"solution-body\" aria-hidden=\"true\">\n <div class=\"highlight\"><pre><span></span><span class=\"k\">def</span> <span class=\"nf\">co_se_deje</span><span class=\"p\">(</span><span class=\"n\">func</span><span class=\"p\">):</span>\n <span class=\"k\">def</span> <span class=\"nf\">nahradni_funkce</span><span class=\"p\">(</span><span class=\"n\">x</span><span class=\"p\">):</span>\n <span class=\"k\">print</span><span class=\"p\">(</span><span class=\"n\">f</span><span class=\"s2\">"Voláme {func.__name__}({x})"</span><span class=\"p\">)</span>\n <span class=\"n\">vysledek</span> <span class=\"o\">=</span> <span class=\"n\">func</span><span class=\"p\">(</span><span class=\"n\">x</span><span class=\"p\">)</span>\n <span class=\"k\">print</span><span class=\"p\">(</span><span class=\"n\">f</span><span class=\"s2\">"Výsledek {func.__name__}({x}) = {vysledek}"</span><span class=\"p\">)</span>\n <span class=\"k\">return</span> <span class=\"n\">vysledek</span>\n\n <span class=\"k\">return</span> <span class=\"n\">nahradni_funkce</span>\n</pre></div>\n </div>\n</div><div class=\"admonition note\"><p>Tento dekorátor není úplně praktický. Pokud toho vypíše trochu víc, tak už se\nv tom logu nikdo nevyzná. Myšlenka jako taková ovšem není úplně špatná. Kdyby\ntřeba dekorátor počítal, kolikrát se funkce spustí, a jak dlouho obvykle\ntrvá, mohl by nám pomoct najít místa pro optimalizaci.</p>\n</div><h3>Nápověda pro funkce</h3>\n<p>Zkuste si v interaktivní konzoli Pythonu spustit následující příklad:</p>\n<div class=\"highlight\"><pre><span></span><span class=\"gp\">>>> </span><span class=\"n\">help</span><span class=\"p\">(</span><span class=\"k\">print</span><span class=\"p\">)</span>\n<span class=\"go\">Help on built-in function print in module builtins:</span>\n\n<span class=\"go\">print(...)</span>\n<span class=\"go\"> print(value, ..., sep=' ', end='\\n', file=sys.stdout, flush=False)</span>\n\n<span class=\"go\"> Prints the values to a stream, or to sys.stdout by default.</span>\n<span class=\"go\"> Optional keyword arguments:</span>\n<span class=\"go\"> file: a file-like object (stream); defaults to the current sys.stdout.</span>\n<span class=\"go\"> sep: string inserted between values, default a space.</span>\n<span class=\"go\"> end: string appended after the last value, default a newline.</span>\n<span class=\"go\"> flush: whether to forcibly flush the stream.</span>\n</pre></div><p>Dostaneme krátkou nápovědu o tom, jak používat funkci <code>print</code>.</p>\n<p>Odkud se tato nápověda bere? Z dokumentačního komentáře. Takže bychom měli\ndostat pěknou nápovědu třeba i pro naši známou funkci <code>fib</code>.</p>\n<div class=\"highlight\"><pre><span></span><span class=\"gp\">>>> </span><span class=\"kn\">from</span> <span class=\"nn\">fib</span> <span class=\"kn\">import</span> <span class=\"n\">fib</span>\n<span class=\"gp\">>>> </span><span class=\"n\">help</span><span class=\"p\">(</span><span class=\"n\">fib</span><span class=\"p\">)</span>\n<span class=\"go\">Help on function nahradni_funkce in module fib:</span>\n\n<span class=\"go\">nahradni_funkce(x)</span>\n\n<span class=\"go\">>>></span>\n</pre></div><p>Něco je špatně. Protože jsme původní implementaci funkce <code>fib</code> pomocí\ndekorátoru nahradili naší pomocnou funkcí, komentář se cestou ztratil. Mohli\nbychom přidat dokumentační komentář k náhradní funkci, ale přece nebudeme\nstejný kód kopírovat dvakrát.</p>\n<p>Standardní knihovna má naštěstí možnost, jak to snadno opravit. V modulu\n<code>functools</code> je definovaný dekorátor <code>wraps</code>, který umí zkopírovat dokumentační\nkomentář a jméno z jedné funkce do druhé.</p>\n<div class=\"highlight\"><pre><span></span><span class=\"kn\">import</span> <span class=\"nn\">functools</span>\n\n\n<span class=\"k\">def</span> <span class=\"nf\">co_se_deje</span><span class=\"p\">(</span><span class=\"n\">func</span><span class=\"p\">):</span>\n <span class=\"nd\">@functools.wraps</span><span class=\"p\">(</span><span class=\"n\">func</span><span class=\"p\">)</span>\n <span class=\"k\">def</span> <span class=\"nf\">nahradni_funkce</span><span class=\"p\">(</span><span class=\"n\">x</span><span class=\"p\">):</span>\n <span class=\"k\">pass</span>\n</pre></div><div class=\"highlight\"><pre><span></span><span class=\"gp\">>>> </span><span class=\"kn\">from</span> <span class=\"nn\">fib</span> <span class=\"kn\">import</span> <span class=\"n\">fib</span>\n<span class=\"gp\">>>> </span><span class=\"n\">help</span><span class=\"p\">(</span><span class=\"n\">fib</span><span class=\"p\">)</span>\n<span class=\"go\">Help on function fib in module fib:</span>\n\n<span class=\"go\">fib(x)</span>\n<span class=\"go\"> Spočítá x-té číslo ve Fibonacciho posloupnosti.</span>\n\n<span class=\"go\">>>></span>\n</pre></div><h3>Předávání všech argumentů.</h3>\n<p>Při psaní dekorátorů je dobré myslet na to, jak moc univerzální by měly být.\nNapříklad náš <code>co_se_deje</code> momentálně funguje pouze pro funkce, které mají\njeden argument.</p>\n<p>To je ale docela hloupé omezení. Stejně dobře bychom mohli chtít sledovat\nvolání jiné funkce, která má třeba argumentů víc.</p>\n<p>Pokud dekorátor nepotřebuje vědět nic o argumentech funkce, je docela praktické\njej nadefinovat tak, aby byly prostě všechny předal dál, ať už jich je kolik\nchce.</p>\n<p>To můžeme udělat následovně:</p>\n<div class=\"highlight\"><pre><span></span><span class=\"k\">def</span> <span class=\"nf\">co_se_deje</span><span class=\"p\">(</span><span class=\"n\">func</span><span class=\"p\">):</span>\n <span class=\"nd\">@functools.wraps</span><span class=\"p\">(</span><span class=\"n\">func</span><span class=\"p\">)</span>\n <span class=\"k\">def</span> <span class=\"nf\">nahradni_funkce</span><span class=\"p\">(</span><span class=\"o\">*</span><span class=\"n\">args</span><span class=\"p\">,</span> <span class=\"o\">**</span><span class=\"n\">kwargs</span><span class=\"p\">):</span>\n <span class=\"k\">print</span><span class=\"p\">(</span><span class=\"n\">f</span><span class=\"s2\">"Voláme {func.__name__}{args}"</span><span class=\"p\">)</span>\n <span class=\"n\">vysledek</span> <span class=\"o\">=</span> <span class=\"n\">func</span><span class=\"p\">(</span><span class=\"o\">*</span><span class=\"n\">args</span><span class=\"p\">,</span> <span class=\"o\">**</span><span class=\"n\">kwargs</span><span class=\"p\">)</span>\n <span class=\"k\">print</span><span class=\"p\">(</span><span class=\"n\">f</span><span class=\"s2\">"Výsledek {func.__name__}{args} = {vysledek}"</span><span class=\"p\">)</span>\n <span class=\"k\">return</span> <span class=\"n\">vysledek</span>\n <span class=\"k\">return</span> <span class=\"n\">nahradni_funkce</span>\n</pre></div><p>Do n-tice <code>args</code> posbíráme všechny poziční argumenty, do slovníku <code>kwargs</code>\nvšechny pojmenované argumenty. A při volání dekorované funkce je všechny zase\npředáme dál.</p>\n<p>Ve výstupu teď používáme pouze poziční argumenty. Přidání těch pojmenovaných je\ncvičení pro čtenáře.</p>\n<h2>Příklad 2 – opakování HTTP požadavků</h2>\n<p>Pokud náš program musí pracovat s nějakou externí službou nebo systémem, může\nse stát, že komunikace mezi nimi nebude vždy bezproblémová. Pěkný příklad je\ntřeba stahování webové stránky se špatným připojením. S tím z Pythonu nic\nudělat nemůžeme.</p>\n<p>Můžeme ale zkusit požadavek zopakovat, pokud poznáme, že je to typ chyby, kde\nopakování může pomoct.</p>\n<p>Začneme s jednoduchým programem, který udělá HTTP požadavek.</p>\n<div class=\"highlight\"><pre><span></span><span class=\"k\">def</span> <span class=\"nf\">stahni</span><span class=\"p\">():</span>\n <span class=\"sd\">"""Stáhne stránku a něco s ní udělá."""</span>\n <span class=\"k\">print</span><span class=\"p\">(</span><span class=\"s2\">"Stahuju stránku"</span><span class=\"p\">)</span>\n <span class=\"n\">odpoved</span> <span class=\"o\">=</span> <span class=\"n\">requests</span><span class=\"o\">.</span><span class=\"n\">get</span><span class=\"p\">(</span><span class=\"s2\">"https://httpbin.org/status/200,400,500"</span><span class=\"p\">)</span>\n <span class=\"k\">print</span><span class=\"p\">(</span><span class=\"n\">f</span><span class=\"s2\">"Dostali jsme {odpoved.status_code}"</span><span class=\"p\">)</span>\n <span class=\"n\">odpoved</span><span class=\"o\">.</span><span class=\"n\">raise_for_status</span><span class=\"p\">()</span>\n <span class=\"k\">return</span> <span class=\"s2\">"OK"</span>\n\n\n<span class=\"k\">if</span> <span class=\"vm\">__name__</span> <span class=\"o\">==</span> <span class=\"s2\">"__main__"</span><span class=\"p\">:</span>\n <span class=\"n\">stahni</span><span class=\"p\">()</span>\n</pre></div><p>Použitá stránka náhodně odpoví jedním z vyjmenovaných kódu, takže ve dvou\ntřetinách případů bychom měli dostat chybu. Pokud požadavek zkusíme zopakovat,\nmáme dobrou šanci, že to projde.</p>\n<p>Začneme s jednoduchým dekorátorem, který jenom zavolá funkci.</p>\n<div class=\"highlight\"><pre><span></span><span class=\"k\">def</span> <span class=\"nf\">opakuj_pri_neuspechu</span><span class=\"p\">(</span><span class=\"n\">func</span><span class=\"p\">):</span>\n <span class=\"sd\">"""Pokud volání funkce vyhodí výjimku, budeme ji ignorovat a zkusíme funkci</span>\n<span class=\"sd\"> zavolat znovu.</span>\n<span class=\"sd\"> """</span>\n <span class=\"nd\">@functools.wraps</span><span class=\"p\">(</span><span class=\"n\">func</span><span class=\"p\">)</span>\n <span class=\"k\">def</span> <span class=\"nf\">nahradni_funkce</span><span class=\"p\">(</span><span class=\"o\">*</span><span class=\"n\">args</span><span class=\"p\">,</span> <span class=\"o\">**</span><span class=\"n\">kwargs</span><span class=\"p\">):</span>\n <span class=\"k\">return</span> <span class=\"n\">func</span><span class=\"p\">(</span><span class=\"o\">*</span><span class=\"n\">args</span><span class=\"p\">,</span> <span class=\"o\">**</span><span class=\"n\">kwargs</span><span class=\"p\">)</span>\n\n <span class=\"k\">return</span> <span class=\"n\">nahradni_funkce</span>\n\n\n<span class=\"nd\">@opakuj_pri_neuspechu</span><span class=\"p\">()</span>\n<span class=\"k\">def</span> <span class=\"nf\">stahni</span><span class=\"p\">():</span>\n <span class=\"k\">pass</span>\n</pre></div><p>Co bude dělat naše náhradní funkce. Donekonečna bude zkoušet zavolat\ndekorovanou funkci. Pokud se to podaří, vrátí její výsledek. Pokud dostaneme\nvýjimku <code>requests.exceptions.HTTPError</code>, chvilku počkáme, a půjdeme na další\npokus.</p>\n<div class=\"highlight\"><pre><span></span><span class=\"kn\">import</span> <span class=\"nn\">functools</span>\n<span class=\"kn\">import</span> <span class=\"nn\">time</span>\n\n<span class=\"kn\">import</span> <span class=\"nn\">requests</span>\n\n\n<span class=\"k\">def</span> <span class=\"nf\">opakuj_pri_neuspechu</span><span class=\"p\">(</span><span class=\"n\">func</span><span class=\"p\">):</span>\n <span class=\"sd\">"""Pokud volání funkce vyhodí výjimku, budeme ji ignorovat a zkusíme funkci</span>\n<span class=\"sd\"> zavolat znovu.</span>\n<span class=\"sd\"> """</span>\n\n <span class=\"nd\">@functools.wraps</span><span class=\"p\">(</span><span class=\"n\">func</span><span class=\"p\">)</span>\n <span class=\"k\">def</span> <span class=\"nf\">nahradni_funkce</span><span class=\"p\">(</span><span class=\"o\">*</span><span class=\"n\">args</span><span class=\"p\">,</span> <span class=\"o\">**</span><span class=\"n\">kwargs</span><span class=\"p\">):</span>\n <span class=\"k\">while</span> <span class=\"bp\">True</span><span class=\"p\">:</span>\n <span class=\"k\">try</span><span class=\"p\">:</span>\n <span class=\"k\">return</span> <span class=\"n\">func</span><span class=\"p\">(</span><span class=\"o\">*</span><span class=\"n\">args</span><span class=\"p\">,</span> <span class=\"o\">**</span><span class=\"n\">kwargs</span><span class=\"p\">)</span>\n <span class=\"k\">except</span> <span class=\"n\">requests</span><span class=\"o\">.</span><span class=\"n\">exceptions</span><span class=\"o\">.</span><span class=\"n\">HTTPError</span><span class=\"p\">:</span>\n <span class=\"k\">print</span><span class=\"p\">(</span><span class=\"s2\">"Chyba, zkusíme to znovu"</span><span class=\"p\">)</span>\n <span class=\"n\">time</span><span class=\"o\">.</span><span class=\"n\">sleep</span><span class=\"p\">(</span><span class=\"mi\">1</span><span class=\"p\">)</span>\n\n <span class=\"k\">return</span> <span class=\"n\">nahradni_funkce</span>\n</pre></div><p>Teď by program měl vypisovat, že se snaží stránku stáhnout několikrát, a\nopakovat to tak dlouho, dokud se to nepodaří.</p>\n<p>Co když ale potřebujeme opakování pokusů na více místech, ale chceme reagovat\nna jiné výjimky?</p>\n<p>Mohli bychom si nadefinovat nový dekorátor pro každý typ podmínky, kterou\nchceme zachycovat. To zní jako hodně práce a duplicitního kódu.</p>\n<p>Místo toho můžeme dekorátor upravit tak, aby přijímal argumenty, a pak mu\nřekneme, kterou výjimku ošetřovat.</p>\n<p>Výraz za <code>@</code> musí při vyhodnocení vždy vracet funkci, která se chová jako\ndekorátor. Takže musíme přidat jednu vrstvu do našich vnořených funkcí.</p>\n<p>Funkce <code>opakuj_pri_neuspechu</code> je vlastně továrna na dekorátory. Vždy, když ji\nzavoláme, vrátí nám funkci, která se chová podle našich potřeb a funguje jako\ndekorátor.</p>\n<div class=\"highlight\"><pre><span></span><span class=\"kn\">import</span> <span class=\"nn\">functools</span>\n<span class=\"kn\">import</span> <span class=\"nn\">time</span>\n\n<span class=\"kn\">import</span> <span class=\"nn\">requests</span>\n\n\n<span class=\"k\">def</span> <span class=\"nf\">opakuj_pri_neuspechu</span><span class=\"p\">(</span><span class=\"n\">vyjimka</span><span class=\"p\">):</span>\n\n <span class=\"k\">def</span> <span class=\"nf\">dekorator</span><span class=\"p\">(</span><span class=\"n\">func</span><span class=\"p\">):</span>\n\n <span class=\"nd\">@functools.wraps</span><span class=\"p\">(</span><span class=\"n\">func</span><span class=\"p\">)</span>\n <span class=\"k\">def</span> <span class=\"nf\">nahradni_funkce</span><span class=\"p\">(</span><span class=\"o\">*</span><span class=\"n\">args</span><span class=\"p\">,</span> <span class=\"o\">**</span><span class=\"n\">kwargs</span><span class=\"p\">):</span>\n <span class=\"k\">while</span> <span class=\"bp\">True</span><span class=\"p\">:</span>\n <span class=\"k\">try</span><span class=\"p\">:</span>\n <span class=\"k\">return</span> <span class=\"n\">func</span><span class=\"p\">(</span><span class=\"o\">*</span><span class=\"n\">args</span><span class=\"p\">,</span> <span class=\"o\">**</span><span class=\"n\">kwargs</span><span class=\"p\">)</span>\n <span class=\"k\">except</span> <span class=\"n\">vyjimka</span><span class=\"p\">:</span>\n <span class=\"k\">print</span><span class=\"p\">(</span><span class=\"s2\">"Chyba, zkusíme to znovu"</span><span class=\"p\">)</span>\n <span class=\"n\">time</span><span class=\"o\">.</span><span class=\"n\">sleep</span><span class=\"p\">(</span><span class=\"mi\">1</span><span class=\"p\">)</span>\n\n <span class=\"k\">return</span> <span class=\"n\">nahradni_funkce</span>\n\n <span class=\"k\">return</span> <span class=\"n\">dekorator</span>\n\n\n<span class=\"nd\">@opakuj_pri_neuspechu</span><span class=\"p\">(</span><span class=\"n\">requests</span><span class=\"o\">.</span><span class=\"n\">exceptions</span><span class=\"o\">.</span><span class=\"n\">HTTPError</span><span class=\"p\">)</span>\n<span class=\"k\">def</span> <span class=\"nf\">stahni</span><span class=\"p\">():</span>\n <span class=\"k\">pass</span>\n</pre></div>\n\n\n " } } }