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 [1]:
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)
7
8

(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 e technika használatát. Kb. 500 soros programig könnyű elboldogulni az eddig tanult eszközökkel, de a fölött az OOP elemei látványosan könnyebbé, gyorsabbá teszik a programírást.)

Nézzük mi történik, ha szeretném külön számontartani, hogy az óraszámból hányban ellenőriznek jelenlétet. Az lenne a logikus, ha a tuple-ben az össz óraszám mellett lenne a jelenlét. Így módosul a kód (tutor link):

In [2]:
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]   #<<< itt kell modositani
    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)
7
8

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 [3]:
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)
7
8

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 mindegyik ilyen helyen módosítani kell a kódot, é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 [4]:
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)
7
8

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""".

Ez azt is megoldja, hogy ha a kódban elfelejtjük mindenhol betenni a plusz paramétereket, akkor már az objektum létrehozásakor szól 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 mint egy ilyen típusú példányára.

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 e módszert. Az object helye maradhat üresen is e példában, később látjuk majd, mit írhatunk oda.)

In [5]:
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 [6]:
k = Komplex()

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

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

print k.re
5

A . (pont) operátorral érhetjük el egy objektum adattagjait és tagfüggvényeit (= metódusait = osztályon belül definiált függvényeit). 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 [8]:
class Komplex(object):
    def __init__(self, real, imaginary):
        self.re = real
        self.im = imaginary

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

Egy osztály konstruktora a speciális nevű \__init__ különleges metódus (speciális metódus). Ezek előre adott nevű metódusok, melyek mindig két aláhúzás-karakterrel kezdődnek és azzal végződnek (így a programozó kódja ezekkel nem akadhat össze, mert ilyen nevű metódus csak a megadottak közül választható, amelynek pontosan definiálva van a funkciója).

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 [9]:
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)
7
8

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 [10]:
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, k3.im
2 4

Mit keres ez a függvény csak úgy a kódunkban, hisz érezhető, hogy valójában a Komplex osztályhoz tartozik:

In [11]:
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, k3.im
2 4

Figyeljük meg, hogy a függvényt Komplex.komplex_osszeg-két értük el, hisz így tudatni kell a pythonnal, hogy mely osztályhoz tartozó függvényt hívjuk. Nem túl szép, lehetne kicsit szebben:

In [12]:
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, k3.im
2 4

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. Innen tudja a python, hogy mely osztályhoz tartozó osszeg függvényt kell meghívnia (lehet más osztálynak ilyen nevű függvénye).

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 [13]:
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, k3.im
2 4

Persze az \__add__ metódus közvetlen meghívásával is lehetne:

In [14]:
k4 = k1.__add__(k2)
print k4
print k4.re, k4.im
<__main__.Komplex object at 0x7f6e50282990>
2 4

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 objektumra (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 hiány, a kiírás:

In [15]:
print k3
<__main__.Komplex object at 0x7f6e50282810>

Erre is van különleges metódus:

In [16]:
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):
        return str(self.re) + " + " + str(self.im) + "i"

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

print k3
2 + 4i
In [17]:
Komplex(3, 2)
Out[17]:
3 + 2i

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.

Még ez sem tökéletes:

In [18]:
print Komplex(0, 0)    # 0
print Komplex(3, 0)    # 3
print Komplex(-2, 1)   # -2 + i
print Komplex(-2, -1)  # -2 - i
0 + 0i
3 + 0i
-2 + 1i
-2 + -1i
In [ ]: