19. Objektno programiranje#

Objektno programiranje je resna tema, ki se je ni mogoče kar tako naučiti v nekaj tednih, vseeno pa je koristno, da se naučimo osnove. Ker vam mora priti stvar v kri, pa ne pričakujmo čudežev. Pri krožku se boste naučili zgolj nekaterih tehnikalij, ne pa vsega, kar v zvezi s tem ponuja Python, vsega, kar ponujajo drugi jeziki in, predvsem, filozofije, ki je za tem.

19.1. Dedovanje#

Definirajmo razred Oseba. Vsaka oseba ima ime in spol (Z ali M); oba podatka podamo kot argumenta konstruktorju. Oseba zna pozdravljati: metoda pozdravi izpiše Pozdravljeni, jaz sem {}, kjer namesto {} vstavi svoje ime.

class Oseba:
    def __init__(self, ime, spol):
        self.ime = ime
        self.spol = spol

    def pozdravi(self):
        print("Pozdravljeni, jaz sem {}".format(self.ime))

Tu smo mimogrede prvič srečali konstruktor (metodo __init__) z argumenti. Ob konstrukciji objekta Oseba moramo vedno povedati tudi njeno ime in spol.

benjamin = Oseba("Benjamin", "M")

Vse, kar naredi konstruktor, je, da prepiše vrednost argumenta v (istoimenski) atribut objekta. Se pravi, shrani argument v objekt.

Osebe znajo tudi vljudno pozdravljati:

benjamin.pozdravi()
Pozdravljeni, jaz sem Benjamin

Na fakulteti sta, poenostavljeno povedano, dve vrsti oseb, študenti in učitelji. Za študente beležimo njihove ocene pri posameznih predmetih. Ocene bomo pisali v slovar, katerega ključi bodo imena predmetov, vrednosti pa ocene pri teh predmetih. Študenta je mogoče oceniti, zato bo imel metodo oceni, ki ji bomo kot argument podali ime predmeta in oceno, pa bo zapisala to v slovar. Poleg tega bo imel funkcijo povprecje, ki bo izračunala njegovo poprečno oceno.

Poleg tega pa imajo tudi študenti ime in spol, in tudi pozdravljati znajo. Z drugimi besedami: študent je vrsta osebe. Študent ima vse lastnosti osebe in zna vse, kar znajo osebe, poleg tega pa še nekaj več (beležiti ocene in računati poprečja). Zato bomo razred Student izpeljali iz razreda Oseba (Angleži bi rekli Student is derived from Oseba). Student bo podedoval vse, kar ima Oseba (Student inherits attributes and methods from Oseba). Razred Oseba bo prednik (parent) razreda Student.

Kako to storimo? Zelo preprosto: ko definiramo razred Student, v oklepaju dodamo še razred, iz katerega naj bo ta izpeljan.

class Student(Oseba):
    def __init__(self, ime, spol):
        super().__init__(ime, spol)
        self.ocene = {}

    def oceni(self, predmet, ocena):
        self.ocene[predmet] = ocena

    def poprecje(self):
        if not self.ocene:
            return 0
        return sum(self.ocene.values()) / len(self.ocene)

Najprej preverimo, ali reč deluje.

rebeka = Student("Rebeka", "Z")
rebeka.oceni("Programiranje 1", 10)
rebeka.oceni("Uvod v racunalnistvo", 9)
rebeka.poprecje()
9.5

Sestavimo tri študente, zložimo jih v seznam in vsakemu študentu dodelimo deset naključnih ocen pri naključno izbranih predmetih.

from random import choice, randint

s1 = Student("Ana Karenina", "Z")
s2 = Student("Berta Novak", "Z")
s3 = Student("Cilka Celarec", "Z")

studenti = [s1, s2, s3]

predmeti = ["Uvod v racunalnistvo", "Programiranje 1", "Racunalniska arhitektura",
            "Matematika", "Diskretne strukture", "Programiranje 2",
            "Podatkovne baze", "Racunalniske komunikacije", "Operacijski sistemi",
            "Osnove verjetnosti in statistike"]

for student in studenti:
    for i in range(10):
        student.oceni(choice(predmeti), randint(6, 10))

Zdaj lahko izpišemo poprečne ocene vseh študentov.

for student in studenti:
    print(student.ime, student.poprecje())
Ana Karenina 8.142857142857142
Berta Novak 8.0
Cilka Celarec 7.571428571428571

Vse to so bile študentske zadeve. Ker je Rebeka tudi oseba, pa zna tudi pozdravljati.

rebeka.pozdravi()
Pozdravljeni, jaz sem Rebeka

Še enkrat preverite in se prepričajte: metode pozdravi nismo definirali v razredu Student, Rebeka pa vseeno pozdravlja. Ta metoda je torej podedovana od razreda-prednika Oseba.

Zdaj pa poglejmo definicijo metod razreda. Dodeljevanje ocene (oceni) je trivialno. Pri računanju poprečja (poprecje) moramo paziti le na študente, ki nimajo še nobene ocene, zato bo vseh tam enak 0.

Zanimiv pa je konstruktor, ki mora le pripraviti slovar, v katerega bo metoda oceni dodajala ocene. Ostalo pa prepusti podedovanemu konstruktorju - tako da ga pokliče.

Razred Student je podedoval, recimo, metodo pozdravi. Ko rečemo rebeka.pozdravi(), se pokliče podedovano pozdravljanje. Student je sestavil novo metodo poprecje in tudi tu ni dilem: ko rečemo rebeka.poprecje(), se pokliče funkcija oceni iz razreda Student. Metodi __init__ pa sta dve, podedovana in nova! Pravilo je preprosto: istoimenska metoda iz izpeljanega razreda povozi podedovano metodo.

Da je res tako, se najprej prepričajmo na preprostem zgledu: razredu Student definirajmo novo metodo pozdravi.

class Student(Oseba):
    def __init__(self, ime, spol):
        super().__init__(ime, spol)
        self.ocene = {}

    def oceni(self, predmet, ocena):
        self.ocene[predmet] = ocena

    def poprecje(self):
        if not self.ocene:
            return 0
        return sum(self.ocene.values()) / len(self.ocene)
        
    def pozdravi(self):
        print("Pozdravljeni, jaz sem {} in sem student(ka)".format(self.ime))

Sestavimo novo Rebeko in jo pozovimo, naj nas pozdravi.

rebeka = Student("Rebeka", "Z")
rebeka.pozdravi()
Pozdravljeni, jaz sem Rebeka in sem student(ka)

Rebeka ima tule dve metodi pozdravi, vendar se pokliče nova in ne podedovana.

Kaj pa, če bi hoteli poklicati podedovano metodo in ne nove? Tega ne počnemo. To je nenaravno, ne predstavljam si situacije, kjer bi nam v lepo napisanem programu to moglo priti prav … razen v enem primeru: ko nova metoda kliče staro. Tule lahko razmišljamo takole: Student je izpeljan iz Oseba in torej zna pozdravljati. Vse, kar doda k pozdravu, je “in sem student(ka)”. To pomeni, da bi lahko najprej prepustili pozdravljanje podedovani metodi in v Studentovem pozdravi le še dodatno izpisali “in sem student(ka)”. To se stori takole:

class Student(Oseba):
    def __init__(self, ime, spol):
        super().__init__(ime, spol)
        self.ocene = {}

    def oceni(self, predmet, ocena):
        self.ocene[predmet] = ocena

    def poprecje(self):
        if not self.ocene:
            return 0
        return sum(self.ocene.values()) / len(self.ocene)
        
    def pozdravi(self):
        super().pozdravi()
        print("in sem student(ka)")

Rezultat sicer ni povsem enak (“in sem student(ka)” je namreč napisano v novi vrsti), a za primer bomo to vzeli v zakup.

super() je čudna funkcija, ki nam - precej poenostavljeno - vrne self kot da bi bil objekt razreda, ki je prednik razreda Student (torej razreda Oseba). Torej, če rečemo self.pozdravi() se pokliče metoda pozdravi, ki smo jo definirali v razredu Student, saj je self objekt razreda Student. Če pa pokličemo super().pozdravi(), se pokliče metoda pozdravi razreda Oseba, ker je razred Oseba prednik razreda Student. (Za tiste, ki veste, kaj je večkratno dedovanje: funkcija super v resnici vrača nekaj veliko veliko bolj zapletenega, saj je potrebno, kadar dedujemo iz večih razredov, vsakič poiskati, kateri od podedovanih razredov vsebuje funkcijo, ki jo želimo klicati. Če hočete moder nasvet: večkratno dedovanje je gladko tlakovana široka pot v pekel. Izogibajte se ga.)

Zdaj lahko končno pogledamo, kaj naredi konstruktor. Poleg self dobi še dva argumenta, ime, spol. Zanju poskrbi stari, podedovani konstruktor, zato ga pokličemo, takole: super().__init__(ime, spol). V novem konstruktorju je potrebno le še pripraviti slovar z ocenami.

Pa definirajmo še razred Ucitelj. Tudi ta je Oseba (ima ime in starost in zna pozdravljati). Ena od njegovih lastnosti je seznam predmetov, ki jih predava.

class Ucitelj(Oseba):
    def __init__(self, ime, spol, predmeti):
        super().__init__(ime, spol)
        self.predmeti = predmeti
        
u1 = Ucitelj("Tomaž Poljanšek", "M", ["Programiranje 2"])

Konstruktor nas, upam, ne preseneča več: ime in spol prepusti podedovanemu konstruktorju, sam pa le še nastavi seznam predmetov.

19.2. Kar je, je; česar ni, pa ni#

Sestavimo funkcijo, ki izpiše imena vseh oseb na podanem seznamu.

def izpisi_imena(s):
    for e in s:
        print(e.ime)

Pokličemo jo lahko s seznamom študentov.

izpisi_imena(studenti)
Ana Karenina
Berta Novak
Cilka Celarec

Pokličemo jo lahko tudi s seznamom, v katerem so tako učitelji kot študenti.

ljudje = [s1, s2, s3, u1]
izpisi_imena(ljudje)
Ana Karenina
Berta Novak
Cilka Celarec
Tomaž Poljanšek

Pa če imamo kar nek seznam?

s = [1, 2, 3]
izpisi_imena(s)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-16-4fcdfb3963eb> in <module>
      1 s = [1, 2, 3]
----> 2 izpisi_imena(s)

<ipython-input-13-207846f9e46f> in izpisi_imena(s)
      1 def izpisi_imena(s):
      2     for e in s:
----> 3         print(e.ime)

AttributeError: 'int' object has no attribute 'ime'
s = ["Ana", "Berta", "Cilka"]
izpisi_imena(s)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-17-54c61afa8ccb> in <module>
      1 s = ["Ana", "Berta", "Cilka"]
----> 2 izpisi_imena(s)

<ipython-input-13-207846f9e46f> in izpisi_imena(s)
      1 def izpisi_imena(s):
      2     for e in s:
----> 3         print(e.ime)

AttributeError: 'str' object has no attribute 'ime'

Ne in ne. Ne s seznamom števil ne s seznamom nizov ni zadovoljna. Čemu ne? No, saj pove: 'int' object has no attribute 'ime'. Nekje v funkciji piše e.ime, pri čemer je e element seznama. V prvem primeru to pomeni, da hočemo poklicati 1.ime, kar je očitno traparija. V drugem hočemo klicati, recimo, "Ana".ime, kar prav tako ne gre. Ana sicer ni brez vsega, ima celo kup metod - replace, upper, endswith in še desetine drugih - a metode oz. atributa ime ni med njimi.

Funkcija izpisi_imena ima torej določene zahteve: kot argument ji moramo dati seznam (točneje: nekaj, prek česar je mogoče spustiti zanko for, recimo seznam) in elementi tega seznama morajo biti objekti, ki imajo polje ime. Z drugimi besedami: biti morajo objekti tipa Oseba.

Res? Pravzaprav ne. Zadošča, da imajo polje ime, ni pa treba, da so osebe.

class KrNeki:
    def __init__(self):
        self.ime = "jazbec"
a, b, c = KrNeki(), KrNeki(), KrNeki()

a
<__main__.KrNeki at 0x7f8bc08266d0>
a.ime
'jazbec'
izpisi_imena([a, b, c])
jazbec
jazbec
jazbec

(Tisti, ki so vajeni takoimenovanih statično tipiziranih jezikov, se zgražajo: funkcija izpisi_imena bi morala povedati, kakšnega tipa morajo biti objekti, ki jih sprejme. Tisti, ki imamo radi tudi dinamično tipizirane jezike, pa razumemo, zakaj je včasih boljše eno, včasih drugo.)

Podobno kot Python se obnašajo tudi drugi jeziki iste sorte, na primer JavaScript (kjer so reči še bolj drastične, saj imamo objektov, nimamo pa razredov!). Temu slogu (ne)preverjanja tipov pravimo duck typing: When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck. Pri tem ni pomembno, ali gre v resnici za raco ali ne; če se objekt vede kot raca, ga lahko obravnavamo kot raco.

19.4. Abstraktni razredi#

Kar smo povedali ravnokar, je res res pomembno. To je najpomembnejša reč v objektnem programiranju, zato jo moramo še malo raziskovati.

nekdo = Oseba("Akakij Akakijevič", "M")

nekdo.pozdravi()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-29-a47a23fbd476> in <module>
      1 nekdo = Oseba("Akakij Akakijevič", "M")
      2 
----> 3 nekdo.pozdravi()

<ipython-input-26-f11e22da7a6f> in pozdravi(self)
      5 
      6     def pozdravi(self):
----> 7         print("Pozdravljeni, jaz sem {} {}".format(self.naziv(), self.ime))
      8 
      9 class Student(Oseba):

AttributeError: 'Oseba' object has no attribute 'naziv'

To smo v bistvu že videli zgoraj: študenti niso znali pozdravljati, dokler jim nismo priskrbeli metode naziv. A to v bistvu pomeni, da je napaka v razredu Oseba, ker zahteva nekaj, česar (potencialno) ni.

Temu se ne reče napaka. Temu se reče abstrakten razred. V nekaterih jezikih bi bilo potrebno “napovedati” metodo naziv kot “čisto navidezno metodo” (pure virtual method). Razredu Oseba bi rekli, da je abstrakten (abstract class) in objektov tega razreda ne bi bilo mogoče sestavljati - napako bi dobili že ob klicu Oseba("Akakij Akakijevič", "M"), češ da takšnih, nepopolnih objektov ne sme biti.

V Pythonu in podobnih jezikih ni tako. Razred Oseba je okrnjen, vendar bi lahko načelno imel tudi kakšne metode, ki bi normalo delovale. Študenti bi lahko še vedno prejemali ocene, le pozdravljali ne bi.

19.6. O Nepythonu#

Če želimo na tem predavanju vsaj omeniti tudi objektno programiranje na splošno, ne le v Pythonu, moramo povedati za bistveno razliko med objektnim programiranjem v Pythonu (in drugih tipičnih dinamično tipiziranih jezikih (se opravičujem za besednjak), na primer JavaScriptu) in objektnim programiranjem v statično tipiziranih jezikih, na primer C++, Javi in C#.

Metodam, kakršen je tale naziv, pravimo navidezne metode (virtual method). Za navidezne metode je značilno, da imajo različni podedovani razredi lahko različne metode, pri čemer pa predniki “vedo”, katero metodo poklicati. Tako Oseba.pozdrav včasih pokliče Oseba.naziv, včasih Student.naziv in včasih Ucitelj.naziv. Če bi se pojavil še kak četrti, peti, ali osmi razred s svojim nazivom, bi znal Oseba.pozdrav poklicati tudi tega.

V Pythonu izraz sam nima pomena (čemu “navidezna”?!), pa tudi govoriti o navideznih metodah v Pythonu nima veliko smisla, saj drugačnih metod kot navideznih v Pythonu sploh ni. V večini statično tipiziranih jezikih pa obstajajo tudi “nenavidezne” metode. Če naziv ne bi bil navidezna metoda, bi Oseba.pozdrav vedno poklical Oseba.naziv, četudi bi imel objekt, s katerim imamo opravka, svoj, “specializiran” naziv.

Drugo, po čemer se ti jeziki razlikujejo od Pythona (in Javascripta in podobnih), je, da bi, kot smo že omenili, zahtevali, da je Oseba.naziv nujno definiran (točneje: deklariran), če hočemo, da Oseba.pozdrav kliče self.naziv - ne glede na to, ali bi bila to navidezna metoda ali ne. Včasih se zgodi tudi, da funkcije, kakršna je Oseba.naziv, sploh ne moremo napisati (ker bi morala delati kaj, česar se z Osebo ne da narediti, temveč lahko to delajo šele njeni nasledniki). V tem primeru, uh, bi definirali čisto navidezno metodo (pure virtual method), razred Oseba bi bila abstraktni razred … No, boljše, da ne rinemo v to. Vedite le, da nam je v Pythonu prihranjenih veliko komplikacij.

Na krožku objektno programiranje v Pythonu popraskamo le po površju. Veliko konceptov, kot na primer statične metode, spremenljivke razreda in podobno, ki ste jih morda srečali v drugih jezikih, pozna tudi Python. Vendar je vse to še pretežko. Te stvari boste spoznavali sproti. Še več pa je tehnik, ki so specifične za Python ali pa jih poznajo le skriptni jeziki.