# Prvorazredni objekti

V programu imamo različne objekte - številke, nize, sezname, terke… V programu ustvarjamo nove objekte. Objekti izginejo iz pomnilnika, ko jih nihče več ne potrebuje. Objekte lahko zlagamo v sezname in terke. Objekte lahko podajamo funkcijam kot argumente. Objekte lahko dobimo kot rezultat funkcije.

Nekateri jeziki (Python je med njimi, klasični prevajani jeziki pa tipično ne) se odlikujejo po tem, da je tudi funkcija objekt. Prvorazredni objekt. To pomeni, da lahko s funkcijami počnemo vse, kar lahko počnemo z drugimi objekti. Lahko ustvarjamo nove funkcije; in funkcije lahko izginejo iz pomnilnika, ko jih nihče več ne potrebuje. Lahko jih zlagamo v seznem, podajamo kot argumente in dobimo kot rezultat.

Objekti se seveda razlikujejo po svojih zmožnostih: nekatere lahko odštevamo in delimo, druge le seštevamo, tretjih niti to. Funkcij pač ne moremo sešteti. Lahko pa jih pokličemo. Prek seznamov pa lahko gremo z zanko `for`, česar ni mogoče početi ne s funkcijami ne s števili. A to je stvar definicije tipa. Bistvo pa je, da lahko z vsakim objektom, najsibo število, niz ali funkcija, počnemo stvari, našetete v prejšnjem odstavku.

## Kaj je definicija funkcije?

Ko Python vidi definicijo funkcije, priredi pripadajočo kodo (in še kaj zraven) imenu. Definicija funkcije, na primer,

    def f(x):
        return 2 * x

je le nekoliko drugače zapisano prirejanje. Lahko si predstavljamo, da je gornje ekvivalentno spodnjemu.

    f = def(x):
        return x

To sicer ni pravilna koda v Pythonu, v, recimo, Javascriptu pa v resnici lahko dejansko napišemo

    > f = function(t) { return 2 * t }
    > f(5)
    10

(ali pa kaj še krajšega, a nepoznavalcu Javascripta manj očitnega).

### Za uvod in okus

Imena funkcij so samo imena. Tako kot imena kateregakoli drugega objekta. Torej ni nič narobe (a tudi nič posebej prav), če rečemo

    napiši = print

in potem

    napiši(42)

    42

Zdaj se `napiši` nanaša na isti objekt kot `print`. Imenu `print` lahko celo priredimo drugo vrednost (ker je to seveda totalno smiselno).

    print = 42
    napiši(print)

    42

ali celo

    print = abs
    abs(-5)

    5

V tej obliki je to čisto brez zveze, vendar nam bo morda pomagalo razumeti, kar sledi, v po-naslednjem razdelku.

Zdaj pa samo popravimo `print`, da ne bo kasneje štale.

    print = napiši

### Lambda-funkcije

Tudi v Pythonu lahko takole, na hitro definiramo funkcijo. Takšnim funkcijam navadno rečemo lambda-funkcije.

Torej: `lambda x, y: x + y` je funkcija, ki prejme dva argumenta (poimenovali smo ju `x` in `y`) ter vrne njuno vsoto. Definicija lambda funkcije je sestavljena iz ključne besede `lambda`, ki ji sledi seznam argumentov, kot pri običajnih funkcijah (a brez oklepajev), nato dvopičje in nato izraz. Brez `return`-a. Samo izraz. In samo en izraz.

Pythonove lambda-funkcije so omejene, zelo omejene. Če bi si človek česa želel v Pythonu, bi bile to boljše lambda-funkcije. Vendar jih ni in najbrž nikoli ne bo. Sintaktično se ne ujamejo z njim. Kdor hoče mogočne lambde (in ostati v mainstream jezikih), naj gre programirat v Kotlinu.

Takšna lambda seveda ničemur ne služi. Če jo hočemo še kdaj videti in poklicati, ji moramo dati ime. Recimo tako:

    sestej = lambda x, y: x + y

To se ne dela. Namesto tega bomo vedno pisali

def sestej(x, y): return x + y

Lambdo lahko pokličemo tudi kar takoj, ne da bi ji dali ime.

    (lambda x, y: x + y)(5, 1)

    6

Tudi to se ne dela, ker je neumno. (Ne spreglejte pa oklepajev: samo funkcijo smo morali dati v oklepaj. Če bi napisali `lambda x, y: x + y(5, 1)`, bi bilo to videti, kot da kličemo funkcijo `y`. Točneje, to je definicija lambde, ki vrača `x + y(5, 1)`.

Če se lambd ne uporablja ne tako ne drugače - kje pa se jih? “Le z menoj, bralec!”, je napisal Bulgakov.

### Funkcija kot argument

Pri Programiranju 1 stalno ponavljamo tole nalogo: iščemo element seznama, ki je največji po določenem kriteriju. Bodisi po velikosti, kot jo razume Python, bodisi po dolžini, po absolutni vrednosti, po številu a-jev, ki jih vsebuje (če gre za nize) … Vsakič napišemo novo funkcijo.

    def najdaljsi(s):
        # Vrne najdaljši element s
        naj_e = None
        naj_dol = None
        for e in s:
            dol = len(e)
            if naj_dol is None or dol > naj_dol:
                naj_dol = dol
                naj_e = e
        return naj_e

    def najvecji_abs(s):
        # Vrne element s z
        naj_e = None
        naj_abs = None
        for e in s:
            abse = abs(e)
            if naj_abs is None or abse > naj_abs:
                naj_abs = abse
                naj_e = e
        return naj_e

    def najvec_a_jev(s):
        # Vrne element (niz) z največ a-ji
        naj_e = None
        naj_ajev = None
        for e in s:
            ajev = e.count("a") + e.count("A")
            if naj_ajev is None or ajev > naj_ajev:
                naj_ajev = ajev
                naj_e = e
        return naj_e

Vse to je očitno eno in isto, razlikuje se le po tem, da enkrat pokličemo `len`, enkrat `abs`, enkrat pa seštevamo število malih in velikih a-jev.

Splošna funkcija `najvecji` bi bila videti tako.

    def max(s, key):
        max_e = None
        max_key = None
        for e in s:
            k = key(e)
            if max_key is None or k > max_key:
                max_key = k
                max_e = e
        return max_e

Vse je enako kot prej, le da namesto `len` ali `abs` kličemo `key`, pri čemer je `key` argument funkcije `max`.

Zdaj lahko naredimo tako

    imena = ["Ana", "Berta", "Dani"]
    max(imena, len)

    'Berta'

kot

    stevila = [22, 5, -6, -42, 1]
    max(stevila, abs)

    -42

V, recimo, prvem primeru, je znotraj funkcije `s` isto kot `imena` (en in isti seznam), `key` isto kot `len`. Ko pokličem `key(e)`, je to isto, kot če bi klicali `len(e)`, saj sta `key` in `len` ena in ista reč.

Kaj pa tretji primer, v katerem ne kličemo funkcije, temveč računamo `e.count("a") + e.count("A")`? Tu moramo funkcijo narediti sami. Lahko bi pisali

    def stevilo_ajev(s):
        return s.count("a") + s.count("A")

    max(imena, stevilo_ajev)

    'Ana'

vendar je to nepraktično. Dolgo. Definirati celo funkcijo za tako preprosto reč. Tu je torej mesto, kjer uporabimo lambde.

    max(imena, lambda s: s.count("a") + s.count("A"))

    'Ana'

Python seveda natančno takšno funkcijo `max` že ima. Razlikuje se le po tem, da moramo argument s ključem nujno podati tako, da ga poimenujemo, na primer. Še en lep primer: v seznamu seznamov poišči seznam z največjo vsoto elementov:

    tabela = [ [1, 2, 3], [1, 2],[10, 1]]
    max(tabela, key=sum)

    [10, 1]

Podoben argument ima tudi funkcija `min`. Imata ga celo metoda `sort` in funkcija `sorted`.

    sorted(stevila, key=abs)

    [1, 5, -6, 22, -42]

    sorted(tabela, key=sum)

    [[1, 2], [1, 2, 3], [10, 1]]

Tule smo uredili števila po njihovi absolutni vrednosti (kot “ključ” za urejanje smo uporabili funkcijo `abs`) in potem še po vsoti njihovih elementov.

### Primer: seznami funkcij

Napišimo funkcijo, ki tabelira podani seznam funkcij na vrednostih od 0 do 1, s korakom 0.1.

    def tabeliraj(fs):
        for x10 in range(11):
            x = x10 / 10
            print(x, end="\t")
            for f in fs:
                print(f(x), end="\t")
            print()

    from math import *
    tabeliraj([sqrt, sin, lambda x: x**2])

    0.0	0.0	0.0	0.0
    0.1	0.31622776601683794	0.09983341664682815	0.010000000000000002
    0.2	0.4472135954999579	0.19866933079506122	0.04000000000000001
    0.3	0.5477225575051661	0.29552020666133955	0.09
    0.4	0.6324555320336759	0.3894183423086505	0.16000000000000003
    0.5	0.7071067811865476	0.479425538604203	0.25
    0.6	0.7745966692414834	0.5646424733950354	0.36
    0.7	0.8366600265340756	0.644217687237691	0.48999999999999994
    0.8	0.8944271909999159	0.7173560908995228	0.6400000000000001
    0.9	0.9486832980505138	0.7833269096274833	0.81
    1.0	1.0	0.8414709848078965	1.0

Z argumentom `end="\t"` smo funkcijo `print` prosili, naj po vsakem izpisu ne gre v novo vrstico temveč le doda tabulator. To ni bistveno. Bistvena je notranja zanka: funkcija `tabeliraj` je kot argument dobila seznam funkcij, torej gremo v notranji zanki čez ta seznam in `f` je vsakič druga funkcija.

Funkcija je seveda na moč neelegantna. O tem, kako to narediti lepše, se bomo še učili; za zdaj le pokažimo, da kdo ne bi imel po krivici slabega mnenja.

    def tabeliraj(fs):
        for x10 in range(11):
            print("\t".join(map(str, [x] + [f(x) for x in fs])))

S številom decimalk v izpisu pa se res ukvarjajmo kdaj drugič.

## Ustvarjanje in vračanje funkcij

Rekli smo, da definicija funkcije ustvari objekt-funkcijo in jo priredi imenu. Potemtakem smemo narediti tudi to.

    def f():
        def g(x):
            return x ** 2

        return g

So jeziki, v katerih ni mogoče pisati funkcij znotraj funkcij. So tudi jeziki, v katerih je to dovoljeno in takšnim funkcijam rečejo lokalne funkcije. In teh funkcij se ne da klicati iz zunanjega okolja. In te funkcije so ustvarjene le enkrat. So pa jeziki, v katerih to deluje boljše.

    t = f()

Kaj bom dobil, če pokličem `f`? Kaj je `t`? Pač, funkcija, ne.

    t(7)

    49

Zdaj pa naredim še

    u = f()

Sta `t` in `u` ista funkcija? V mnogih jezikih bi - če kaj temu podobnega sploh dopuščajo - bili. V mnogih pa ne. Python je med slednjimi. Najboljši dokaz bo tak:

    def f(k):
        def g(x):
            return x ** k

        return g

    kvadriraj = f(2)
    kubiraj = f(3)

    kvadriraj(5)

    kubiraj(5)

Funkcija `f` vsakič ustvari novo funkcijo `g` in jo vrne. Ta funkcija ima dostop do okolja, v katerem je nastala. (Pravo ime za to je: closure.) Zato lahko uporablja `k`.

Deluje celo to

    def f(k):
        k2 = k

        def g(x):
            return x ** k2

        return g

In celo

    def f(k):
        k2 = k

        def g(x):
            return x ** k2

        return g

Zgoraj smo definirali funkcijo `max`, ki prejme seznam in ključ, podobno kot v Python vdelana funkcija `max`. Recimo, da bi potrebovali funkcije `najdaljse_ime`, `najvecja_vsota` in `naj_absolutist`.

    def najdaljse_ime(s):
        return max(s, len)

    def najvecja_vsota(s):
        return max(s, sum)

    def naj_absolutist(s):
        return max(s, abs)

Tole se da narediti tudi preprosteje. (Ja, še preprosteje!) Napišemo lahko funkcijo za sestavljanje takšnih funkcij.

    def naj_funkcija(f):
        def naj_f(s):
            return naj(s, f)
        return naj_f

Preden razložimo, kaj dela to čudo, povejmo, kako se uporablja. Kar smo napisali, je funkcija za sestavljanje funkcij.

    najdaljse_ime = naj_funkcija(len)
    najvecja_vsota = naj_funkcija(sum)
    naj_absolutist = naj_funkcija(abs)

S to `naj_funkcija` lahko sestavljamo nove funkcije, ki vračajo največji element glede na podani kriterij.

Kaj naredi `naj_funkcija`? Definira funkcijo in jo vrne. Nova funkcija se imenuje `naj_f`, vendar je to samo lokalno ime znotraj funkcije `naj_funkcija`. Torej, še enkrat: `naj_funkcija` sestavi neko funkcijo in jo vrne. To funkcijo nato priredimo “spremenljivki” (“imenu” … kakorkoli že hočete reči temu) `najdaljse_ime`, `najvecja_vsota`, `naj_absolutist`… Te funkcije, `najdaljse_ime`, `najvecja_vsota`, `naj_absolutist` so torej funkcije, ki jih je sestavila in vrnila `naj_funkcija`.

Kaj pa dela funkcija, ki jo sestavi `naj_funkcija`? Funkcija, ki jo bo vrnila `naj_funkcija`, sprejme en argument, seznam `s`, in vrne največji element tega seznama, pri čemer ne primerja elementov “direktno”, temveč glede na vrednost funkcije `f`. Katere funkcije `f`? Tiste, ki smo jo podali kot argument funkciji `naj_funkcija`.

## Konec

Kje pa, s tem smo šele začeli. Prihodnjič: dekoratorji!