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
45) fahr_v_celz(
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):
= fahr_v_celz(x)
y return round(y, 2)
Pazite tole!
def rounder(f):
def rounded(x):
= f(x)
y return round(y, 2)
return rounded
Preden povemo, kako deluje, poglejmo, kako uporabljati.
= rounder(fahr_v_celz)
fahr_v_celz2 = rounder(celz_v_fahr)
celz_v_fahr2 = rounder(km_v_milje)
km_v_milje2 = rounder(milje_v_km) milje_v_km2
45) fahr_v_celz2(
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.
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
= rounder(fahr_v_celz)
fahr_v_celz
def celz_v_fahr(c):
return c * 9 / 5 + 32
= rounder(celz_v_fahr)
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
.
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:
= f(x)
stored[x] return stored[x]
return cached
@cache
def fahr_v_celz(f):
print(f"Računam pretvorbo iz {f}")
return (f - 32) * 5 / 9
50) fahr_v_celz(
Računam pretvorbo iz 50
10.0
59) fahr_v_celz(
Računam pretvorbo iz 59
15.0
50) fahr_v_celz(
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?
= 0
klicev
def fibo(n):
global klicev
+= 1
klicev
if n == 0 or n == 1:
return 1
return fibo(n - 1) + fibo(n - 2)
30) fibo(
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.
= 0
klicev
@cache
def fibo(n):
global klicev
+= 1
klicev
if n == 0 or n == 1:
return 1
return fibo(n - 1) + fibo(n - 2)
30) fibo(
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.
100) fibo(
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.
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:
= f(*x)
stored[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
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.