Funkcije

Vsem, ki že znate programirati in torej že znate napisati funkcijo v kateremkoliže jeziku, je potrebno pokazati samo primer in vse bo jasno.

def delitelji(n):
    s = []
    for i in range(1, n):
        if n % i == 0:
            s.append(i)
    return s

To je vse, kar nas zanima od "rednega predavanja".

Zdaj pa detajli v zvezi s funkcijami, s katerimi se na rednih predavanjih skoraj (ali sploh) ne bomo ukvarjali.

Generiki in račje tipiziranje

Vsaka funkcija v Pythonu je generik (ali, po C++-ovsko, template), ne da bi morali to kaj posebej definirati. Točneje, skoraj ne obstaja način, da bi to preprečili. Tako je zato, ker v Pythonu ne definiramo tipov spremenljivk in prav tako ne tipov argumentov ali rezultatov.

Za tiste, ki izraza generik ali template ne poznate: gre za to, da lahko ena in ista funkcija sprejema argumente različnih tipov.

def sestej(a, b):
    return a + b
sestej(1, 5)
6
sestej("Ana", "marija")
'Anamarija'
sestej("Ana", 5)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-5-2c6500e8edaf> in <module>
----> 1 sestej("Ana", 5)

<ipython-input-2-7e63af8aa86d> in sestej(a, b)
      1 def sestej(a, b):
----> 2     return a + b

TypeError: can only concatenate str (not "int") to str

Zadnja funkcija ne deluje, je pa tole lep trenutek za to, da izvemo, kako ne deluje. Ko funkcijo pokličemo s sestej(1, 5), imamo, v bistvu

a = 1
b = 5

Izraz a + b se izvede tako, da Python pokliče metodo a.__add__ in poda b kot argument.

a.__add__(b)
6

Metoda a.__add__ vrne vsoto "sebe" in argumenta.

Podobno je z nizi.

a = "Ana"
b = "marija"
a.__add__(b)
'Anamarija'

V zadnjem primeru, sestej("Ana", 5) pa imamo

a = "Ana"
b = 5
a.__add__(b)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-9-24945ae2d4a7> in <module>
      1 a = "Ana"
      2 b = 5
----> 3 a.__add__(b)

TypeError: can only concatenate str (not "int") to str

Python se torej preprosto ne vznemirja zaradi tipov. Vsaka funkcija se vede kot generik: podamo ji lahko poljubne argumente, funkciji je vseeno, Pythonu je vseeno ... če stvari ne gredo skupaj, pa bo prišlo do napake, ko nekdo nečesa ne bo mogel narediti. Se pravi, ko si bo moral niz prišteti število, ko bomo poskušali indeksirati float (v tem primeru bo Python poklical float-ovo metodo __item__ in se bo izkazalo, da le-ta te metode nima) ...

Temu se reče duck typing: če nek objekt hodi kot raca in gaga kot raca, potem je raca. Vsaj za all intents and purposes.

Funkcije lahko pišemo z mislijo na to.

def vsota(s):
    v = 0
    for x in s:
        v += x
    return v

Tej funkciji lahko podamo kot argument stvari, prek katerih je možno nagnati zanko for (niz, seznam, terka, range, slovar, množica, datoteka...) in katere elementi so stvari, ki jih je možno seštevati. No, skoraj tako.

vsota({1, 2, 3})
6
vsota(range(5))
10
vsota(["Ana", "Berta", "Cilka"])
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-13-3951a9717356> in <module>
----> 1 vsota(["Ana", "Berta", "Cilka"])

<ipython-input-10-0f50951fc0c7> in vsota(s)
      2     v = 0
      3     for x in s:
----> 4         v += x
      5     return v

TypeError: unsupported operand type(s) for +=: 'int' and 'str'

To ne deluje, ker v začetku nastavimo v = 0, potem pa poskušamo v += "Ana". Naša funkcija vsota torej ne zahteva le, da le, da se dajo elementi seštevati, temveč tudi, da se dajo prišteti k 0. Vsaj prvi od teh elementov.

Prva rešitev bi bila

def vsota(s):
    v = s[0]
    for x in s[1:]:
        v += x
    return v

Zdaj deluje

vsota(["Ana", "Berta", "Cilka"])
'AnaBertaCilka'

in celo

vsota("Benjamin")
'Benjamin'

ker gre pač z zanko čez niz in sešteva njene črke. Vendar naša funkcija zdaj zahteva, da lahko dobimo prvi element s-a in da lahko naredimo rezino, ki vsebuje vse elemente, razen prvega. Zato je nehalo delovati seštevanje množic.

vsota({1, 2, 3})
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-17-5b638e840346> in <module>
----> 1 vsota({1, 2, 3})

<ipython-input-14-1e091943e7c7> in vsota(s)
      1 def vsota(s):
----> 2     v = s[0]
      3     for x in s[1:]:
      4         v += x
      5     return v

TypeError: 'set' object is not subscriptable

Najbolj splošna oblika je

def vsota(s):
    v = None
    for e in s:
        if v == None:
            v = e
        else:
            v += e
    return v

Lahko bi naredili tudi nekoliko drugače, uporabili bi lahko iteratorje. A to se bomo naučili kdaj drugič.

Več ali manj kot en rezultat

V Pythonu vse funkcije vračajo rezultat. Nobenih void funkcij. In to natančno en rezultat. Ne dveh. Nekaj primerov.

def ne_vrne_nicesar():
    x = 42

def vrne_odgovor():
    return 42

def vrne_dva():
    return "Koliko je 6x8?", 42

Najprej pokličimo funkcijo, ki ne vrne ničesar.

nic = ne_vrne_nicesar()
nic

No, vidite, nič se ne izpiše. :)

To je zato, ker Jupyter oz. IPython oz. Python v konzoli izpiše None le, če eksplicitno pokličemo print.

print(nic)
None

Če torej funkcija ne vrne ničesar, vrne None.

Druge funkcije nima smisla klicati. Pač pa bomo tretjo.

vprašanje, odgovor = vrne_dva()
vprašanje
'Koliko je 6x8?'
odgovor
42

Tudi ta funkcija je vrnila le eno stvar. Terko ("Koliko je 6x8?", 42). Le da smo jo napisali brez oklepajev. Kadar funkcija navidez vrača več stvari, napišemo return tako, da je videz pravi - kot da vračamo več stvari torej. In tudi kličemo jo, kot da bi vračala več stvari. V resnici pa gre za igro terk.

None je prikladna vrednost, ki lahko pove, da funkcija ni mogla vrniti ničesar pametnega.

def prvo_sodo(s):
    for e in s:
        if e % 2 == 0:
            return e
prvo_sodo([1, 2, 3, 4, 5])
2
prvo_sodo([1, 3, 5])

Tu je potrebno biti previden. Najprej: da gornja funkcija vrne None, če ni sodih števil, je praktično "stranski učinek". Tako raje ne pišemo. Lint bi nas na to opozoril. Kadar funkcija včasih vrne rezultat, včasih pa vrne None, pa je vračanje None praviloma eksplicitno. Sicer bi lahko kdo, ki bere funkcijo, mislil, da v nekaterih primerih ne vrne ničesar, ker je programer pozabil na return. Lepo napisana funkcija prvo_sodo je torej

def prvo_sodo(s):
    for e in s:
        if e % 2 == 0:
            return e
    return None

Drugo opozorilo: ne zlorabljajmo None-a. Takšne funkcije nas lahko vodijo v prikrite napake. Če bi takole napisali funkcijo prvo_sodo in prvo_liho, bi lahko kdo ponevedoma takole računal vsoto prvega lihega in prvega sodega števila.

def prvo_liho(s):
    for e in s:
        if e % 2 == 1:
            return e
    return None

s = [1, 3, 5, 7]
prvo_sodo(s) + prvo_liho(s)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-29-da993963350e> in <module>
      6 
      7 s = [1, 3, 5, 7]
----> 8 prvo_sodo(s) + prvo_liho(s)

TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'

Nullable tipi

Dve možnosti imamo.

Očitno smo v obeh primerih na približno istem. To je tudi razlog, da sem približno skopiral odstavek.

To nima zveze s Pythonovim dinamičnim tipiziranjem in še manj z generiki, to je problem mnogih jezikov.

Novejši popularni jeziki (se učijo od starejših bolj teoretičnih jezikov in) to rešujejo z "option"-i ali z "nullable tipi". V Kotlinu bi takšno funkcijo definirali z

fun firstEven(s: List[Int]) -> Int?
    ... in tako naprej

Tip rezultata Int? pomeni, da bo funkcija vrnila Int ... ali pa ne. (V nekaterih jezikih se temu reče "maybe int", "mogoče int"). Tako kot Pythonova funkcija vrne int ali pa bo vrnila None. Če potem poskusimo sešteti

firstEven(s) + firstOdd(s)

nam tega ne bo pustil, ker rezultata teh dveh funkcij nista Int, temveč sta samo "mogoče Int". Tudi to ne gre.

const a: Int = firstEven(s)

Rezultata funkcije firstEven ne moremo prirediti spremenljivki tipa Int - in se potem delati, da obstaja. Biti mora Int?:

const a: Int? = firstEven(s)

Kako se znebimo tega "mogoče"? V Kotlinu tako, da priskrbimo privzeto vrednost ali pa preverimo, da vrednost ni null. Operator a ?: b vrne a, če le-ta ni null. Če je, pa vrne b. Recimo, da bi v primeru, da prvega lihega števila ni, želeli uporabiti kar 1. To naredimo tako: firstEven(s) ?: 1.