Osztályok és objektumok

Funkcionális vs. objektumorientált programozás

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.

A 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!

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

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

In [3]:
type(x)
Out[3]:
__main__.Robot

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:

In [4]:
type(1)
Out[4]:
int
In [5]:
type(3.0)
Out[5]:
float
In [6]:
type([1,2,3])
Out[6]:
list
In [7]:
type(abs)
Out[7]:
builtin_function_or_method

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:

In [8]:
import numpy # betöltjük a numpy modult
In [9]:
type(numpy.sin) # a numpy-ban definiált sin függvény típusa
Out[9]:
numpy.ufunc

Osztályváltozók, attribútumok és metódusok

Ahhoz, hogy valami hasznosat tudjunk csinálni az objektumainkkal, a fenti definíciót egészítsük ki egy kicsit!

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

In [11]:
x=Robot()
Most születik egy robot!
Ennek a robotnak még nincs neve... :(

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:

In [12]:
x.robot_company
Out[12]:
'ELTE robot company'
In [13]:
Robot.robot_company
Out[13]:
'ELTE robot company'
In [14]:
Robot.total_number_of_robots_created
Out[14]:
1
In [15]:
x.total_number_of_robots_created
Out[15]:
1

Ha kell, akkor természetesen meg is változtathatjuk őket:

In [16]:
Robot.robot_company='KÖZGÁZGÉP'
In [17]:
x.robot_company
Out[17]:
'KÖZGÁZGÉP'
In [18]:
Robot.robot_company
Out[18]:
'KÖZGÁZGÉP'

Na de mi az a 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:

In [19]:
x.set_name('Gizi')
Megváltozott a nevem Gizi-re!

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.

In [20]:
x.name
Out[20]:
'Gizi'

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!

In [21]:
x.build_year=2017

Természetesen az osztály más objektumai ezek után nem fognak rendelkezni ezzel az attribútummal:

In [22]:
y=Robot('Malvin')
Most születik egy robot!
Ezt a robotot úgy hívják, hogy Malvin !
In [23]:
y.build_year
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-23-01509fea4f07> in <module>()
----> 1 y.build_year

AttributeError: 'Robot' object has no attribute '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:

In [24]:
dir(Robot)
Out[24]:
['__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__',
 'robot_company',
 'set_name',
 'total_number_of_robots_created']
In [25]:
dir(x)
Out[25]:
['__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__',
 'build_year',
 'name',
 'robot_company',
 'set_name',
 'total_number_of_robots_created']
In [26]:
dir(y)
Out[26]:
['__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__',
 'name',
 'robot_company',
 'set_name',
 'total_number_of_robots_created']

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.

Speciális method-ok

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:

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

In [28]:
f=Fifi('bug') 
In [29]:
g=Fifi('bugger')
In [30]:
f>g
Out[30]:
False

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.

Privát és publikus

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:

  • privát attribútumok szigorúan csak az objektum belső működése során használatosak. Ha egy attribútum neve __ 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.
  • Védett attribútumok olyan változók, amik ugyan külső felhasználók számára elérhetőek, de nem illik őket használni, csak az osztály alosztályainak definiálása során (az alosztályokat a következő bekezdésben tárgyaljuk). Ha egy változó neve egyetlen _ aláhúzással kezdődik, akkor védett változónak minősül.
  • Publikus attribútumoknak a mindenki számára elérhető változókat nevezik.

Lássunk erre a három fajta attribútumra egy-egy példát:

In [32]:
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."            
    
In [33]:
bigyo=Bigyula()

A publikus és védett attribútumok kívülről hozzáférhetőek:

In [34]:
bigyo.pub
Out[34]:
'Velem azt teszel, amit akarsz.'
In [34]:
bigyo._prot
Out[34]:
'Én védett vagyok.. de csak ha vigyázol rám.'
In [35]:
bigyo._prot='ÁÁÁ'
In [36]:
bigyo._prot
Out[36]:
'ÁÁÁ'

A privát attribútumok már nem:

In [37]:
bigyo.__priv
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-37-59dbf81b5a2e> in <module>()
----> 1 bigyo.__priv

AttributeError: 'Bigyula' object has no attribute '__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ó.

Öröklés

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.

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

In [39]:
zz=AkksisRobot('Boborján')
Most születik egy robot!
Ezt a robotot úgy hívják, hogy 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.

In [40]:
zz.set_akksi(42)
In [41]:
zz.akkumulator_allasa()
Az akksi 42-t mutat

A korábban definiált, x objektumban tárolt robot viszont nem rendelkezik ezekkel metódusokkal:

In [42]:
x.akkumulator_allasa()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-42-8f1992834119> in <module>()
----> 1 x.akkumulator_allasa()

AttributeError: 'Robot' object has no attribute 'akkumulator_allasa'