Bevezetés az objektumorientált programozásba

Motiváció

Nézzük rá az alábbi kódra, melyet egy hallgató írt az órái kredit és óraszám számolására (tutor link):

In [ ]:
def osszora(felvett, minden):
    ora = 0
    for targy in minden:
        if targy[0] in felvett:
            ora += targy[1]
    return ora

def osszkredit(felvett, minden):
    kredit = 0
    for targy in minden:
        if targy[0] in felvett:
            kredit += targy[2]
    return kredit


# Egy targy formatuma: (nev, oraszam, kredit)
osszes_targy = [
    ("Info1", 3, 4),
    ("Info2", 3, 3),
    ("Kombi1", 4, 4),
    ("Kombi2", 3, 3)]

felvett_targyak = ["Info1", "Kombi1"]
print osszora(felvett_targyak, osszes_targy)
print osszkredit(felvett_targyak, osszes_targy)

(Megjegyzés: Az objektumorientált programozás, a kivételekkel való hibakezeléshez hasonlóan, olyan technológia, amelynek az előnyei igazán csak nagyobb programok írásánál jönnek elő. Ilyen kis példánál el kell túlozni a problémák nagyságát ahhoz hogy megindokoljuk ezen technikák használatát, de attól még hogy itt esetleg a megoldás bonyolultabbnak tűnik mint a probléma, nagyobb programoknál ugyanez a megoldás már jobb arányt produkál.)

Nézzük mi történik, ha szeretném külön számontartani, hogy az óraszámból mennyi olyan, ahol jelenlétet ellenőriznek. Az lenne a logikus, ha a tuple-ben az össz óraszám mellett lenne. Ezért más kódot is módosítani kell, míg végül valami ilyesmire jutunk (tutor link):

In [ ]:
def osszora(felvett, minden):
    ora = 0
    for targy in minden:
        if targy[0] in felvett:
            ora += targy[1]
    return ora

def osszkredit(felvett, minden):
    kredit = 0
    for targy in minden:
        if targy[0] in felvett:
            kredit += targy[3]
    return kredit


# Egy targy formatuma: (nev, oraszam, jelenlet, kredit)
osszes_targy = [
    ("Info1", 3, 2, 4),
    ("Info2", 3, 2, 3),
    ("Kombi1", 4, 2, 4),
    ("Kombi2", 3, 1, 3)]

felvett_targyak = ["Info1", "Kombi1"]
print osszora(felvett_targyak, osszes_targy)
print osszkredit(felvett_targyak, osszes_targy)

A fontos dolog az, hogy megváltoztattam hogy mit tárolok el egy-egy tárgyról, és emiatt meg kellett változtatnom az osszkredit függvényt, pedig a krediteket így is, úgy is eltároltam. Ebből látható, hogy az ilyen tuple-ös (vagy listás) megoldás nem fenntartható, ha valamit változtatni akarok a tárolási módszeren, akkor annak következtében mindenhol a kódban változtatnom kell, ahol ezeket az adatokat használom.

Egyik alternatíva szótárban tárolni a dolgokat. Ekkor a lista minden eleme egy szótár, ami pontosan ugyanazokat a kulcsokat tartalmazza (tutor link):

In [ ]:
def osszora(felvett, minden):
    ora = 0
    for targy in minden:
        if targy["nev"] in felvett:
            ora += targy["oraszam"]
    return ora

def osszkredit(felvett, minden):
    kredit = 0
    for targy in minden:
        if targy["nev"] in felvett:
            kredit += targy["kredit"]
    return kredit


osszes_targy = [
    {"nev" : "Info1", "oraszam" : 3,
     "jelenlet" : 2, "kredit" : 4},
    {"nev" : "Info2", "oraszam" : 3,
     "jelenlet" : 2, "kredit" : 3},
    {"nev" : "Kombi1", "oraszam" : 4,
     "jelenlet" : 2, "kredit" : 4},
    {"nev" : "Kombi2", "oraszam" : 3,
     "jelenlet" : 1, "kredit" : 3}]

felvett_targyak = ["Info1", "Kombi1"]
print osszora(felvett_targyak, osszes_targy)
print osszkredit(felvett_targyak, osszes_targy)

Ez nem egy rossz megoldás, így már be lehet rakni új tulajdonságokat nagyobb probléma nélkül. Azért még van vele egy-két probléma:

  • Mindegyik tantárgy létrehozásánál le kell írni az adatmezők neveit (azt hogy "nev", "oraszam", stb.).
  • Ha a kódban több hely is van, ahol létrehozok ilyen tantárgyakat, akkor ha bevezetek ilyen új adatot, akkor mindenhol módosítani kéne, és lehet hogy csak később derül ki ha valahol elfelejtettük módosítani.
  • Minden függvénynél ami ilyen formátumban tárolt tantárgyakat vár, dokumentálni kell, hogy ez pontosan mit jelent.
  • </ul> Ezeket még kb. meg lehet oldani ha bevezetünk egy ujtargy nevű függvényt, és mindig azt használjuk ha tantárgyat akarunk létrehozni a kódban (tutor link):

In [ ]:
def osszora(felvett, minden):
    ora = 0
    for targy in minden:
        if targy["nev"] in felvett:
            ora += targy["oraszam"]
    return ora

def osszkredit(felvett, minden):
    kredit = 0
    for targy in minden:
        if targy["nev"] in felvett:
            kredit += targy["kredit"]
    return kredit


def ujtargy(nev, oraszam, jelenlet, kredit):
    return {"nev" : nev, "oraszam" : oraszam,
            "jelenlet" : jelenlet, "kredit" : kredit}

osszes_targy = [
    ujtargy("Info1", 3, 2, 4),
    ujtargy("Info2", 3, 2, 3),
    ujtargy("Kombi1", 4, 2, 4),
    ujtargy("Kombi2", 3, 1, 3)]

felvett_targyak = ["Info1", "Kombi1"]
print osszora(felvett_targyak, osszes_targy)
print osszkredit(felvett_targyak, osszes_targy)

Ez a dokumentáláson is segít, írhatja azt pl. az osszora függvény dokumentációja hogy "A minden paraméter tantárgyak adatait tartalmazó szótárakat tartalmaz, melyek az ujtargy függvénnyel lettek létrehozva. Azt is megoldja, hogy ha a kódban elfelejtjük mindenhol betenni a plusz paramétereket, akkor már egyből az objektum létrehozásakor szólni fog a python hogy van hiányzó paraméter, nem csak később, használat közben derül ki esetleg a hiba.

Így már elég közel járunk egy tényleges osztály koncepcióhoz.

Osztály és objektum

Az osztályra gondolhatunk úgy, mint egy típusra (pl lista), míg az objektumra úgy miny egy ilyen típusú valami.

Hozzunk létre egy Komplex osztályt. (Szokás az osztályok neveit mind nagybetűsnek venni, hogy könnyebben megkülönböztethető legyen. A python dokumentáció is ajánlja ezt mint egy lehetőséget, bár a python beépített osztályok nem követik ezt a módszert. Az "object" helyére kerülhetne más is elvileg, de ezt csak később látjuk majd.)

In [ ]:
class Komplex(object):
    pass

Ezzel már létezik a Komplex osztály. Nem tud még semmit, de létrehozhatunk egy ilyen típusú objektumot:

In [ ]:
k = Komplex()

Sőt, akár ennek adhatunk adattagokat is (valós és képzetes rész):

In [ ]:
k.re = 5
k.im = 2

A . (pont) operátorral érhetjük el egy objektum adattagjait és metódusait. (Ilyen volt pl a listáknál az append.)

Bár lehetséges egy objektum létrehozása után adattagokat adni hozzá, szerencsésebb lenne, ha a létrehozásakor adtuk volna meg neki ezeket az értékeket. Ehhez létezik az osztálynak konstruktora:

In [ ]:
class Komplex(object):
    def __init__(self, real, imaginary):
        self.re = real
        self.im = imaginary

k = Komplex(4, 3)
print k.re
print k.im

Egy osztály konstruktora a speciális nevű \__init__ metódus (osztályon belül definiált függvény = metódus).

Ennek a self paramétere az épp létrehozandó objektumra mutat, így állítjuk be a létrehozandó objektum adattagjait.

Tutor magyarázat

Nézzük meg most a korábbi példát osztályokkal (tutor link):

In [ ]:
def osszora(felvett, minden):
    ora = 0
    for targy in minden:
        if targy.nev in felvett:
            ora += targy.oraszam
    return ora

def osszkredit(felvett, minden):
    kredit = 0
    for targy in minden:
        if targy.nev in felvett:
            kredit += targy.kredit
    return kredit

class Targy(object):
    def __init__(self, nev, oraszam, jelenlet, kredit):
        self.nev = nev
        self.oraszam = oraszam
        self.jelenlet = jelenlet
        self.kredit = kredit

osszes_targy = [
    Targy("Info1", 3, 2, 4),
    Targy("Info2", 3, 2, 3),
    Targy("Kombi1", 4, 2, 4),
    Targy("Kombi2", 3, 1, 3)]

felvett_targyak = ["Info1", "Kombi1"]
print osszora(felvett_targyak, osszes_targy)
print osszkredit(felvett_targyak, osszes_targy)

Térjünk most vissza a komplexes példára. Milyen jó lenne, ha tudnánk összeadni komplexeket. Írjunk hát erre egy függvényt:

In [ ]:
class Komplex(object):
    def __init__(self, real, imaginary):
        self.re = real
        self.im = imaginary
        
def komplex_osszeg(k1, k2):
    uj_re = k1.re + k2.re
    uj_im = k1.im + k2.im
    return Komplex(uj_re, uj_im)

k1 = Komplex(4, 3)
k2 = Komplex(-2, 1)
k3 = komplex_osszeg(k1, k2)

print k3.re
print k3.im

Nade mit kerese ez a függvény csak úgy a kódunkban, mikor érezhető, hogy valójában a Komplex osztályhoz tartozik:

In [ ]:
class Komplex(object):
    def __init__(self, real, imaginary):
        self.re = real
        self.im = imaginary
        
    def komplex_osszeg(k1, k2):
        uj_re = k1.re + k2.re
        uj_im = k1.im + k2.im
        return Komplex(uj_re, uj_im)

k1 = Komplex(4, 3)
k2 = Komplex(-2, 1)
k3 = Komplex.komplex_osszeg(k1, k2)

print k3.re
print k3.im

Figyeljük meg, hogy a függvényt Komplex.komplex_osszeg-két értük el. Nem túl szép ez így, de lehetne szebben:

In [ ]:
class Komplex(object):
    def __init__(self, real, imaginary):
        self.re = real
        self.im = imaginary
        
    def osszeg(self, k2):
        uj_re = self.re + k2.re
        uj_im = self.im + k2.im
        return Komplex(uj_re, uj_im)

k1 = Komplex(4, 3)
k2 = Komplex(-2, 1)
k3 = k1.osszeg(k2)

print k3.re
print k3.im

Tehát úgy tudunk metódust gyártani, ha egy osztályon belüli függvény első paraméterét self-re állítjuk. Ekkor a self arra az objektumra fog utalni, amelyen meghívtuk a függvényt, jelen esetben k1-re.

Az összes többi paraméter a metódus paramétere lesz, ebben az esetben k2.

Ez így már tűrhetően olvasható, de az az igazság, hogy még ennél is szebbé tehetjük:

In [ ]:
class Komplex(object):
    def __init__(self, real, imaginary):
        self.re = real
        self.im = imaginary
        
    def __add__(self, k2):
        uj_re = self.re + k2.re
        uj_im = self.im + k2.im
        return Komplex(uj_re, uj_im)

k1 = Komplex(4, 3)
k2 = Komplex(-2, 1)
k3 = k1 + k2

print k3.re
print k3.im

Az \__add__ is egy speciális metódus, mely a + operátor működését definiálja. Első paramétere self a bal oldali objektumre (ebben az esetben k1-re) utal, míg második paramétere a jobb oldalira (ebben az esetben k2-re).

Hasonlóan van ilyen a többi operátorra: \__sub\__, \__mul\__, \__div\__

Egy utolsó szépséghiba a kódunkban még a kiírás, hisz ideális esetben ezzel szépen írná ki a számot:

In [ ]:
print k3

Erre is van megoldás:

In [ ]:
class Komplex(object):
    def __init__(self, real, imaginary):
        self.re = real
        self.im = imaginary
        
    def __add__(self, k2):
        uj_re = self.re + k2.re
        uj_im = self.im + k2.im
        return Komplex(uj_re, uj_im)
    
    def __repr__(self):
        s = ""
        s += str(self.re)
        s += " + "
        s += str(self.im)
        s += "i"
        return s

k1 = Komplex(4, 3)
k2 = Komplex(-2, 1)
k3 = k1 + k2

print k3

A \__repr__ speciális metódusnak egy stringet kell visszaadnia és amikor meghívunk egy ilyen típusú objektumon egy kiírást, akkor ezt a metódust fogja használni az objektum reprezentálására.

Ez természetesen még nem tökéletes, hisz például negatív képzetes résznél csúnya, de lehetne még tovább javítani:

In [ ]:
print Komplex(-2, -1)
In [ ]: