Python Dekorátory

Dekorátory slouží k ovlivnění chování funkcí či metod, aniž bychom je přímo modifikovali. Klasickým příkladem je @memoize - ukládání výsledků složité funkce do memcache pro zrychlení opakovaného použití.

Pro pochopení dekorátorů potřebujete nejprve pochopit klíčové vlastnosti funkcí v Pythonu. Funkce je objekt, jako všechno ostatní. Proto můžete funkci přiřadit jiné jméno, předat jí jako parametr do jiné funkce, vrátit jako výsledek volání funkce a také funkce do sebe vnořovat, podobně jako třeba prvky seznamu.

Začněme jiným jménem, to je velmi jedoduché:

#jine jmeno pro funkci
def clasic(words):
  freq = {}
  for word in words:
      if word in freq:
          freq[word] += 1
      else:
          freq[word] = 1

  return freq

nove_jmeno = clasic

Také na parametru není asi nic zásadně složitého:

def volany_ucastnik(param_fce): 
    print "Zrovna tvrde pracuju"
    param_fce()
    print 'uz je to udelano'

def nejaka_jina_funkce():
    print '@TODO dulezita funkce'

volany_ucastnik(nejaka_jina_funkce)

Proč by tedy funkce nemohla vrátit jinou funkci?:

def promluv(slovo, styl = 'nahlas'):

    def nahlas():
        return slovo.upper()+'!'

    def potichu():
        return slovo.lower()+'...'

    if (styl == 'nahlas'):
        return nahlas
    else:
        return potichu

slovo = promluv('test')
print slovo()

Také vnořené funkce jsou poměrně známé a nejde o žádné "Python only" specifikum:

def vnejsi():
    x = 3
    def vnitrni():
        print "vnitrni tiskne poprve x : {}".format(x)
        y = x #vnitrni funkce ma pristup k hodnotam vnejsi, ale nemuze je menit!
        print "vnitrni tiskne podruhe x : {}".format(y+1)

    vnitrni()    
    print "vnejsi tiskne x : {}".format(x)


vnejsi()

Kombinací všech čtyř vlastností můžeme vytvořit funkci, která přijme funkci jako argument, v rámci své vnitřní funkce tento argument zavolá a výsledek vrátí v podobě této vnitřní funkce. Aby to mělo nějaký praktický význam, je volání argumentu doplněno o nějaký další kód - například ono uložení výsledku výpočtu do cache.

Představte si tedy například funkci, která vytiskne výsledek argumentu (tedy jiné funkce) na standardní výstup a doplní ho o stručný komentář.

def printer(worker):
    def wrapper(arg):
        y = worker(arg)
        print "vysledek vasi funkce je: {}".format(y)

    return wrapper

Výsledkem funkce printer je tedy opět funkce, která vyžaduje zadání jednoho argumentu. Ten následně předává funkci worker, která ho potřebuje k výpočtu. Výsledek funkce worker se vytiskne na obrazovku hned jak je k dispozici. Funkci printer můžeme použít následovně:

def mocnina(x):
    return x*x

mocnina_vystup = printer(mocnina)
mocnina_vystup(3)

Výsledkem volání bude "vysledek vasi funkce je: 9". Funkce mocnina tedy "najednou tiskne", i když to předtím neuměla. A to aniž bychom nějak změnili její kód. Nadále zůstává čistou funkcí bez vedlejších efektů.

Doplnili jsme ale její chování - odekorovali jsme jí. Funkce printer je tedy dekorátor. Tento způsob volání dekorátoru není ale příliš praktický. Původní funkce dostává vlastně nové jméno, což může komplikovat čtení a hledání chyb. V případě že použíjeme zápis:

def mocnina(x):
    return x*x

printer(mocnina)(3)

může být kód pro někoho ještě víc nepřehledný a složitější na pochopení. Protože Python má dobrou čitelnost kódu v základní zenové mantře, přináší pro dekorátory zjednodušenou syntaxi v podobě @decorator.

Funkci mocnina tedy můžeme se stejným výsledkem zapsat a zavolat takto:

@printer
def mocnina(x):
    return x*x

mocnina(3)

Chování bude identické, ale zápis je stručnější a přehlednější. Navíc v tomto případě nemusíme měnit ani kód, který dekorovanou funkci volá a dekorátory tak můžeme přidávat a odebírat podle potřeby.

Na závěr se podívejte na slibovaný dekorátor @memoize - který můžete použít pro ukládání výsledků náročných funkcí pro jejich opakované použití.

import functools

def memoize(obj):
    cache = obj.cache = {}

    @functools.wraps(obj)
    def memoizer(*args, **kwargs):
        key = str(args) + str(kwargs)
        if key not in cache:
            cache[key] = obj(*args, **kwargs)
        return cache[key]
    return memoizer

Funkce wraps z balíčku functools slouží pro uchování kontextu původní funkce - jména, dokumentačního řetězce a dalších. To umožňuje mimo jiné snadnější hledání případných chyb. Magické proměnné args a *kwargs zajistí, že dekorované funkci jsou korektně předány všechny její argumenty. Dekorátor je tak univerzální.

A to je jako úvod do problematiky dekorátorů v Pythonu vše. Memoize a řadu dalších prakticky použitelných dekorátorů najdete ve wiki PythonDecoratorLibrary.