Az objektumok a logikailag összetartozó adatokat (amelyek az objektumok állapotát jellemzik), valamint az ezeken műveletet végző hívható objektumokat, azaz metódusokat összerendelik. Ezt hívják egységbe zárásnak (encapsulation).
Az azonos típusú objektumoknál a metódusok azonosak, ezért megkülönböztetni egymástól az egyes példányokat az adatattribútumok eltérő értékei alapján lehet. Például a Háromszög típusú objektumok mindegyikére lehet definiálni egy azonos képletet tartalmazó kerület és terület számító metódust, valamint az oldalak hosszát képviselő adatattribútumokat. Az egyes, megkülönböztethető háromszögobjektumok az oldalhosszakban fognak eltérni.
Ha tehát a metódusok azonosak, akkor – ellentétben az egyediséget meghatározó adatattribútumokkal – felesleges memóriafelhasználás lenne, ha minden egyes objektumhoz külön létrejönnének, és a memóriában permanensen tárolódnának a metódusobjektumok. Ezért ezeket elég egyszer létrehozni, és az egyes objektumpéldányok ezeket a közös hívható objektumokat fogják használni.
Az osztályok definiálásával épp e közös használhatóságot valósítjuk meg. Az osztály törzsében azokat a függvényeket és adatattribútumokat definiáljuk, amelyeket majd a példányok mindegyike használni fog. Ahhoz azonban, hogy ezek a függvények már használatra készen rendelkezésre álljanak a példányok számára, szükséges, hogy az osztálydefiníció törzsében lefusson a kód, így létrehozva a függvényobjektumokat, amelyek referenciái (nevei) az osztály névterébe bekerülnek.
A példány metódushívásakor az történik, hogy az interpreter rákeres a metódusnévre a példány osztályában. Ha ott megtalálja mint érvényes függvényattribútum, akkor egy hívható objektumot hoz létre, amelynek átadja mind a példányobjektumot, mind az imént megtalált függvényobjektumot. Ezzel hozza létre a metódusobjektumot, amelyet aztán meg is hív olyan módon, hogy a megtalált függvény első argumentumának a példányt adja, utána pedig a híváskor meghatározott többi argumentumot.
Lényeges, hogy szemben az osztályban definiált függvényekkel, a metódusobjektum csak átmenetileg, a hívás idejére jön létre, így a memórafelhasználás csak átmeneti. /Természetesen, ha valamilyen módon változóhoz rendeljük, akkor mindaddig létezni fog, amíg van rá hivatkozás./
Tehát, bár gyakran mondjuk azt, hogy metódusokat definiálunk az osztályban, valójában függvényeket definiálunk, amelyeket majd a példányok hívásakor létrejövő metódusobjektumok használnak.
Az eddig leírtakat az alábbi példákkal szemléltetjük.
Elsőnek azt nézzük meg, hogy az osztály törzsében levő utasítások még a példányok létrehozása előtt végrehajtódnak. Ehhez egy Rectangle1 nevű osztályt definiálunk, amelyben a két oldal megadásán kívül lehetőség van egy scale nevű metódus meghívására, amely egy olyan téglalap példánnyal tér vissza, amelynek oldalai az argumentumban megadott szorosa az eredeti téglalap oldalainak. Az osztálydefiníció utolsó utasítása egy kiírás. A tesztkódok egyszerűek: először létrehozunk egy példányt, majd meghívjuk rá a scale metódust, és kiírjuk az eredményt. A programot lefuttatva a várt új téglalapot kapjuk. De előtte még láthatjuk az osztálydefiníció végén szereplő kiírás szövegét. Ez azt jelzi, hogy az osztálydefinícióban szereplő utasítások még a példányosítás előtt végrehajtásra kerülnek.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class Rectangle1: def __init__(self, a, b): self.a, self.b = a, b def __repr__(self): return f'{type(self).__name__}{self.a, self.b}' def scale(self, r): return type(self)(self.a * r, self.b * r) print(f'Rectangle1 osztálydefiníció vége.') # Teszt r1 = Rectangle1(2, 3) print(r1.scale(2)) # Eredmény: # Rectangle1 osztálydefiníció vége. # Rectangle1(4, 6) |
A következő kódsorokban egy hasonló, téglalap példányokat szolgáltató Rectangle2 osztályt definiáltunk. Itt most a példány metódushívását kívánjuk szimulálni egy saját készítésű Method osztály igénybevételével. Ezt úgy érjük el, hogy a Rectangle2 osztály __init__ függvényén belül a példányosításkor létrejövő, self általhivatkozott objektumhoz egy scale nevű attribútumot hozunk létre, és ehhez rendeljük a Method osztály olyan példányát, amelynek a létrejött téglalap példányt és a Rectangle2 osztály scale nevű függvényobjektumát adjuk át. Ahhoz, hogy lássuk, amikor a Method példány meghívásra kerül, annak __call__ függvényében egy szöveget írunk ki.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
class Method: def __init__(self, instance, function): self.instance = instance self.function = function def __call__(self, *args, **kwargs): print('Method meghívása.') return self.function(self.instance, *args, **kwargs) class Rectangle2: def __init__(self, a, b): self.a, self.b = a, b # A scale attribútumnévhez a megfelelő Method példányt rendeljük. self.scale = Method(self, Rectangle2.scale) def __repr__(self): return f'{type(self).__name__}{self.a, self.b}' def scale(self, r): return type(self)(self.a * r, self.b * r) print('Rectangle2 osztálydefiníció vége.') # Teszt r2 = Rectangle2(2, 3) print(r2.scale(2)) # Eredmény: # Rectangle2 osztálydefiníció vége. # Method meghívása. # Rectangle2(4, 6) |
A tesztsorok és azok futási eredményei hasonlóak, mint előzőleg, azzal az eltéréssel, hogy megjelenik az a szöveg is, amely a Method objektum hívásakor íródik ki. Ez tehát azt mutatja, hogy jelen esetben nem a normál metódushívás történt a példányon, ahogy az a Rectangle1 esetében, hanem az általunk meghatározott Method objektum lett meghívva.
A closure függvényekre is tekinthetünk úgy, hogy az objektumpéldányokhoz hasonlóan magukhoz csatolják az állapotadatokat (a körülzáró függvény változóit), így akár ezeket is lehetne „példányként” használni. De, ahogy az a több closure függvény azonos körülzárt lokális változóhoz való hozzáféréséről szóló bejegyzés végén említésre került, ez csak kevés példány esetén lehet megoldás, mert minden egyes closure létrehozásakor az adatattribútumok mellett új hívható objektum is létrejön, ami memóriafelhasználás szempontjából nem kívánatos.
Jelen bejegyzésben a cél az volt, hogy rávilágítsunk az osztálydefinícióban szereplő függvények és a példányokon meghívott metódusok alapvető viszonyára egyszerű példákkal illusztrálva. A valóságban a metódusok létrejöttének mechanizmusa ennél összetettebb. A részletekről a Python tudásépítés lépésről lépésre című e-könyv „Függvény és objektum szimbiózisa – metódusok” fejezetben, valamint a „Attribútumnév és attribútumérték egymásra találása” szakaszban találhatunk további információkat.