# Funkcije

Opazili ste - in malo vam gre tudi na živce, bi rekel - da stalno ponavljamo ene in iste zadeve. Recimo: stalno je potrebno iskati ključ seznama, ki mu pripada največja vrednost. V smislu, imamo slovar, katerega ključi so predmeti in vrednosti njihove cene, naloga pa hoče, da poiščemo najdražji predmet.

    cene = {
        'slika': 45,
        'pozlačen dežnik': 29,
        'Meldrumove vaze': 78,
        'skodelice': 83,
        'kip': 107,
        'čajnik': 15,
        'srebrn jedilni servis': 63,
        'perzijska preproga': 21}

In potem se, vsakič znova, ubijamo z

    max_k = max_v = None

    for k, v in cene.items():
        if max_v is None or v > max_v:
            max_v = v
            max_k = k

    print(max_k)

    kip

Opravičujem se za kratka, nezgovorna imena spremenljivk, ampak vsega tega imam res že dosti.

Imam dobro novico in dve slabi.

Dobra novica: obstaja funkcija `argmax`, ki ji podamo slovar in vrne ključ, ki pripada največji vrednosti.

Prva slaba novica: dobra novica ne drži. Funkciji `argmax` ne podamo slovarja, temveč seznam parov. Funkcija primerja druge elemente parov, poišče največjega in vrne pripadajoči prvi element. Vendar ta slaba novica ni tako usodna: funkciji pač podamo `d.items()` (če je `d` naš slovar). Tako bo dobila `[('slika', 45), ('pozlačen dežnik', 29), ('Meldrumove vaze', 78), ('skodelice', 83), ('kip', 107), ('čajnik', 15), ('srebrn jedilni servis', 63), ('perzijska preproga', 21)]` in vrnila `'kip'`, saj je to prvi element, ki mu pripada največji drugi element.

Druga slaba novica: dobra novica sploh ne drži. Take funkcije ni. Fake novica.

Vedno rad citiram nepozabnega srednješolskega profesorja matematike Francija Oblaka: "Česar ni, se pa nar'di." Naredimo torej takšno funkcijo.

Oglejmo si celico 2, v kateri smo računali ta naš slavni maksimum. V njej je zapisano vse, kar mora delati ta funkcija. Seveda pa si želimo, da bi poiskala maksimum v poljubnem slovarju, ne nujno v `cene`. Poleg tega se bomo dogovorili, da tisto, v čemer išče maksimum, ne bo slovar temveč seznam parov. Imeli bomo torej nekaj takšnega:

    max_k = max_v = None

    for k, v in s:
        if max_v is None or v > max_v:
            max_v = v
            max_k = k

To je le prepisana vsebina gornje celice, le `cene.items()` sem zamenjal s `s`. Ta `s` bo tisto, kar je funkcija dobila kot argument.

Kaj se to pravi, "`s` bo tisto, kar je funkcija dobila kot argument"?! No, pač, če bomo poklicali `argmax(cene.items())`, bo funkcija `s` vseboval `cene.items()`.

Očitno bo potrebno postoriti (vsaj) še dvoje. Troje, pravzaprav. Četvero. (To že postaja podobno Španski inkviziciji)!

1. Te vrstice je treba nekako "zgrupirati", povedati Pythonu, da bo to koda neke funkcije.
2. Funkciji je potrebno dati ime (`argmax`).
3. Povedati je potrebno, da bo zahtevala en argument.
4. Povedati je potrebno, pod kakšnim imenom bomo znotraj funkcije videli (dostopali do...) podatka, danega kot argument (oziroma, v splošnem, do podatkov, podanih kot argumenti).

Vse to storimo praktično v eni vrstici.

    def argmax(s):
        max_k = max_v = None

        for k, v in s:
            if max_v is None or v > max_v:
                max_v = v
                max_k = k

1. Z besedo `def` povemo, da sledi definicija ganz nove funkcije. (Dandanašnja mladina sploh ne zna več prave slovenščine, zato jo je treba kdaj spomniti, da obstajajo tudi druge, bolj slovenske besede kot totally.)

2. Sledi ime funkcije, torej `argmax`.

3. Nato pridejo oklepaji in v njih toliko imen, kolikor argumentov zahteva funkcija. Med imena (če jih je več) postavimo vejice. Če funkcija ne mara argumentov, vseeno napišemo oklepaje, čeravno so prazni oklepaji videti neumno. Tu so možne še vsakršne druge komplikacije, kot recimo argumenti, ki jih lahko izpustimo in imajo privzeto vrednost, ali pa funkcije, ki prejemajo poljubno število argumentov, funkcije, ki zahtevajo, da argumente poimenujemo. Začetni tečaj programiranja, sploh za študente, ki najbrž ne bodo profesionalni računalnikarji, vsega tega ne potrebuje.

4. To, kar funkcija dobi kot argument(e), vidimo pod imeni, ki so navedena v oklepajih. Naša funkcija je navedla eno ime, `s`, torej kot argument pričakuje eno stvar in znotraj funkcije se bomo na to stvar sklicevali z imenom `s`. Pod kakšnim imenom je ta stvar znana zunaj, nas niti ne zanima niti (če nismo res vztrajni in dobro poznamo Python) tega ne moremo izvedeti. In prav je tako.

Preskusimo, če deluje!

    print(argmax(cene.items()))

    None

`None`? Kakšen `None`?! Mar nismo obljubili, da bo funkcija vračala ... Aja. Kje pa piše, kaj ta funkcija vrne? V funkciji živi kup spremenljivk - ne le `max_v` in `max_k` temveč tudi `k` in `v`. Python ne more vedeti, katera od njih naj bo rezultat funkcije. Če sploh katera; obstajajo tudi funkcije (recimo `print`), ki ne vračajo ničesar. Da, Python si je očitno predstavljal, da ta funkcija ne bo vračala ničesar, pa se nam je zato posmehnil s tem `None`.

V seznam potreb španske inkvizicije torej moramo dodati še

1. Povedati moramo, kaj funkcija vrne.

in dopolniti funkcijo.

    def argmax(s):
        max_k = max_v = None

        for k, v in s:
            if max_v is None or v > max_v:
                max_v = v
                max_k = k
        return max_k

In jo preskusiti.

    print(argmax(cene.items()))

    kip

To je to: z besedo `return` povemo, kaj naj funkcija vrne.

## Več `return`-ov

Napišimo funkcijo `dragoceni(s)`, ki vrne predmete, katerih cene so večje od 70. Točneje, funkcija prejme seznam parov, pri čemer predpostavljamo, da je drugi element para število. Vrne tiste prve elemente, pri katerih je drugi element večji od `70`. Rezultat morajo biti Meldrumove vaze, skodelice in kip.

    def dragoceni(s):
        for k, v in s:
            if v > 70:
                return k

    predmeti = dragoceni(cene.items())

    print(predmeti)

    Meldrumove vaze

Kaj pa skodelice in kip?

Nekatere je najbrž zaskrbelo že prej, nekateri razumejo zdaj, nekateri pa še bodo. Naloga ni bila ravno najboljše definirana. Kaj točno naj bi funkcija vračala? Niz? Več nizov? Se pravi, kakšnega tipa naj bi bili `predmeti`?

Besedilo naloge je nekako impliciralo, da bo šlo za več nizov, ni pa jasno povedala, kako naj bi bilo teh več nizov shranjenih. Naivno smo postavili `return` v zanko in pričakovali, da se bo izvedel večkrat ... Python pa bo že nekaj naredil s temi več rezultati. No, ne bo.

Trije nauki.

1. Ko se Python naleti na `return`, se izvajanje funkcije konča. Funkcija vrne vrednost, ki sledi `return` in to je to. Če je `return` v zanki, bo zanke pač konec.
2. Tako kot smo ob sestavljanju slovarja sklenili, da bomo poprej razmislili, kaj bodo njegovi ključi in njegove vrednosti, moramo tudi ob sestavljanju funkcije pomisliti, kaj (na primer: kakšnega tipa) bodo njeni argumenti in njen rezultat. Tu nismo.
3. Funkcija lahko vrača tudi druge stvari, recimo sezname.

Tretji nauk še ni naučen, zato le hitro popravimo funkcijo.

    def dragoceni(s):
        dragi = []
        for k, v in s:
            if v > 70:
                dragi.append(k)
        return dragi

    predmeti = dragoceni(cene.items())

    print(predmeti)

    ['Meldrumove vaze', 'skodelice', 'kip']

Napišimo še eno podobno funkcijo s podobnim naukom: funkcijo `obstajajo_dragoceni`, ki vrne `True`, če obstaja kak dragocen predmet in `False`, če ne.

    def obstajajo_dragoceni(s):
        dragi = []
        for k, v in s:
            if v > 70:
                return True
        return False

Razumemo? Ta funkcija ima dva `return`-a. Kot smo uvideli, se lahko zgodi le eden - pač tisti, na katerega naletimo prej. Za vsak predmet preverimo, ali je dragocen. Če je, vrnemo `True`. Kdaj pa bomo vrnili `False`? Po koncu zanke - če se je ta iztekla, ne da bi jo prekinil kak `return True`. Če torej ni bilo nobenega dragocenega predmeta.

## Funkcije se kličejo med sabo

V programih, ki smo jih pisali doslej, smo klicali različne Pythonove funkcije. Tudi v svojih funkcijah jih lahko. V gornjih primerih tega sicer ni bilo veliko, klicali smo le `append`, a najbrž ne bi nihče dvignil obrvi, če bi poklicali tudi kakšen `len` ali `print` ali karkoli drugega. Kaj pa naše funkcije? Lahko v svojih funkcijah kličemo svoje funkcije? Najsevedajše da. Večje programe gradimo kot skladovnice lastnih funkcij; preprostejše funkcije kombiniramo v vedno zapletenejše, nivoje in nivoje visoko. Pri tem predmetu česa prav zapletenega ne bo, preprost primer pa lahko vidimo že tu: funkcija `dragoceni` je splošnejša od funkcije `obstajajo_dragoceni`. Prva vrne seznam dragocenih predmetov, druga pa pove le, ali dragoceni predmeti obstajajo. Druga funkcija bi lahko preprosto poklicala prvo in preverila, ali le-ta vrne prazen seznam.

    def obstajajo_dragoceni(s):
        # Ne programirajte tako!!!
        if dragoceni(s) != []:
            return True
        else:
            return False

Današnji primeri so res bogati. Vsak kaže par stvari. Tale vsaj dve. Uh, tri. Inkvizicija.

1. Programiranje si boste olajšali, če boste razmišljali o tem, kaj že imate in to uporabljali. Še več; včasih boste napisali dve podobni funkciji in če boste pametni, boste tisto, kar jima je skupno, izločili v tretjo funkcijo. Tako boste lahko onidve funkciji skrajšali tako, da boste poklicali, kar je skupnega.
2. Svarilo: funkcija `obstajajo_dragoceni` je s tem postala počasnejša! Prej je zanka `for` tekla do prvega dragocenega predmeta in se tam končala, prekinil jo je `return`. Zdaj `obstajajo_dragoceni` sicer ne vsebuje nobene zanke, vendar sproži zanko v `dragoceni`, ta pa vedno teče do konca, pa še predmete zlaga v nek seznam, ki ga potem zavržemo, saj nas zanima le, ali je prazen ali ne. Nas to boli? Odvisno od situacije. Če takole, za lastne potrebe analiziramo neke kratke datoteke, ni problema. Če bi bili podatki ogromni in če bi funkcijo, kot je `obstajajo_dragoceni` klicali velikokrat, pa bi jo raje pustili takšno, kot je bila.
3. Tako se ne programira!

Nadaljujmo s tretjo točko. `dragoceni(s) != []` je logični izraz, katerega rezultat je že `True` ali `False`. Funkcija bi se pravilneje (ne v smislu tega, da vrača pravilen rezultat, temveč v smislu, uh, smiselnosti) glasila

    def obstajajo_dragoceni(s):
        return dragoceni(s) != []

Nauk 4: funkcije so lahko tudi zelo kratke. V življenju sem napisal že veliko funkcij, dolgih eno samo vrstico. Mogoče malenkost daljših in bolj zapletenih, vendar - funkcije, dolge eno samo vrstico, niso nič izjemnega. V nekaterih jezikih pa se daljših pravzaprav (v nekem smislu) niti ne da pisati.

## Več argumentov

Gornje funkcije ne upoštevajo inflacije. Danes je 80 veliko, jutri pa ne boste mogli v Hofer ali Lidl, ne da bi tam pustili vsaj toliko. (Za arhiv: tole pišem 7. novembra 2023. Bomo videli, kako se bo staralo.)

Napišimo boljši funkciji `dragoceni` in `obstajajo_dragoceni`: poleg seznama naj prejmeta še mejo "dragocenosti". Če bomo poklicali `dragoceni(cene.items(), 70)` bomo dobili enak rezultat kot zdaj, če `dragoceni(cene.items(), 100)` pa samo še kip.

    def dragoceni(s, meja):
        dragi = []
        for k, v in s:
            if v > meja:
                dragi.append(k)
        return dragi

    def obstajajo_dragoceni(s, meja):
        return dragoceni(s, meja) != []

## Argumenti s privzetimi vrednostmi

Mimogrede se naučimo še to: pogosto pišemo funkcije, ki "skoraj ne potrebujejo" nekega argumenta. Recimo, da bi funkcijo, kot je `dragoceni`, klicali na sto mestih v programu in meja bi bila praktično vedno `70`, le v parih primerih pa kaj drugega. V tem primeru bi argumentu `meja` nastavili prizveto vrednost na `70`. Argument bi s tem postal opcijski - lahko ga podamo ali pa tudi ne. Če ga izpustimo, bi imel vrednost 70.

To je preprosto narediti.

    def dragoceni(s, meja=70):
        dragi = []
        for k, v in s:
            if v > meja:
                dragi.append(k)
        return dragi

    def obstajajo_dragoceni(s, meja=70):
        return dragoceni(s, meja) != []

    dragoceni(cene.items(), 100)

    ['kip']

    dragoceni(cene.items())

    ['Meldrumove vaze', 'skodelice', 'kip']

## Več rezultatov ... in nič rezultatov

Kaj se zgodi, če funkcija nima ukaza `return` (ali pa ga ima, vendar med izvajanjem funkcije nikoli ni prišlo do njega, ker je bil, recimo, skrit znotraj nekega `if`, čigar pogoj nikoli ni bil resničen), že vemo. Funkcija v tem primeru vrne `None`.

Funkcije vedno vrnejo natančno eno stvar. Ne nič, ne dve, ne pet. Eno.

To je slabo. Ampak ne preveč, saj se lahko na to pravilo preprosto požvižgamo. Kot prvo: če ne vrnemo ničesar, funkcija vrne `None`. S tem sicer vrne (natančno) eno stvar, vendar nas to nič ne boli. Naj jo.

Enako se požvižgamo na pravilo, da sme funkcija vrniti le eno stvar in jih mirno vrnemo več.

    def argminmax(s):
        min_k = min_v = None
        max_k = max_v = None

        for k, v in s:
            if min_v == None or v < min_v:
                min_v = v
                min_k = k
            if max_v == None or v > max_v:
                max_v = v
                max_k = k
        return min_k, max_k

    najm, najv = argminmax(cene.items())

    print(najm)
    print(najv)

    čajnik
    kip

Razumemo, kaj se dogaja?

`min_k, max_k` je v resnici terka.

    argminmax(cene.items())

    ('čajnik', 'kip')

Ko smo spoznali terke, smo omenili, da jih smemo pisati brez oklepajev, vendar to počnemo v redkih primerih. Tu je primer, ko terke vedno pišemo brez oklepajev. (To je spet nepisano pravilo: lahko bi dodali tudi oklepaja). Delamo se (predstavljamo si), da je `return` vrnil dve stvari (čeprav je v resnici eno, namreč terko z dvema elementoma). Ob klicu funkcije rezultat takoj razpakiramo.

Pa imamo: funkcije, ki navidez vračajo več vrednosti. Ta konkretno, dve.

Zato se lahko na pravilo, da funkcije vedno vračajo natančno eno stvar, preprosto požvižgamo. Navidez vračajo tudi nobene ali več. Da je v resnici le ena, nihče ne bo opazil.