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:
# 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"]
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))
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:
# 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"]
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))
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:
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"]
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))
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:
"nev"
, "oraszam"
, stb.).ujtargy
nevű függvényt, és mindig azt használjuk ha tantárgyat akarunk létrehozni a kódban:
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))
Ez a dokumentáláson is segít, írhatja azt pl. az osszora
függvény dokumentációja hogy
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.
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.
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:
t = Targy()
t2 = Targy()
print(t)
type(t)
Sőt, akár ennek adhatunk adattagokat is (mint a valós és képzetes rész a komplexeknél):
t.nev = "Info2"
t.oraszam = 3
print(t.oraszam)
class Targy:
evfolyam = 2
t = Targy()
t.evfolyam
t.evfolyam = 3
t.evfolyam
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.
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:
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:
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:
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"]
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))
Példa: Hozzunk létre egy Komplex
osztályt komplex számok kezelésére.
class Komplex:
def __init__(self, real, imaginary):
self.re = real
self.im = imaginary
k = Komplex(4, 3)
print(k.re, k.im)
Milyen jó lenne, ha tudnánk összeadni komplexeket. Írjunk hát erre egy függvényt:
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)
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).
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)
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).
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)
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
.
Ez így már tűrhetően olvasható, de az az igazság, hogy még ennél is szebbé tehetjük:
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)
Persze az __add__
metódus közvetlen meghívásával is lehetne:
k4 = k1.__add__(k2)
print(k4.re, k4.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 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.
A következő sajnos nem tűnik hasznosnak:
print(k3)
k3
Van egy speciális metódus, ami a print
meghívásakor illetve string-é alakításkor fejti ki hatását.
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
str(Komplex(3, 2))
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:
print(Komplex(0, 0)) # 0
print(Komplex(3, 0)) # 3
print(Komplex(-2, 1)) # -2 + i
print(Komplex(-2, -1)) # -2 - i