### Funkcije, ki sestavljajo in vračajo funkcije

Napišimo funkcije, ki pretvarjajo Fahrenheite v Celzije, Celzije v Fahrenheite in, hm kilometre v milje in nazaj.

    def fahr_v_celz(f):
        return (f - 32) * 5 / 9

    def celz_v_fahr(c):
        return c * 9 / 5 + 32

    def km_v_milje(km):
        return km / 1.60934

    def milje_v_km(m):
        return m * 1.60934

    fahr_v_celz(45)

    7.222222222222222

Rad bi, da vse te funkcije vračajo le dve decimalki. Pravzaprav, ne, rad, bi funkcije `fahr_v_celz2`, `celz_v_fahr2`, `km_v_milje2` in `milje_v_km2`, ki bi vračale taiste reči, vendar le na dve decimalki natančno.

Tega se bomo lotili malo drugače, ne - kot bi pričakovali - z

    def fahr_v_celz2(x):
        y = fahr_v_celz(x)
        return round(y, 2)

Pazite tole!

    def rounder(f):
        def rounded(x):
            y = f(x)
            return round(y, 2)

        return rounded

Preden povemo, kako deluje, poglejmo, kako uporabljati.

    fahr_v_celz2 = rounder(fahr_v_celz)
    celz_v_fahr2 = rounder(celz_v_fahr)
    km_v_milje2 = rounder(km_v_milje)
    milje_v_km2 = rounder(milje_v_km)

    fahr_v_celz2(45)

    7.22

Kaj se dogaja? Funkciji `rounder` kot argument podamo funkcijo, recimo `fahr_v_celz`. Znotraj `rounder` je `f` torej isto kot `fahr_v_celz`.

Zdaj pa poglejmo `rounder`: ta definira funkcijo `rounded`. Če si mislimo, da namesto `f` piše `fahr_v_celz` ... je ta funkcija pravzaprav popolnoma enaka funkciji `fahr_v_celz2`, ki smo jo napisali par celic višje. Funkcija `rounded` torej pravzaprav definira funkcijo `fahr_v_celz2` - ki jo sicer imenuje `rounded`.

In na koncu ... `return rounded`.

Funkcija `rounder` torej ne naredi drugega, kot da definira in vrne funkcijo `fahr_v_celz2`, čeprav pod drugim imenom. Ker jo pokličemo z

    fahr_v_celz2 = rounder(fahr_v_celz)

ima rezultat tega klica `rounder`-ja ime `fahr_v_celz2`. Kot mora.

Klicu `rounder(fahr_v_celz)`, ki torej vrne (v bistvu) `fahr_v_celz2`, sledijo klici `rounder(celz_v_fahr)`, `rounder(km_v_milje)` in `rounder(milje_v_km)`, ki definirajo in vrnejo še ostale tri funkcije.

### Dekoratorji

Funkcija `rounder` nemara ni višek uporabnosti, koncept sam pa. Pogosto si pripravimo (ali pa nam kdo drug pripravi) funkcijo, namenjeno takšnemu "ovijanju" drugih funkcij. Uporabimo jih kar tako, kot da bi zgoraj pisali

    def fahr_v_celz(f):
        return (f - 32) * 5 / 9

    fahr_v_celz = rounder(fahr_v_celz)

    def celz_v_fahr(c):
        return c * 9 / 5 + 32

    celz_v_fahr = rounder(celz_v_fahr)

    # in tako naprej

Vsako funkcijo torej ovijemo kar takoj po tem, ko jo sestavimo. Ker damo oviti funkciji tudi enako ime, je izvirna funkcija pravzaprav izgubljena, dostopamo lahko le do ovite različice, tiste, ki že zaokroža.

Za to obstaja skrajšana sintaksa.

    @rounder
    def fahr_v_celz(f):
        return (f - 32) * 5 / 9

    @rounder
    def celz_v_fahr(c):
        return c * 9 / 5 + 32

Stvari, ki smo jo napisali pred definicijo funkcije, rečemo dekorator. Pythonu s tem povemo, naj sicer definira funkcijo, kot je pisano, vendar pravkar definirano funkcijo posleduje dekoratorju (`rounder`); končna definicija funkcije bo tisto, kar vrne `rounder`.

### Primer: memoizacija

Klasičen primer za dekoratorje je memoizacija. Napisali bomo dekorator, ki pokliče ovito funkcijo in vrne njen rezultat. Mimogrede pa si ga še zapomni za naslednjič: če bo še kdo poklical funkcijo z enakim argumentom, bo dekorator vrnil kar shranjeni rezultat, namesto da bi ponovno klical funkcijo.

    def cache(f):
        stored = {}

        def cached(x):
            if x not in stored:
                stored[x] = f(x)
            return stored[x]

        return cached

    @cache
    def fahr_v_celz(f):
        print(f"Računam pretvorbo iz {f}")
        return (f - 32) * 5 / 9

    fahr_v_celz(50)

    Računam pretvorbo iz 50

    10.0

    fahr_v_celz(59)

    Računam pretvorbo iz 59

    15.0

    fahr_v_celz(50)

    10.0

V zadnjem klicu se ni izpisalo nič, saj je imel dekorator rezultat funkcije za argument `50` že shranjen.

Pretvarjanje Fahrenheitov v Celzije je preprosto, tega nima smisla shranjevati. Kaj pa Fibonaccijeva števila po rekurzivni formuli?

    klicev = 0

    def fibo(n):
        global klicev
        klicev += 1

        if n == 0 or n == 1:
            return 1
        return fibo(n - 1) + fibo(n - 2)

    fibo(30)

    1346269

Funkcija je videti nedolžna, vendar: poglejmo, kolikokrat se je poklicala!

    klicev

    2692537

Klicev je dvakrat toliko, kolikor je veliko Fibonaccijevo število. To je, če razmislimo, kar logično. Vendar tu ne bomo razmišljali o tem. Raje bomo razmišljali o tem, kako imenitna stvar je memoizacija.

    klicev = 0

    @cache
    def fibo(n):
        global klicev
        klicev += 1

        if n == 0 or n == 1:
            return 1
        return fibo(n - 1) + fibo(n - 2)

    fibo(30)

    1346269

    klicev

    31

To pa je tako logično, da pravzaprav ni o čem razmišljati: izračunati je potrebno vseh 31 Fibonaccijevih števil od 0-tega do 30-tega, vendar nobenega ne računamo več kot enkrat, zato bo klicev točno 31.

Izračunamo lahko tudi stoto Fibonaccijevo število.

    fibo(100)

    573147844013817084101

Brez memoizacije to ne bi bilo možno saj bi zahtevalo 1021 klicev.

    26.7 * 10 ** 9 * 365.25 * 24 * 3600 * 1000

    8.4258792e+20

Približno en klic za vsako milisekundo obstoja vesolja.

### Posplošena memoizacija

Imenitnost naše funkcije kvari le to, da zna ovijati le funkcije z enim argumentom. Če hočemo oviti takšne s poljubnim številom, je potrebno dekorator malenkost spremeniti.

    def cache(f):
        stored = {}

        def cached(*x):
            if x not in stored:
                stored[x] = f(*x)
            return stored[x]

        return cached

Dodali smo dve zvezdici pred `x`, da se ta ne nanaša več na en sam argument temveč na terko z vsemi argumenti.

Če tega ne razumete, nič hudega. Tako ali tako funkcija že obstaja, najdemo jo v modulu `functools`.

    from functools import cache

## Zaključek

Dekoratorji so uporabni še za marsikaj. Četudi se vam morda zdijo eksotika, jih boste - če boste imeli odprte oči - pogosto zagledali in pogosto tudi pisali.