Az előző mintapéldákban megtanultuk, hogy a Python
nyelvben milyen fajta változók (számok, karakterláncok, list
-ek, dict
-ek) vannak. Ezeket vezérlő utasításokkal (if
, for
, while
... ) és függvénydefiníciókkal (def
konstrukció) kombinálva megismerkedtünk a hagyományos programozás alapjaival.
Az alábbiakban megismerkedünk a modern kor egyik fő programozási stílusával, melyben nem a műveletek megalkotása áll a középpontban, hanem az egymással kapcsolatban álló programegységek (objektumok) hierarchiájának megtervezése. Ez az objektumorientált programozás paradigmája. A
Python
nyelv alapvetően objektumorientált, a benne szereplő változók és függvények igazából mind objektumok. A korábbi mintanotebookokban $-$ ugyan burkoltan $-$ de már találkoztunk az objektumorientáltság jeleivel. Például voltak olyan függvények, amiket egy változó mögé "."-al írva használtunk:
x=array([1,2,3])
x.sum()
Az objektumorientált programozás elsőre kicsit bonyolultnak tűnhet, így ha az alábbi mintapéldákból nem lenne minden fogalom világos, akkor az következő magyar illetve angol nyelvű oldalakon található részletesebb leírások remélhetőleg tisztázzák a felmerülő kérdéseket:
Magyar:
ELTE-python dokumentáció
Ugorj fejest a pythonba
Angol:
Python3 Object-Oriented Programming
Az alábbiakban tehát megismerkedünk a python
nyelv objektumorientált szintaxisával.
class
konstrukció és példányosítás¶Az objektumorientált programozásban az objektumok olyan entitások, melyek egyszerre hordoznak adatokat és azokon az adatokon elvégezhető operációkat. Egy objektumot létrehozni egy terv alapján lehet. Az objektumok megtervezésére a Python
$-$ több más nyelvekhez hasonlóan $-$ a class
konstrukciót kínálja. A class
szó magyarul osztályt jelent. Tehát ezentúl, ha azt mondjuk hogy egy osztályt definiálunk, akkor arra gondolunk, hogy készítünk egy tervet. Az osztálydefiníció alapján elkészült dolgokat, azaz objektumokat példányoknak $-$ vagy angol kifejezéssel instance-nak $-$ szokás hívni. Az alábbi ábrák remélhetőleg a vizuálisan gondolkozók számára is jól illusztrálják a fent bevezetett fogalmakat:
**Az osztályok olyanok, mint a tervrajzok:** | **Az objektumok pedig a tervek alapján elkészült példányok:** |
Lássunk egy konkrét egyszerű példát a class
konstrukcióra! Definiáljunk egy osztályt!
class Robot:
'''
Ez egy robot osztály!
Ez az osztály egyelőre nem csinál semmit :(
'''
pass
A fenti definícióval létrehoztunk egy Robot
nevű osztályt! Egyelőre így magában ez az osztály nem sok mindent tud. A definícióban pusztán egy dokumentációs karakterlánc van, illetve egy pass
kifejezés. A pass
utasítás nem csinál semmit. Akkor használható, ha szintaktikailag szükség van egy utasításra, de a programban nem kell semmit sem csinálni.
Hozzunk most létre a fenti osztálydefiníció alapján egy objektumot!
x = Robot()
Tehát most az x
változónk egy a Robot
osztályba tartozó objektum! Egy objektumról a type
függvény segítségével tudjuk eldönteni, hogy milyen osztályba tartozik:
type(x)
A type függvény tehát közli, hogy az x
változó egy olyan objektum, ami a __main__.Robot
osztály alapján példányosodott. Itt a __main__
pusztán arra utal, hogy a Robot
osztály ebben a futási környezetben lett definiálva, és nem egy modulból töltöttük be.
Vizsgáljuk meg néhány más egyszerű objektum típusát:
type(1)
type(3.0)
type([1,2,3])
type(abs)
Ahogy vártuk, a tizedespont nélül írt szám int
, a tizedesponttal írt szám float
, a lista list
, valamint az abszolút értéket meghatározó abs
függvény builtin_function_or_method
típusú.
Ha egy osztályt egy modulban definiáltunk, akkor a típus hordozni fogja a definiált modul nevét:
import numpy # betöltjük a numpy modult
type(numpy.sin) # a numpy-ban definiált sin függvény típusa
Ahhoz, hogy valami hasznosat tudjunk csinálni az objektumainkkal, a fenti definíciót egészítsük ki egy kicsit!
class Robot:
'''
Ez egy robot osztály!
A robot születésekor már adhatunk nevet!
A robotot később átnevezhetjük!
Minden robot neve a saját objektumában a name változóban van.
Minden robotot gyártó cég neve a robot_company osztályváltozóban van.
'''
robot_company='ELTE robot company'
total_number_of_robots_created=0
def __init__(self,name=None):
Robot.total_number_of_robots_created+=1
print('Most születik egy robot!')
self.name=name
if name:
print('Ezt a robotot úgy hívják, hogy '+str(name)+' !')
else:
print('Ennek a robotnak még nincs neve... :(')
def set_name(self,name):
'''
Ezzel a függvénnyel meg tudjuk változtatni a robot nevét.
'''
print('Megváltozott a nevem '+str(name)+'-re!')
self.name=name
Gyártsunk le ebből az osztályból egy példányt!
x=Robot()
A fenti osztály már több mindent tud, három fontos dolgot illusztrál.
A robot_company
és a total_number_of_robots_created
változók az osztály minden példányában elérhető úgynevezett osztályváltozók. Hivatkozhatunk rájuk az osztály nevén keresztül, vagy az osztály egyik példányán keresztül is a ".
" operátorral:
x.robot_company
Robot.robot_company
Robot.total_number_of_robots_created
x.total_number_of_robots_created
Ha kell, akkor természetesen meg is változtathatjuk őket:
Robot.robot_company='KÖZGÁZGÉP'
x.robot_company
Robot.robot_company
self
??¶Az Robot
osztály definiálása során az osztályokhoz függvényeket is rendeltünk a már megszokott def
szerkezettel. Az osztályokra ható függvényeket az irodalomban szokás metódusoknak, vagy angolul method-nak hívni. Vizsgáljuk meg, mit csinál a Robot
osztályba definiált két függvény!
Először nézzük meg a set_name
függvényt. Ennek formálisan két bemeneti változója van, a self
és a name
.
A name
bemeneti változó ebben a konstrukcióban pontosan úgy viselkedik, mint ahogy ezt más függvényeknél is már megszoktuk.
Ezzel ellentétben a self
szócska speciális. A self
változó helyére a függvény hívásakor az objektum maga adódik át!
Az objektumra vonatkozó metódusokat szintén a "." operátorral érhetjük el a következőképpen:
x.set_name('Gizi')
Azaz nem kell kiírni a self
helyére az objektum nevét (azt számunkra megteszi a "." konstrukció), és elég egyetlen bemenő változó a függvénynek!
Az __init__
függvény egy speciális függvény. Segítségével az objektum létrehozásakor történő dolgokat tudjuk befolyásolni. A fenti példában alapvetően két dolog történik egy Robot
objektum létrehozásakor.
Előszöris eggyel növekszik a total_number_of_robots_created
osztály változó. Ezentúl, ha az objektum létrehozásánál megadunk egy karakterláncot, akkor azt az objektumra jellemző name
változóban eltárolja. Ráadásul még kiír pár dolgot annak megfelelően, hogy adtunk-e nevet vagy sem.
A fent definiált metódusok a name
objektumváltozót manipulálják. Általában az osztályváltozókon felül az objektumoknak vannak saját objektumváltozói is, amelyeket csak saját maguk birtokolnak, az osztály többi elemei nem. Az objektumváltozókra szintén a "."-al tudunk legegyszerűbb esetben hivatkozni. Az objektumváltozókat szokás az objektum attribútumainak nevezni.
x.name
Az attribútumok természetszerűen csak az adott objektumra vonatkoznak. Ha egy objektumnak még nincs definiálva az osztálydefinícióban valamilyen attribútuma, akkor azt később is definiálhatjuk!
x.build_year=2017
Természetesen az osztály más objektumai ezek után nem fognak rendelkezni ezzel az attribútummal:
y=Robot('Malvin')
y.build_year
Egy osztályról vagy egy objektumról a dir()
függvény segítségével megtudhatjuk, hogy milyen attribútumai, illetve metódusai vannak:
dir(Robot)
dir(x)
dir(y)
Ahogy azt várjuk, az y
változóban tárolt robotnak nincs build_year
attribútuma, a Robot
osztályból is hiányzik ez az attribútum definíció.
Az általunk definiált metódusokon és attribútumokon kívül mind a Robot
osztály, mind pedig az x
és y
objektumok rendelkeznek egy halom __
-al közrefogott metódussal, lássuk, mik is ezek.
A fenti Robot
osztályban láttuk, hogy az objektumok létrehozásakor történő dolgokat az __init__
speciális metódussal tudjuk befolyásolni. Azt is láttuk a fenti dir()
függvény hívásoknál, hogy mind a Robot
osztálynak, mind pedig a létrehozott példányoknak van több más __
-al közrefogott metódusa. Az ilyen függvények a speciális metódusok. A speciális metódusok egyetlen „specialitása”, hogy ezeket nem közvetlenül hívjuk, hanem a Python
akkor hívja őket, amikor egy bizonyos szintaxisú utasítást hajtasz végre az osztályon vagy az osztály egy példányán.
Lássunk speciális metódusokra egy kicsit komplikáltabb példát:
class Fifi:
'''
Ez a Fifi osztály, melynek elemeit össze tudjuk
hasonlítani aszerint, hogy milyen hosszú a nevük.
'''
def __init__(self,name=None):
'''
Inicializáláskor adunk egy nevet
'''
self.name=name
def __lt__(self, other):
'''
Ez a függvény lehetővé teszi a < jel használatát.
'''
return len(self.name)<len(other.name)
def __eq__(self, other):
'''
Ez a függvény lehetővé teszi a == jel használatát.
'''
return len(self.name)==len(other.name)
def __gt__(self, other):
'''
Ez a függvény lehetővé teszi a > jel használatát.
'''
return len(self.name)>len(other.name)
A fent definiált Fifi
osztály elemeit össze tudjuk hasonlítani a valós számoknál megszokott <, == és > jelek segítségével:
f=Fifi('bug')
g=Fifi('bugger')
f>g
A fenti példa tehát azt illusztrálja, hogy vannak bizonyos speciális függvények, amelyek megmondják, hogy bizonyos megszokott operátorok (például a relációs jelek) hogyan hassanak az általunk létrehozott osztály elemeire. Ezen kívül még sok mindenre lehet használni speciális metódusokat, egy osztály objektumait használhatjuk például:
A speciális metódusokról itt egy jó összefoglaló található, a rájuk vonatkozó hivatalos Python-dokumentáció pedig itt lelhető meg. Magyarul pedig itt egy tömörebb referencia anyag.
Az osztályok tervezése során sokszor előfordul, hogy bizonyos változók csak az osztály belső működésével kapcsolatosak, és nem szükséges, hogy kívülről hozzáférhetőek legyenek. A Python
az osztályattribútumok hozzáférhetősége szempontjából három kategóriát különböztet meg:
__
két aláhúzással kezdődik, akkor az osztály példányainak ezen változói csak az osztály belső függvényei számára érhetőek el. _
aláhúzással kezdődik, akkor védett változónak minősül. Lássunk erre a három fajta attribútumra egy-egy példát:
class Bigyula():
'''Ez egy bigyula osztály '''
def __init__(self):
self.__priv = "Én rettentő titkos vagyok."
self._prot = "Én védett vagyok.. de csak ha vigyázol rám."
self.pub = "Velem azt teszel, amit akarsz."
bigyo=Bigyula()
A publikus és védett attribútumok kívülről hozzáférhetőek:
bigyo.pub
bigyo._prot
bigyo._prot='ÁÁÁ'
bigyo._prot
A privát attribútumok már nem:
bigyo.__priv
Természetesen nem csak az attribútumok lehetnek védettek vagy privátok. Az osztályok metódusai is hasonlóan kategorizáltak, rájuk is érvényes az aláhúzásokkal kezdődő nevezési konvenció.
Az objektumorientált programozás egyik fő absztrakciós előnye az öröklődés. Ennek az a lényege, hogy egy osztályt származtathatunk már korábban megírt osztályokból, akár egyszerre többől is! Ennek az értelmét a fenti kép illusztrálja. Az állatvilágban minden élőlény alapvetően rendelkezik bizonyos közös tulajdonságokkal, a fenti példán ezt az agy jelenléte jelzi (brain=true). Ebből az ősosztályból származik minden állat, közöttük az ember is. Minden alosztálynak meglehetnek a maga sajátosságai, illetve minden alosztály további alosztályokra bomolhat.
Lássunk az öröklődésre is egy egyszerű példát! Terjesszük ki a Robot
osztályt egy akku
attribútummal és néhány függvénnyel, ami ennek az atribútumnak a kezelését szolgálja.
class AkksisRobot(Robot): # <-- Így mondjuk meg, hogy egy olyan osztály, amit a Robot osztályból származtatunk!
'''
Ez egy akksis robot.
'''
def akkumulator_allasa(self):
'''
Ez a függvény megnézi, hogy van-e akku.
'''
if self.akku:
print('Az akksi '+str(self.akku)+'-t mutat')
else:
print('Nincs akksim.. :( ')
def set_akksi(self,akku=10):
'''
Ez a függvény tesz valamit az akksiba.
'''
self.akku=akku
Tehát az osztálydefinició első sorában zárójelben jelezhetjük, hogy melyik osztályból szeretnénk származtatni az alosztályt. Lássuk, hogy viselkedik egy példány ebből az új robotból!
zz=AkksisRobot('Boborján')
Figyeljük meg, hogy nem definiáltunk __init__
függvényt az alosztályba, és ezért az ősosztály __init__
függvénye hívódik meg az objektum létrehozásakor!
A most létrehozott zz
objektum természetesen rendelkezik a set_aksi()
és akkumulator_allasa()
metódusokal.
zz.set_akksi(42)
zz.akkumulator_allasa()
A korábban definiált, x
objektumban tárolt robot viszont nem rendelkezik ezekkel metódusokkal:
x.akkumulator_allasa()