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.
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
1, 5) sestej(
6
"Ana", "marija") sestej(
'Anamarija'
"Ana", 5) sestej(
---------------------------------------------------------------------------
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
= 1
a = 5 b
Izraz a + b
se izvede tako, da Python pokliče metodo
a.__add__
in poda b
kot argument.
__add__(b) a.
6
Metoda a.__add__
vrne vsoto "sebe" in argumenta.
Podobno je z nizi.
= "Ana"
a = "marija"
b __add__(b) a.
'Anamarija'
V zadnjem primeru, sestej("Ana", 5)
pa imamo
= "Ana"
a = 5
b __add__(b) a.
---------------------------------------------------------------------------
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):
= 0
v for x in s:
+= x
v 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.
1, 2, 3}) vsota({
6
range(5)) vsota(
10
"Ana", "Berta", "Cilka"]) vsota([
---------------------------------------------------------------------------
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):
= s[0]
v for x in s[1:]:
+= x
v return v
Zdaj deluje
"Ana", "Berta", "Cilka"]) vsota([
'AnaBertaCilka'
in celo
"Benjamin") vsota(
'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.
1, 2, 3}) vsota({
---------------------------------------------------------------------------
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):
= None
v for e in s:
if v == None:
= e
v else:
+= e
v return v
Lahko bi naredili tudi nekoliko drugače, uporabili bi lahko iteratorje. A to se bomo naučili kdaj drugič.
V Pythonu vse funkcije vračajo rezultat. Nobenih void
funkcij. In to natančno en rezultat. Ne dveh. Nekaj primerov.
def ne_vrne_nicesar():
= 42
x
def vrne_odgovor():
return 42
def vrne_dva():
return "Koliko je 6x8?", 42
Najprej pokličimo funkcijo, ki ne vrne ničesar.
= ne_vrne_nicesar()
nic 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.
= vrne_dva() vprašanje, odgovor
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
1, 2, 3, 4, 5]) prvo_sodo([
2
1, 3, 5]) prvo_sodo([
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
= [1, 3, 5, 7]
s + prvo_liho(s) prvo_sodo(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'
Dve možnosti imamo.
Lahko se zmenimo, da je tole v redu. V dokumentaciji funkcije
mora jasno pisati, da bo vrnila None
, če v seznamu ni
sodega oz. lihega števila. Vsak, ki pokliče to funkcijo, je odgovoren,
da popazi na rezultat. Če pozabi - je to njegov problem.
Problem pri tem principu je, da se napaka ne zgodi nujno takoj temveč
precej pozneja. Recimo, da kličemo prvo_sodo
in rezultat
shranimo - bogve kam. Nekje precej kasneje, v čisto drugem koncu
programa, preberemo to vrednost, tam bi morala biti številka ...
naletimo pa na None
. In program se sesuje nekje tam. Zdaj
pa najdi to napako, če jo moreš. Zato velja, da se morajo napake zgoditi
čim prej in ne čim kasneje.
Lahko se torej zmenimo, naj funkcija vrže exception. V dokumentaciji funkcije mora jasno pisati, da bo vrgla exception, če v seznamu ni sodega oz. lihega števila. Vsak, ki pokliče to funkcijo, je odgovoren, da popazi na argumente. Če pozabi - je to njegov problem.
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
(s) + firstOdd(s) firstEven
nam tega ne bo pustil, ker rezultata teh dveh funkcij nista
Int
, temveč sta samo "mogoče Int
". Tudi to ne
gre.
: Int = firstEven(s) const a
Rezultata funkcije firstEven
ne moremo prirediti
spremenljivki tipa Int
- in se potem delati, da obstaja.
Biti mora Int?
:
: Int? = firstEven(s) const a
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
.