Bevezetés az objektumorientált programozásba

Motiváció

Az objektumorientált (objektumközpontú) programozás (OOP, Object Oriented Programming) olyan programozási paradigma/technológia, mely az objektumok köré szervezi a kód szerkezetét (pl. a függvények helyett). Az OOP (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ő. Kis példákná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 rá az alábbi kódra, melyet egy hallgató írt az órái kredit és óraszám számolására:

In [1]:
# Egy tárgy formátuma: (név, óraszám, kredit)
osszes_targy = [
    ("Info1", 3, 4),
    ("Info2", 3, 3),
    ("Kombi1", 4, 4),
    ("Kombi2", 3, 3)]

felvett_targyak = ["Info1", "Kombi1"]
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[2]
    return kredit

print(osszora(felvett_targyak, osszes_targy))
print(osszkredit(felvett_targyak, osszes_targy))
7
8

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:

In [3]:
# Egy tárgy formátuma: (név, óraszám, kötelező, kredit)
osszes_targy = [
    ("Info1", 3, 2, 4),
    ("Info2", 3, 2, 3),
    ("Kombi1", 4, 3, 4),
    ("Kombi2", 3, 1, 3)]

felvett_targyak = ["Info1", "Kombi1"]
In [4]:
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

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 több helyen 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:

In [5]:
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"]
In [6]:
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

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:

In [7]:
def ujtargy(osszes, nev, oraszam, jelenlet, kredit):
    x = {"nev": nev, "oraszam": oraszam,
         "jelenlet": jelenlet, "kredit": kredit}
    osszes.append(x)

ujtargy(osszes_targy, "Info3", 2, 2, 3)

print(osszes_targy)
print(osszora(felvett_targyak, osszes_targy))
print(osszkredit(felvett_targyak, 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}, {'nev': 'Info3', 'oraszam': 2, 'jelenlet': 2, 'kredit': 3}]
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árak listája, 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. Például az 5 szám példánya az int típusnak.

Hozzunk létre egy Targy 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.

In [8]:
class Targy:
    pass

Ezzel már létezik a Targy osztály. Nem tud még semmit, de létrehozhatunk egy ilyen típusú objektumot az osztály nevével utána zárójellel:

In [9]:
t = Targy()
t2 = Targy()
print(t)
type(t)
<__main__.Targy object at 0x7f841e5399b0>
Out[9]:
__main__.Targy

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

In [10]:
t.nev = "Info2"
t.oraszam = 3

print(t.oraszam)
3
In [12]:
class Targy:
    evfolyam = 2
    
t = Targy()
t.evfolyam
Out[12]:
2
In [13]:
t.evfolyam = 3
t.evfolyam
Out[13]:
3

A . (pont) operátorral érhetjük el egy objektum adattagjait (vagy tagváltozóit) és tagfüggvényeit (metódusait). Ilyen volt pl. a listáknál az append. Az adattagok és metódusok gyűjtőneve az attribútum.

Konstruktor

Bár a fentiek szerint lehetséges egy objektum létrehozásakor egy tagváltozónak előre definiált értéket adni, és ezek értékét később meg lehet változtatni, és lehet újabb tagváltozókat hozzáadni, de szerencsésebb lenne, ha már a példány létrehozásakor adhatnánk értékeket a tagváltozóknak. Ezt végzi el az osztály konstruktora:

In [14]:
class Targy:
    def __init__(self, nev, oraszam, jelenlet, kredit):
        self.nev = nev
        self.oraszam = oraszam
        self.jelenlet = jelenlet
        self.kredit = kredit

Egy osztály konstruktora a speciális nevű __init__ különleges (speciális) metódus. Akkor fejti ki hatását, amikor létrehozunk egy példányt egy osztályból. Ezt az osztály neve után zárójel és esetleges paraméterek felsorolásával tehetjük meg.

t = Targy( x, y, ... )

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

Az __init__ első argumentumát self-nek szokás nevezni, ezzel kifejezve, hogy ez az argumentum utal magára a létrehozott példányra. De bármilyen más változónév is használható. Az előzővel ekvivalens a következő definíció is:

In [ ]:
class Targy:
    def __init__(targy, neve, oraszama, jelen, kr):
        targy.nev = neve
        targy.oraszam = oraszama
        targy.jelenlet = jelen
        targy.kredit = kr

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

In [15]:
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"]
In [16]:
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

print(osszora(felvett_targyak, osszes_targy))
print(osszkredit(felvett_targyak, osszes_targy))
7
8

Példa: Hozzunk létre egy Komplex osztályt komplex számok kezelésére.

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

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

Milyen jó lenne, ha tudnánk összeadni komplexeket. Írjunk hát erre egy függvényt:

In [18]:
class Komplex:
    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

Metódusok

Ez a függvény valójában szorosan tartozik a Komplex osztályhoz, jobb ha oda is tesszük (nem globális, csak lokális függvény).

In [19]:
class Komplex:
    def __init__(self, real, imaginary):
        self.re = real
        self.im = imaginary
        
    def osszeg(k1, k2):        # lokális
        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

Még jobb ha nem kell kiírni az osztály nevét, csak a két megfelelő dolgot összeadni. Ez a metódus. Ez olyan függvény, mely egy osztályon belül van definiálva, és első argumentuma kötelezően a self (nevezzük azt bárhogy).

In [20]:
class Komplex:
    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 írni, 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 (ami a pont bal oldalán áll), 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 (pont utáni) paraméter a metódus paramétere lesz, ebben az esetben k2.

Speciális metódusok

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

In [21]:
class Komplex:
    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 [22]:
k4 = k1.__add__(k2)
print(k4.re, k4.im)
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).

Speciális metódus lehet más is, például: __sub__, __mul__, __div__. De ilyen volt az __init__ is.

Ezek azért speciálisak mert nem csak a nevükkel, hanem operátor által is meg lehet hívni. Ezek a python nyelv által definiált véges névkészletből valóak. Mindig két aláhúzás-karakterrel kezdődnek és azzal végződnek.

Kiírás

A következő sajnos nem tűnik hasznosnak:

In [24]:
print(k3)
k3
<__main__.Komplex object at 0x7f842c314438>
Out[24]:
<__main__.Komplex at 0x7f842c314438>

Van egy speciális metódus, ami a print meghívásakor illetve string-é alakításkor fejti ki hatását.

In [25]:
class Komplex:
    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 __str__(self):
        """a str() és a print() függvényekhez"""
        return str(self.re) + " + " + str(self.im) + "i"

    def __repr__(self):
        """az interaktív outputhoz és a repr() függvényhez"""
        return str(self.re) + " + " + str(self.im) + "i"
    
k1 = Komplex(4, 3)
k2 = Komplex(-2, 1)
k3 = k1 + k2

print(str(k1))
print(k2)
print(k3)    # meghívja a __str__ metódust
k3           # meghívja a __repr__ metódust
4 + 3i
-2 + 1i
2 + 4i
Out[25]:
2 + 4i
In [26]:
str(Komplex(3, 2))
Out[26]:
'3 + 2i'

A __str__ speciális metódusnak egy stringet kell visszaadnia és amikor meghívunk egy ilyen típusú objektumon egy kiírást, akkor ez a metódus fog lefutni.

Esetünkben ez még nem tökéletes:

In [27]:
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 [ ]: