Podpora funkcionálního programování v Pythonu a knihovna functools (3. část)

10. 8. 2023
Doba čtení: 26 minut

Sdílet

 Autor: Depositphotos
Ve třetím článku o funkcionálním programování v Pythonu si popíšeme většinu funkcí, které nalezneme v knihovně functools. Taktéž se zmíníme o podpoře dekorátorů, což je další funkcionální technika převedená do Pythonu.

Obsah

1. Podpora funkcionálního programování v Pythonu a knihovna functools (3. část)

2. Funkce vyššího řádu partialmethod

3. Třída s metodou s parametry

4. Metody enable a disable vzniklé transformací set_enabled

5. Další příklad použití funkce partialmethod – doplnění většího množství parametrů při transformaci

6. Cache pro výsledky čistých funkcí

7. Klasický výpočet Fibonacciho posloupnosti rekurzivní funkcí

8. LRU cache pro výsledky Fibonacciho posloupnosti pro nejčastěji použité vstupy

9. Přečtení informací o využití LRU cache

10. Programové vymazání LRU cache

11. Cache pro hodnotu vlastnosti objektu

12. Použití standardního dekorátoru @property

13. Použití dekorátoru @cached_property

14. Vygenerování metod s implementací relačních operátorů (total ordering)

15. Dekorátory

16. Ukázka dekorátoru aplikovaného na funkci

17. Dvojice dekorátorů aplikovaných na jednu funkci

18. Praktické použití dekorátoru – měření doby trvání funkce označené dekorátorem

19. Repositář s demonstračními příklady

20. Odkazy na Internetu

1. Podpora funkcionálního programování v Pythonu a knihovna functools (3. část)

Na předchozí dva články o podpoře funkcionálního programování v jazyce Python [1][2] dnes navážeme. Popíšeme si další funkce a dekorátory, které nalezneme ve standardním balíčku functools, zejména funkci partialmethod a dekorátory @lru_cache, @cached_property a @total_ordering. Společně s předchozím článkem jsme se tak věnovali prakticky celému obsahu tohoto potenciálně velmi užitečného balíčku:

Symbol Verze Pythonu Popsáno
wraps 2.5  
update_wrapper 2.5  
partial 2.5 druhý článek, kapitola 9
reduce 3.0 první článek, kapitola 17
lru_cache 3.2 dnešní část, kapitola 6
total_ordering 3.2 dnešní část, kapitola 14
cmp_to_key 3.2  
partialmethod 3.4 dnešní část, kapitola 2
singledispatch 3.4  
cached_property 3.8 dnešní část, kapitola 11
singledispatchmethod 3.8  
cache 3.9  

2. Funkce vyššího řádu partialmethod

V předchozím článku jsme si poměrně dopodrobna popsali funkci vyššího řádu nazvanou partial, která nějakou funkci s obecně n parametry transformovala na jinou funkci s obecně n-m parametry tak, že zbývající parametry (je jich m, typicky však 1) již byly doplněny na nějakou hodnotu. Připomeňme si následující (poněkud umělý) příklad:

from functools import partial
 
 
def mul(x, y):
    return x * y
 
 
print(mul(6, 7))
 
print()
 
doubler = partial(mul, 2)
 
 
for i in range(11):
    print(i, doubler(i))

V balíčku functools kromě partial nalezneme i podobně koncipovanou funkci partialmethod, která ovšem – jak její název správně napovídá – bude použitelná pro transformaci metody, tedy takových funkcí, jejichž prvním argumentem je self (a na něž se aplikují určitá pravidla viditelnosti atd.). Funkce partialmethod je potenciálně velmi užitečná, jak ostatně uvidíme v dalším textu.

3. Třída s metodou s parametry

Ještě předtím, než si ukážeme způsob použití funkce partialmethod si ukažme jednoduchou třídu, jejíž instance obsahují atribut _enabled. Tento atribut se nastavuje metodou pojmenovanou set_enabled a jak je z názvu atributu zřejmé (i bez uvedení typové deklarace), jsou hodnotami tohoto atributu pravdivostní hodnoty:

class Foo:
    def __init__(self):
        self._enabled = False
 
    def set_enabled(self, state):
        self._enabled = state
 
    def __str__(self):
        return "Foo that is " + ("enabled" if self._enabled else "disabled")
 
 
foo = Foo()
print(foo)

Příklad použití:

Foo that is disabled

Samozřejmě si můžeme bez problémů otestovat i vliv volání metody set_enabled (tedy tolik zatracovaného setteru) na stav objektu:

class Foo:
    def __init__(self):
        self._enabled = False
 
    def set_enabled(self, state):
        self._enabled = state
 
    def __str__(self):
        return "Foo that is " + ("enabled" if self._enabled else "disabled")
 
 
foo = Foo()
print(foo)
 
foo.set_enabled(True)
print(foo)
 
foo.set_enabled(False)
print(foo)

Výsledkem spuštění tohoto skriptu budou následující zprávy vypsané na terminál:

Foo that is disabled
Foo that is enabled
Foo that is disabled

4. Metody enable a disable vzniklé transformací set_enabled

Volání:

foo.set_enabled(True)
foo.set_enabled(False)

je ve skutečnosti poněkud neohrabané a hodí se jen ve chvíli, kdy se metoda volá s nějakým výrazem a nikoli s konstantou. Samozřejmě si můžeme vytvořit pomocné metody enable a disable, a to zcela klasickým způsobem – budeme z nich volat původní metodu set_enabled. Ovšem právě v tomto případě je mnohem elegantnější použití partialmethod, která za nás hodnotu parametru doplní automaticky. Dvě nové metody do třídy Foo lze tedy přidat i tak, jak je to naznačeno na obou podtržených řádcích:

from functools import partialmethod
 
 
class Foo:
    def __init__(self):
        self._enabled = False
 
    def set_enabled(self, state):
        self._enabled = state
 
    enable = partialmethod(set_enabled, True)
    disable = partialmethod(set_enabled, False)
 
    def __str__(self):
        return "Foo that is " + ("enabled" if self._enabled else "disabled")
 
 
foo = Foo()
print(foo)
 
foo.enable()
print(foo)
 
foo.disable()
print(foo)

Výsledek bude naprosto stejný, jako tomu bylo v předchozím příkladu:

Foo that is disabled
Foo that is enabled
Foo that is disabled
Poznámka: partialmethod tedy pracuje podobně jako partial, ale bere v úvahu specifický význam prvního parametru self.

5. Další příklad použití funkce partialmethod – doplnění většího množství parametrů při transformaci

V předchozím článku jsme si řekli, že funkci partial můžeme použít i pro doplnění a zapamatování většího množství parametrů. Totéž ovšem platí i pro partialmethod, což znamená, že například můžeme transformovat metodu move_to ze třídy Point se třemi parametry (self, x a y) na novou metodu pouze s parametrem self, která přesune příslušný bod do počátku souřadného systému. Výsledek může vypadat následovně:

from functools import partialmethod
 
 
class Point:
    def __init__(self):
        self._x = 0
        self._y = 0
 
    def move_to(self, x, y):
        self._x = x
        self._y = y
 
    to_origin = partialmethod(move_to, 0, 0)
 
    def __str__(self):
        return f"Point[{self._x}, {self._y}]"
 
 
point = Point()
print(point)
 
point.move_to(1, 2)
print(point)
 
point.to_origin()
print(point)

Samozřejmě si můžeme otestovat, jak se bude výsledný skript chovat po svém spuštění:

Point[0, 0]
Point[1, 2]
Point[0, 0]

6. Cache pro výsledky čistých funkcí

Připomeňme si, že za čisté funkce, resp. čistě funkcionální funkce, považujeme takové funkce, jejichž výsledná hodnota záleží pouze na hodnotách parametrů a nikoli na nějakém vnitřním stavu, hodnotách externích proměnných či například výsledku přístupu k nějakým prostředkům se stavem (databáze, soubory, síťové rozhraní atd.). Jenže když se nad takovými funkcemi zamyslíme, zjistíme, že je vlastně (alespoň teoreticky) možné nahradit volání takové funkce nějakou formou mapování, resp. slovníku, kde klíči slovníku budou kombinace hodnot parametrů funkce a hodnotami budou návratové hodnoty funkce. To je pochopitelně pro většinu funkcí nepraktické, zejména s ohledem na obrovské množství kombinace hodnot parametrů, s nimiž se může funkce volat.

Nicméně mnohé funkce jsou často volány se stejnými kombinacemi parametrů. A takové funkce je možné opatřit (zabalit) vhodnou formou cache pro nejčastěji vyžadované výsledky. Opět se jedná o funkcionalitu dostupnou díky standardnímu balíčku functools.

7. Klasický výpočet Fibonacciho posloupnosti rekurzivní funkcí

Jako ukázku vhodnosti či v některých případech nevhodnosti použití cache pro funkce si ukažme, jak lze implementovat výpočet Fibonacciho posloupnosti rekurzivním výpočtem. Jedná se o klasický „školní“ příklad, který pouze doplníme o změření času trvání výpočtu. Pro zajímavost budeme výpočet desetkrát opakovat pro stejný vstup:

from time import time
 
def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)
 
 
max_n = 40
 
for _ in range(10):
    start = time()
    result = fib(max_n)
    end = time()
    print(result, end - start)

I pro relativně nízkou vstupní hodnotu (40) bude výpočet trvat poměrně dlouho (minimálně při použití CPythonu):

102334155 27.4883930683136
102334155 27.809394598007202
102334155 27.91700768470764
102334155 28.687997817993164
102334155 30.422297954559326
102334155 28.643412351608276
102334155 28.83504009246826
102334155 28.57629656791687
102334155 28.623551607131958
102334155 28.74962282180786
Poznámka: první sloupec obsahuje vypočtenou hodnotu, druhý sloupec čas výpočtu v sekundách.

8. LRU cache pro výsledky Fibonacciho posloupnosti pro nejčastěji použité vstupy

Nyní předchozí skript nepatrně upravíme tak, aby se použila cache pro nejčastěji požadované výsledky (resp. naopak – ty nejméně často vyžadované výsledky jsou z cache odstraňovány). Nejprve importujeme funkci lru_cache z balíčku functools a následně před definici funkce pro výpočet Fibonacciho posloupnosti přidáme dekorátor @lru_cache. Žádné další změny nebudou provedeny:

from time import time
from functools import lru_cache
 
@lru_cache
def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)
 
 
max_n = 40
 
for _ in range(10):
    start = time()
    result = fib(max_n)
    end = time()
    print(result, end - start)

Nyní ovšem budou výpočty probíhat mnohem rychleji! Pro další opakování výpočtu pro daný vstup maxn = 40 je to zřejmé – LRU cache již má uložen příslušný výsledek. Ovšem již první volání této funkce bude rychlejší, a to z toho důvodu, že se pamatují výsledky z jedné větve rekurzivního výpočtu:

102334155 0.0005862712860107422
102334155 4.76837158203125e-07
102334155 7.152557373046875e-07
102334155 4.76837158203125e-07
102334155 4.76837158203125e-07
102334155 4.76837158203125e-07
102334155 4.76837158203125e-07
102334155 4.76837158203125e-07
102334155 4.76837158203125e-07
102334155 2.384185791015625e-07
Poznámka: povšimněte si až neuvěřitelného urychlení z 27 sekund na 0,0005 sekundy v prvním případě. Pochopitelně za to platíme většími nároky na operační paměť.

9. Přečtení informací o využití LRU cache

Samotná LRU cache (least recently used) nám může poskytnout několik informací o počtu zapamatovaných výsledků, využití cache atd. Tyto informace lze získat zavoláním fib.cache_info(), což je samo o sobě zajímavé, protože nám to prozrazuje, že se jedná a atribut funkce obalené dekorátorem (podrobnosti si řekneme za chvíli). Zkusme si tedy výpočet Fibonacciho posloupnosti upravit tak, abychom před každým výpočtem zjistili a vypsali informaci o LRU cache:

from time import time
from functools import lru_cache
 
@lru_cache
def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)
 
 
max_n = 40
 
for _ in range(10):
    print(fib.cache_info())
    start = time()
    result = fib(max_n)
    end = time()
    print(result, end - start)

Získané výsledky prozrazují, jak je cache využita:

CacheInfo(hits=0, misses=0, maxsize=128, currsize=0)
102334155 0.0011126995086669922
CacheInfo(hits=38, misses=41, maxsize=128, currsize=41)
102334155 1.1920928955078125e-06
CacheInfo(hits=39, misses=41, maxsize=128, currsize=41)
102334155 9.5367431640625e-07
CacheInfo(hits=40, misses=41, maxsize=128, currsize=41)
102334155 9.5367431640625e-07
CacheInfo(hits=41, misses=41, maxsize=128, currsize=41)
102334155 4.76837158203125e-07
CacheInfo(hits=42, misses=41, maxsize=128, currsize=41)
102334155 4.76837158203125e-07
CacheInfo(hits=43, misses=41, maxsize=128, currsize=41)
102334155 4.76837158203125e-07
CacheInfo(hits=44, misses=41, maxsize=128, currsize=41)
102334155 2.384185791015625e-07
CacheInfo(hits=45, misses=41, maxsize=128, currsize=41)
102334155 4.76837158203125e-07
CacheInfo(hits=46, misses=41, maxsize=128, currsize=41)
102334155 4.76837158203125e-07
Poznámka: zobrazené informace dávají smysl. Na začátku je cache prázdná, po proběhnutí prvního výpočtu obsahuje 41 výsledků pro parametry 0 až 40 (zcela unikátních a nových), ovšem už po doběhnutí tohoto výpočtu lze zjistit, že cache byla úspěšně využita celkem 38× (jednou z větví rekurze). A další pokusy o výpočet fib(40) již přímo vrátí hodnotu z cache, čímž se zvýší počitadlo hits, ovšem obsazení cache zůstane stejné (počitadlo currsize).

10. Programové vymazání LRU cache

LRU cache je možné v případě potřeby explicitně vymazat. Příkladem by mohla být funkce používaná ve výpočtu na více místech, ovšem každý výpočet začíná s odlišnými vstupními parametry. V případě, že se nechceme spoléhat na „LRU algoritmus“, můžeme před takovým výpočtem zavolat metodu cache_clear(), která cache vymaže:

fib.cache_clear()

Podívejme se nyní na poněkud umělý příklad, v němž opět počítáme n-tý prvek Fibonacciho posloupnosti s využitím LRU cache, ovšem po proběhnutí určitého počtu volání funkce fib cache explicitně vymažeme:

from time import time
from functools import lru_cache
 
@lru_cache
def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)
 
 
max_n = 40
 
for i in range(20):
    if i % 5 == 0:
        fib.cache_clear()
    print(fib.cache_info())
    start = time()
    result = fib(max_n)
    end = time()
    print(result, end - start)

Tento skript kromě výsledků výpočtů a času, který byl stráven výpočty, zobrazuje informaci o využití cache:

CacheInfo(hits=0, misses=0, maxsize=128, currsize=0)
102334155 0.0005662441253662109
CacheInfo(hits=38, misses=41, maxsize=128, currsize=41)
102334155 7.152557373046875e-07
CacheInfo(hits=39, misses=41, maxsize=128, currsize=41)
102334155 2.384185791015625e-07
CacheInfo(hits=40, misses=41, maxsize=128, currsize=41)
102334155 2.384185791015625e-07
CacheInfo(hits=41, misses=41, maxsize=128, currsize=41)
102334155 2.384185791015625e-07
CacheInfo(hits=0, misses=0, maxsize=128, currsize=0)
102334155 1.5497207641601562e-05
CacheInfo(hits=38, misses=41, maxsize=128, currsize=41)
102334155 4.76837158203125e-07
CacheInfo(hits=39, misses=41, maxsize=128, currsize=41)
102334155 2.384185791015625e-07
CacheInfo(hits=40, misses=41, maxsize=128, currsize=41)
102334155 4.76837158203125e-07
CacheInfo(hits=41, misses=41, maxsize=128, currsize=41)
102334155 2.384185791015625e-07
CacheInfo(hits=0, misses=0, maxsize=128, currsize=0)
102334155 1.430511474609375e-05
CacheInfo(hits=38, misses=41, maxsize=128, currsize=41)
102334155 4.76837158203125e-07
CacheInfo(hits=39, misses=41, maxsize=128, currsize=41)
102334155 2.384185791015625e-07
CacheInfo(hits=40, misses=41, maxsize=128, currsize=41)
102334155 2.384185791015625e-07
CacheInfo(hits=41, misses=41, maxsize=128, currsize=41)
102334155 2.384185791015625e-07
CacheInfo(hits=0, misses=0, maxsize=128, currsize=0)
102334155 1.4066696166992188e-05
CacheInfo(hits=38, misses=41, maxsize=128, currsize=41)
102334155 7.152557373046875e-07
CacheInfo(hits=39, misses=41, maxsize=128, currsize=41)
102334155 2.384185791015625e-07
CacheInfo(hits=40, misses=41, maxsize=128, currsize=41)
102334155 2.384185791015625e-07
CacheInfo(hits=41, misses=41, maxsize=128, currsize=41)
102334155 2.384185791015625e-07
Poznámka: tučně jsou zobrazeny ty stavy výpočtu, v nichž došlo k vymazání LRU cache.

11. Cache pro hodnotu vlastnosti objektu

Výše uvedený dekorátor @lru_cache (což je, jak uvidíme dále, ve skutečnosti vhodně zapsaná funkce vyššího řádu, resp. uzávěr), se používá skutečně pro implementaci cache, jejíž obsah se může postupem času měnit na základě toho, s jakými parametry se „cachovaná“ funkce volá. Ve standardní knihovně functools ovšem nalezneme ještě jeden dekorátor s částečně podobným významem a taktéž s podobným jménem.

Tento dekorátor se jmenuje @cached_property a používá se v těch místech programového kódu, kde skutečně vyžadujeme vlastnost (property), která se ovšem (na rozdíl od běžné vlastnosti) vypočte jen jedenkrát a při každém dalším přístupu k ní (samozřejmě je myšleno čtení) se přímo vrátí zapamatovaná hodnota. Tato cache tedy slouží pro zapamatování jediné hodnoty a nemusí být založena na použití algoritmu „LRU“. Taktéž může sloužit pro odložení výpočtu, který by se jinak prováděl při konstrukci a inicializaci objektu.

12. Použití standardního dekorátoru @property

Ještě než si popíšeme výše zmíněný dekorátor @cached_property, podívejme se pro úplnost na použití standardního dekorátoru @property. Tento dekorátor se zapisuje před metodu, jejíž název později použijeme pro čtení vlastnosti objektu – a to bez toho, aby se metoda explicitně volala. Metoda je tedy dekorátorem transformována do odlišně se chovající hodnoty:

class Foo:
    @propety
    def bar(self):
        return something
 
 
x = Foo()
print(x.bar)
Poznámka: povšimněte si, že za x.bar nejsou závorky značící volání funkce.

Opět se podívejme na příklad založený na výpočtu Fibonacciho posloupnosti. V konstruktoru si pouze zapamatujeme hodnotu n a následně vypočteme Fib(n) jen ve chvíli, kdy je to skutečně zapotřebí, tj. při čtení vlastnosti value (a pokud se tato vlastnost nebude číst, výpočet se neprovede vůbec):

from time import time
 
 
class FibonacciNumber:
    def __init__(self, n):
        self._n = n
 
    @property
    def value(self):
        return FibonacciNumber.compute(self._n)
 
    @staticmethod
    def compute(n):
        if n < 2:
            return n
        return FibonacciNumber.compute(n-1) + FibonacciNumber.compute(n-2)
 
 
f = FibonacciNumber(40)
 
for _ in range(10):
    start = time()
    result = f.value
    end = time()
    print(result, end - start)

Jak pravděpodobně správně tušíte, bude každé čtení f.value znovu a znovu spouštět dlouhotrvající výpočet:

102334155 32.67074537277222
102334155 34.46800398826599
102334155 31.666145086288452
102334155 32.48106145858765
102334155 33.00236797332764
102334155 33.22874307632446
102334155 33.054988384246826
102334155 33.02854561805725
102334155 32.91584897041321
102334155 33.35956025123596

13. Použití dekorátoru @cached_property

V případě, že namísto standardního dekorátoru @property použijeme dekorátor @cached_property, bude se program chovat odlišně – výpočet se provede pouze při prvním přístupu k vlastnosti value:

from time import time
from functools import cached_property
 
 
class FibonacciNumber:
    def __init__(self, n):
        self._n = n
 
    @cached_property
    def value(self):
        return FibonacciNumber.compute(self._n)
 
    @staticmethod
    def compute(n):
        if n < 2:
            return n
        return FibonacciNumber.compute(n-1) + FibonacciNumber.compute(n-2)
 
 
f = FibonacciNumber(40)
 
for _ in range(10):
    start = time()
    result = f.value
    end = time()
    print(result, end - start)

O tom, že bude výsledek výpočtu skutečně uložen do cache, se přesvědčíme snadno pohledem na výsledky získané po spuštění skriptu:

102334155 32.47180533409119
102334155 9.5367431640625e-07
102334155 2.384185791015625e-07
102334155 2.384185791015625e-07
102334155 0.0
102334155 2.384185791015625e-07
102334155 0.0
102334155 2.384185791015625e-07
102334155 0.0
102334155 0.0

Pro další urychlení výpočtu pochopitelně můžeme využít jak @cached_property, tak i @lru_cache, a to konkrétně následujícím způsobem:

from time import time
from functools import cached_property, lru_cache
 
 
class FibonacciNumber:
    def __init__(self, n):
        self._n = n
 
    @cached_property
    def value(self):
        return FibonacciNumber.compute(self._n)
 
    @staticmethod
    @lru_cache
    def compute(n):
        if n < 2:
            return n
        return FibonacciNumber.compute(n-1) + FibonacciNumber.compute(n-2)
 
 
f = FibonacciNumber(40)
 
for _ in range(10):
    start = time()
    result = f.value
    end = time()
    print(result, end - start)

Výsledky nyní budou vypočteny jen jedenkrát a navíc velmi rychle:

102334155 0.0005643367767333984
102334155 4.76837158203125e-07
102334155 2.384185791015625e-07
102334155 2.384185791015625e-07
102334155 0.0
102334155 0.0
102334155 2.384185791015625e-07
102334155 0.0
102334155 2.384185791015625e-07
102334155 2.384185791015625e-07

14. Vygenerování metod s implementací relačních operátorů (total ordering)

Velmi zajímavým dekorátorem, který nalezneme v balíčku functools, je dekorátor nazvaný @total_ordering. Používá se, resp. může se použít v deklaracích tříd, jejichž instance (tedy hodnoty) je možné nějakým způsobem jednoznačně uspořádat ve smyslu relace „menší než“, „rovno“ atd. Příkladem může být třída reprezentující sémantickou verzi major.minor, kde majorminor jsou celá čísla. Všechny možné verze zapsané tímto způsobem lze zajisté uspořádat a vzniká tedy potřeba implementace všech šesti relačních operátorů ==, !=, <, <=, > i >=. A právě v takové situaci je možné použít dekorátor @total_ordering, který dokáže příslušné realizace operátorů vygenerovat na základě znalosti pouze relace rovnosti a (například) „menší než“. Ostatně se podívejme na implementaci, kde dekorátor aplikujeme nikoli na metodu, ale na celou třídu:

from functools import total_ordering
 
 
@total_ordering
class Version:
    def __init__(self, major, minor):
        self._major = major
        self._minor = minor
 
    def _is_valid_version(self, other):
        return (hasattr(other, "_major") and
                hasattr(other, "_major"))
 
    def __eq__(self, other):
        if not self._is_valid_version(other):
            return NotImplemented
        return (self._major, self._minor) == \
               (other._major, other._minor)
 
    def __lt__(self, other):
        if not self._is_valid_version(other):
            return NotImplemented
        return (self._major, self._minor) < \
               (other._major, other._minor)
 
 
v1 = Version(1, 0)
v2 = Version(1, 2)
v3 = Version(1, 2)
v4 = Version(2, 1)
 
print(v1==v2)
print(v2==v3)
 
print()
 
print(v1<v2)
print(v1<v4)
print(v2<v4)
 
print(v1>v2)
print(v1>v4)
print(v2>v4)

15. Dekorátory

V závěrečné části dnešního článku si na trojici demonstračních příkladů ukážeme základní způsoby použití takzvaných dekorátorů. Jedná se opět o funkcionální technologii, která nám umožňuje snadno „obalit“ volání nějaké funkce dalším kódem a vrátit výsledek jako novou funkci s přidanými vlastnostmi. Může to vypadat následovně:

def wrapper1(původní_funkce):
    def nová_funkce():
        # nějaký kód
        původní_funkce()
        # nějaký kód
 
    return nová_funkce

Důležité je, že ono vlastní „obalení“ původní funkce je realizováno snadno zapamatovatelnou syntaxí – před definici funkce se na samostatný řádek zapíše jméno dekorátoru a jeho případné parametry:

@wrapper
def hello():
    print("Hello!")

Musíme si ovšem uvědomit, že se ve skutečnosti jedná pouze o syntaktický cukr a podobnou techniku lze použít i v případě, že by dekorátory v Pythonu neexistovaly.

Poznámka: na dekorátory narazíme v Pythonu prakticky v jakémkoli oboru (třeba i u tvorby testů, REST API atd.). Vždy je důležité si uvědomit, že se nejedná o žádnou magii, ale o jinak zapsané volání funkce vyššího řádu.

16. Ukázka dekorátoru aplikovaného na funkci

Podívejme se nyní na velmi jednoduchý dekorátor, který původní (libovolnou) funkci obaluje tak, že před voláním původní funkce zobrazí řádek se znaky „-“ a po návratu z původní funkce opět vypíše řádek se znaky „-“. Kód tohoto příkladu vypadá následovně:

def wrapper1(function):
    def inner_function():
        print("-" * 40)
        function()
        print("-" * 40)
 
    return inner_function
 
 
@wrapper1
def hello():
    print("Hello!")
 
 
hello()

Pokud nyní tento příkladu spustíme, vypíše se na terminál následující trojice zpráv:

----------------------------------------
Hello!
----------------------------------------

Proč tomu tak je? Ve skutečnosti totiž nevoláme původní funkci hello, protože ta již pod tímto jménem neexistuje – byla totiž transformována do nové funkce, kterou si sice nemůžeme jednoduše vypsat ve formě zdrojového kódu, ale měla by vypadat zhruba takto:

def hello():
    print("-" * 40)
    původní_hello()()
    print("-" * 40)
Poznámka: samozřejmě nyní můžeme začít experimentovat s předáváním parametrů použitím nelokálních proměnných (a tím pádem i uzávěrů) atd. atd.

17. Dvojice dekorátorů aplikovaných na jednu funkci

Nyní si předchozí příklad upravme do nové podoby. Nejprve si nadefinujeme dvojici wrapperů („obalovačů“), které před i po zavolání původní funkce vypíšou na terminál buď řadu znaků „-“ nebo řadu znaků „=“:

def wrapper1(function):
    def inner_function():
        print("-" * 40)
        function()
        print("-" * 40)
 
    return inner_function
 
 
def wrapper2(function):
    def inner_function():
        print("=" * 40)
        function()
        print("=" * 40)
 
    return inner_function

Poté si necháme původní funkci hello ztransformovat (tedy „obalit“) dvakrát, a to v tomto zapsaném pořadí:

@wrapper1
@wrapper2
def hello():
    print("Hello!")

Výsledek bude vypadat takto:

----------------------------------------
========================================
Hello!
========================================
----------------------------------------

Co to znamená? Původní funkce hello byla nejdříve obalena do přibližně této podoby:

def novější_hello():
    print("=" * 40)
    původní_hello()()
    print("=" * 40)

a potom došlo k dalšímu obalení:

def nejnovější_hello():
    print("-" * 40)
    novější_hello()()
    print("-" * 40)

Úplný zdrojový kód tohoto demonstračního příkladu bude vypadat následovně:

def wrapper1(function):
    def inner_function():
        print("-" * 40)
        function()
        print("-" * 40)
 
    return inner_function
 
 
def wrapper2(function):
    def inner_function():
        print("=" * 40)
        function()
        print("=" * 40)
 
    return inner_function
 
 
@wrapper1
@wrapper2
def hello():
    print("Hello!")
 
 
hello()

18. Praktické použití dekorátoru – měření doby trvání funkce označené dekorátorem

V dnešním posledním demonstračním příkladu je ukázáno jedno z možných praktických použití dekorátoru. Bude se jednat o dekorátor, který nám umožní obalit nějakou funkci příkazy sloužícími pro měření doby jejího trvání. Samotný algoritmus je přitom triviální: zapamatujeme si časové razítko před spuštěním měřené funkce, měřenou funkci spustíme, vypočteme na základě nového časového razítka dobu trvání měřené funkce, kterou vytiskneme a nakonec vrátíme původní návratovou hodnotu z měřené funkce. Jedna z nejjednodušších implementací tohoto algoritmu může vypadat následovně:

ict ve školství 24

# Original code:
# https://pythonbasics.org/decorators/#Real-world-examples
 
 
import time
 
 
def measure_time(func):
    def wrapper(*arg):
        t = time.time()
        res = func(*arg)
        print("Function took " + str(time.time() - t) + " seconds to run")
        return res
 
    return wrapper
 
 
@measure_time
def tested_function(n):
    time.sleep(n)
 
 
tested_function(1)
tested_function(2)

Z výsledků získaných po spuštění tohoto skriptu vyplývá, že vše pracuje podle předpokladů (naměřený čas bude pochopitelně poněkud delší než zvolená jedna či dvě sekundy):

Function took 1.00141787529 seconds to run
Function took 2.00236320496 seconds to run

19. Repositář s demonstračními příklady

Všechny Pythonovské skripty, které jsme si ukázali předminule, minule i dnes, naleznete na adrese https://github.com/tisnik/most-popular-python-libs. Následují odkazy na jednotlivé příklady (pro jejich spuštění je nutné mít nainstalovánu některou z podporovaných verzí Pythonu 3, žádné další balíčky nejsou zapotřebí):

# Příklad Stručný popis Adresa
1 binary_operator.py ukázka funkce vyššího řádu, která jako parametr akceptuje jinou funkci https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/binary_operator.py
2 get_operator1.py ukázka funkce vyššího řádu, která vrací jinou funkci https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/get_operator1.py
3 get_operator2.py ukázka funkce vyššího řádu, která vrací jinou funkci https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/get_operator2.py
4 standard_operators.py použití standardních operátorů přepsaných do formy funkce https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/standard_operators.py
       
5 binary_operator_types.py varianta příkladu binary_operator.py s plnými typovými deklaracemi https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/binary_operator_types.py
6 get_operator_types.py varianta příkladu get_operator2.py s plnými typovými deklaracemi https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/get_operator_types.py
       
7 map1.py příklad použití funkce map: výpočet délky všech slov v textu https://github.com/tisnik/most-popular-python-libs/blob/master/functools/map1.py
8 map2.py příklad použití funkce map: výpočet absolutní hodnoty všech členů posloupnosti https://github.com/tisnik/most-popular-python-libs/blob/master/functools/map2.py
9 map3.py příklad použití funkce map: aplikace vlastní pojmenované funkce https://github.com/tisnik/most-popular-python-libs/blob/master/functools/map3.py
10 map4.py příklad použití funkce map: aplikace vlastního lambda výrazu https://github.com/tisnik/most-popular-python-libs/blob/master/functools/map4.py
       
11 map_list_comprehension1.py přepis skriptu map1.py tak, aby se použila generátorová notace https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/map_list_comprehension.py
12 map_list_comprehension2.py přepis skriptu map2.py tak, aby se použila generátorová notace https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/map_list_comprehension.py
13 map_list_comprehension3.py přepis skriptu map3.py tak, aby se použila generátorová notace https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/map_list_comprehension.py
14 map_list_comprehension4.py přepis skriptu map4.py tak, aby se použila generátorová notace https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/map_list_comprehension.py
       
15 filter1.py filtrace dat na základě délky řetězce https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/filter1.py
16 filter2.py filtrace numerických dat podle toho, zda se jedná o sudá či lichá čísla https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/filter2.py
17 filter3.py přepis předchozího příkladu s využitím lambda výrazu https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/filter3.py
       
18 filter_list_comprehension1.py přepis skriptu filter_list_comprehension1.py tak, aby se použila generátorová notace https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/filter_list_comprehensi­on1.py
19 filter_list_comprehension2.py přepis skriptu filter_list_comprehension2.py tak, aby se použila generátorová notace https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/filter_list_comprehensi­on2.py
20 filter_list_comprehension3.py přepis skriptu filter_list_comprehension3.py tak, aby se použila generátorová notace https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/filter_list_comprehensi­on3.py
       
21 reduce1.py výpočet faktoriálu s využitím funkce vyššího řádu reduce https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/reduce1.py
22 reduce2.py přepis předchozího příkladu s využitím lambda výrazu https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/reduce2.py
23 reduce3.py tisk tabulky faktoriálů https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/reduce3.py
24 reduce4.py přepis předchozího příkladu s využitím lambda výrazu https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/reduce4.py
25 reduce5.py přepis předchozího příkladu s využitím generátorové notace https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/reduce5.py
       
26 return_function.py funkce jako návratová hodnota jiné funkce https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/return_function.py
27 closure_adder1.py příklad použití uzávěru – konstrukce funkce typu adder https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/closure_adder1.py
28 counter_closure1.py nekorektní implementace čítače s využitím uzávěrů https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/counter_closure1.py
29 counter_closure2.py přístup k nelokálnímu symbolu (Python 2.x i Python 3.x) https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/counter_closure2.py
30 counter_closure3.py přístup k nelokálnímu symbolu (pouze Python 3.x) https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/counter_closure3.py
       
31 access_nonlocal_symbol.py přístup k nelokálnímu symbolu v uzávěru https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/access_nonlocal_symbol.py
32 functions_and_closures.py funkce a uzávěry (umělý příklad) https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/functions_and_closures.py
       
33 partial1.py funkce doubler odvozená (redukcí) z univerzálnější funkce mul https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/partial1.py
34 partial2.py transformace funkce se třemi parametry s využitím partial (nekorektní řešení) https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/partial2.py
35 partial3.py transformace funkce se třemi parametry s využitím partial (korektní řešení) https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/partial3.py
36 partial4.py transformace funkce s dosazením většího množství parametrů s využitím partial https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/partial4.py
37 partial5.py několikanásobná transformace původní funkce na několik nových funkcí https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/partial5.py
38 partial6.py postupná transformace již ztransformovaných funkcí https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/partial6.py
39 partial7.py typ originální funkce i funkcí získaných s využitím transformace pomocí partial https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/partial7.py
40 partial8.py jméno funkce, poziční argumenty funkce a pojmenované argumenty funkce https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/partial8.py
41 partial9.py transformace reduce a pojmenované argumenty původní funkce https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/partial9.py
42 partial_A.py získání informací o redukované funkci s pojmenovanými argumenty https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/partial_A.py
       
43 partial_method1.py třída s metodou s parametry https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/partial_method1.py
44 partial_method2.py třída s metodou s parametry – vliv volání setteru na stav objektu https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/partial_method2.py
45 partial_method3.py metody enable a disable vzniklé transformací set_enabled https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/partial_method3.py
46 partial_method4.py další příklad použití funkce partialmethod – doplnění většího množství parametrů při transformaci https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/partial_method4.py
       
47 lru_cache1.py klasický výpočet Fibonacciho posloupnosti rekurzivní funkcí https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/lru_cache1.py
48 lru_cache2.py LRU cache pro výsledky Fibonacciho posloupnosti pro nejčastěji použité vstupy https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/lru_cache2.py
49 lru_cache3.py přečtení informací o využití LRU cache https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/lru_cache3.py
50 lru_cache4.py programové vymazání LRU cache https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/lru_cache4.py
       
51 cached_property1.py použití standardního dekorátoru @property https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/cached_property1.py
52 cached_property2.py použití dekorátoru @cached_property https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/cached_property2.py
53 cached_property3.py úprava předchozího příkladu tak, aby se využila LRU cache https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/cached_property3.py
       
54 total_ordering.py příklad implementace relačních operátorů založených na dekorátoru @total_ordering https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/total_ordering.py
       
55 decorators2.py příklad použití jednoho dekorátoru aplikovaného na funkci https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/decorators2.py
56 decorators3.py příklad použití dvou dekorátorů aplikovaných na funkci https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/decorators3.py
57 measure_time.py praktické použití dekorátoru – měření doby trvání funkce označené dekorátorem https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/measure_time.py

20. Odkazy na Internetu

  1. functools — Higher-order functions and operations on callable objects
    https://docs.python.org/3/li­brary/functools.html
  2. Functional Programming HOWTO
    https://docs.python.org/3/how­to/functional.html
  3. Functional Programming in Python: When and How to Use It
    https://realpython.com/python-functional-programming/
  4. Functional Programming With Python
    https://realpython.com/learning-paths/functional-programming/
  5. Awesome Functional Python
    https://github.com/sfermigier/awesome-functional-python
  6. Currying
    https://en.wikipedia.org/wi­ki/Currying
  7. Currying in Python – A Beginner’s Introduction
    https://www.askpython.com/pyt­hon/examples/currying-in-python
  8. Fundamental Concepts in Programming Languages
    https://en.wikipedia.org/wi­ki/Fundamental_Concepts_in_Pro­gramming_Languages
  9. When should I use function currying?
    https://stackoverflow.com/qu­estions/24881604/when-should-i-use-function-currying
  10. Toolz
    https://github.com/pytool­z/toolz/tree/master
  11. Coconut: funkcionální jazyk s pattern matchingem kompatibilní s Pythonem
    https://www.root.cz/clanky/coconut-funkcionalni-jazyk-s-pattern-matchingem-kompatibilni-s-pythonem/
  12. A HITCHHIKER'S GUIDE TO functools
    https://ep2021.europython­.eu/media/conference/slides/a-hitchhikers-guide-to-functools.pdf
  13. Coconut aneb funkcionální nadstavba nad Pythonem (2.část)
    https://www.root.cz/clanky/coconut-aneb-funkcionalni-nadstavba-nad-pythonem-2-cast/
  14. Knihovny pro zpracování posloupností (sekvencí) v Pythonu
    https://www.root.cz/clanky/knihovny-pro-zpracovani-posloupnosti-sekvenci-v-pythonu/
  15. clj – repositář s knihovnou
    https://github.com/bfontaine/clj
  16. clj 0.1.0 – stránka na PyPi
    https://pypi.python.org/py­pi/clj/0.1.0
  17. Coconut: Simple, elegant, Pythonic functional programming
    http://coconut-lang.org/
  18. coconut (Python package index)
    https://pypi.python.org/pypi/coconut/
  19. Coconut Tutorial
    http://coconut.readthedoc­s.io/en/master/HELP.html
  20. Coconut FAQ
    http://coconut.readthedoc­s.io/en/master/FAQ.html
  21. Coconut Documentation
    http://coconut.readthedoc­s.io/en/master/DOCS.html
  22. Coconut na Redditu
    https://www.reddit.com/r/Pyt­hon/comments/4owzu7/coconut_fun­ctional_programming_in_pyt­hon/
  23. Repositář na GitHubu
    https://github.com/evhub/coconut
  24. Object-Oriented Programming — The Trillion Dollar Disaster
    https://betterprogramming.pub/object-oriented-programming-the-trillion-dollar-disaster-92a4b666c7c7
  25. Goodbye, Object Oriented Programming
    https://cscalfani.medium.com/goodbye-object-oriented-programming-a59cda4c0e53
  26. So You Want to be a Functional Programmer (Part 1)
    https://cscalfani.medium.com/so-you-want-to-be-a-functional-programmer-part-1–1f15e387e536
  27. So You Want to be a Functional Programmer (Part 2)
    https://cscalfani.medium.com/so-you-want-to-be-a-functional-programmer-part-2–7005682cec4a
  28. So You Want to be a Functional Programmer (Part 3)
    https://cscalfani.medium.com/so-you-want-to-be-a-functional-programmer-part-3–1b0fd14eb1a7
  29. So You Want to be a Functional Programmer (Part 4)
    https://cscalfani.medium.com/so-you-want-to-be-a-functional-programmer-part-4–18fbe3ea9e49
  30. So You Want to be a Functional Programmer (Part 5)
    https://cscalfani.medium.com/so-you-want-to-be-a-functional-programmer-part-5-c70adc9cf56a
  31. So You Want to be a Functional Programmer (Part 6)
    https://cscalfani.medium.com/so-you-want-to-be-a-functional-programmer-part-6-db502830403
  32. Why Programmers Need Limits
    https://cscalfani.medium.com/why-programmers-need-limits-3d96e1a0a6db
  33. Infographic showing code complexity vs developer experience
    https://twitter.com/rossi­pedia/status/1580639227313676288
  34. Python's reduce(): From Functional to Pythonic Style
    https://realpython.com/python-reduce-function/
  35. What is the problem with reduce()?
    https://stackoverflow.com/qu­estions/181543/what-is-the-problem-with-reduce
  36. The fate of reduce() in Python 3000
    https://www.artima.com/we­blogs/viewpost.jsp?thread=98196
  37. Reading 16: Map, Filter, Reduce
    http://web.mit.edu/6.031/www/sp22/clas­ses/16-map-filter-reduce/
  38. Currying
    https://sw-samuraj.cz/2011/02/currying/
  39. Používání funkcí v F#
    https://docs.microsoft.com/cs-cz/dotnet/fsharp/tutorials/using-functions
  40. Funkce vyššího řádu
    http://naucte-se.haskell.cz/funkce-vyssiho-radu
  41. Currying (Wikipedia)
    https://en.wikipedia.org/wi­ki/Currying
  42. Currying (Haskell wiki)
    https://wiki.haskell.org/Currying
  43. Haskell Curry
    https://en.wikipedia.org/wi­ki/Haskell_Curry
  44. Moses Schönfinkel
    https://en.wikipedia.org/wi­ki/Moses_Sch%C3%B6nfinkel
  45. ML – funkcionální jazyk s revolučním typovým systémem
    https://www.root.cz/clanky/ml-funkcionalni-jazyk-s-revolucnim-typovym-systemem/
  46. Funkce a typový systém programovacího jazyka ML
    https://www.root.cz/clanky/funkce-a-typovy-system-programovaciho-jazyka-ml/
  47. Curryfikace (currying), výjimky a vlastní operátory v jazyku ML
    https://www.root.cz/clanky/curryfikace-currying-vyjimky-a-vlastni-operatory-v-jazyku-ml/
  48. Primer on Python Decorators
    https://realpython.com/primer-on-python-decorators/
  49. Python Decorators
    https://www.programiz.com/python-programming/decorator
  50. PythonDecorators (Python Wiki)
    https://wiki.python.org/mo­in/PythonDecorators

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.