Egy korábbi, „Tudáselmélyítés és gyakorlás kész függvények vagy metódusok leutánzásával” című bejegyzésben arról volt szó, hogy a nyelv használatát és a nyelvi elemek megértését nagyban segítheti, ha megpróbálunk már készen rendelkezésre álló függvényeket, metódusokat vagy osztályokat emulálni, vagyis a funkcionalitásukat saját készítésű kóddal utánozni. Ilyenkor ahhoz, hogy ez sikerüljön, alaposan meg kell értenünk az utánozni kívánt nyelvi elemet.
Ebben a bejegyzésben a beépített függvények között található super leutánzásával foglalkozunk. Ez azért jó jelölt, mert néhány dolgot eleve tisztázni kell vele kapcsolatban mielőtt nekilátnánk az emulálásához.
Először is azt, hogy bár a beépített függvények között van felsorolva, de valójában a super egy osztály és nem függvény. Meghívásakor tehát egy osztálypéldány áll elő. Ennek megfelelően a kitűzött feladatunk egy osztálydefiníció előállítása lesz, amelynek neve legyen Super.
A következő annak megértése, hogy mit is csinál a super. A szerepe általában ismert, vagyis, hogy arra szolgál, hogy egy adott osztály feletti öröklési hierarchiában található metódust meg tudjunk hívni. Ezt tipikusan egy osztálydefinícióban szereplő metódus törzsében kell megtenni (általában annak első utasítássoraként). Ilyenkor sokszor elegendő, ha a super-t argumentumok nélkül hívjuk meg, és utána tesszük a meghívandó metódushivatkozást /pl., super().m1()/. Ebben az esetben a kívánt metódus vagy a közvetlen szülőé, vagy ha annak nincs ilyen, akkor valamelyik ősé.
Mivel az előbb említett egy gyakori használati eset, ezért talán kevésbé ismert, hogy a super valójában kétparaméteres. A híváskor tehát általános esetben két argumentumot kell megadni, amelyek együtt határozzák meg, hogy az öröklési hierarchia (pontosabban a metódusfeloldási sorrend, MRO) mely részében és honnantól keresse a kívánt metódust. Az első argumentum a keresés kezdetét határozza meg. A keresés az itt megadott típus után közvetlenül következő típustól kezdődik. A második argumentum azt határozza meg, hogy az a keresési öröklési lánc, amelyben keresünk, honnan kezdődjön. Ezen argumentumként nem csak típust lehet megadni, hanem egy objektumot is. Ebben az esetben a megadott objektum típusa lesz a keresési lánc kezdőtípusa.
Könnyen belátható, hogy ahhoz, hogy a keresés megfelelően végbe tudjon menni a két argumentum nem lehet független egymástól, mert nyilván az első argumentum által meghatározott kezdeti keresési típus bele kell, hogy essen a második argumentum által meghatározott keresési típusláncba. Ez úgy fogalmazható meg, hogy a második argumentum altípusa kell, hogy legyen az első argumentumnak. (Vagy ha objektumot adtunk meg másodiknak, akkor annak az első argumentum példányának kell lennie.)
Láthajuk, hogy a super két argumentuma két előre ismert típus, vagy típus és egy példány. Ebből következik az a szintén talán kevésbé ismert tény, hogy a super nem csak metódustörzsben hívható meg, hanem osztálydefiníción kívül, önállóan is.
Felmerülhet a kérdés, hogy ha a super használatához két argumentum kell, akkor, hogy lehetséges argumentum nélkül meghívni. Nos, argumentum nélkül meghívni csak példány- vagy osztálymetódusban lehet, mégpedig azért, mert ilyen esetben az interpreter adja át automatikusan a szükséges két argumentumot: a metódus első paramétere lesz a super második argumentuma, és a super első argumentuma az az osztály, amelyben a metódus definiálva van.
Amit még érdemes tudni, hogy a super rendelkezik három speciális adatattribútummal, amelyek a __thisclass__, __self__ és __self_class__. A super() első paraméterének átadott osztályt tartalmazza a __thisclass__. A második argumentumot a __self__ és annak típusát a __self_class__ tárolja.
Mindezen ismeretek birtokában már el tudjuk készíteni a super szerepét utánzó Super osztályt. Ennek definícióját láthatjuk alább. A részletes kommentek segítik a megértést.
|
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 36 37 38 |
from functools import partial class Super: def __init__(self, mro_start_type, obj_or_type): # Osztályattribútumként eltároljuk az argumentumokat. Azért osztályattribútumként, mert # a __getattribute__ implementációjában felhasználjuk őket. Ha viszont példányattribútumok # lennének, akkor a __getattribute__ végtelen hívásciklust eredményezne. Super.__thisclass__ = mro_start_type Super.__self__ = obj_or_type Super.__self_class__ = obj_or_type if type(obj_or_type) is type else type(obj_or_type) # Ellenőrizzük, hogy a konstruktorban megadott argumentumok közötti előfeltétel teljesül el. if not issubclass(Super.__self_class__, Super.__thisclass__): raise TypeError('Super(mro_start_type, obj_or_type): az obj_or_type példánya vagy ' 'altípusa kell, hogy legyen első argumentumnak.') def __getattribute__(self, attn): # Előállítjuk a metódusfeloldási sorrendet (MRO) meghatározó listát attól függően, hogy a # konstruktor második argumentuma típus vagy objektum. if type(Super.__self__) is type: mro_chain = Super.__self__.mro() else: mro_chain = Super.__self_class__.mro() # Az MRO sorozatban a keresés kezdetét jelentő index meghatározása. mro_startindex = mro_chain.index(Super.__thisclass__) # Megkeressük az MRO láncban a kérdéses metódust, és ha van találat, akkor visszaadjuk a # függvény- vagy metódusobjektumot attól függően, hogy a konstruktor második argumentuma típus vagy példány. for cls in mro_chain[mro_startindex + 1:]: if attr := getattr(cls, attn, None): return attr if type(Super.__self__) is type else partial(attr, Super.__self__) return object.__getattribute__(self, attn) |
A működést a következő tesztsorokkal ellenőrizzük:
|
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 36 37 38 39 40 41 42 43 44 45 46 |
class A: def m1(self, x): print('in A', x) class B(A): def m1(self, x): print('in B', x) Super(B, self).m1(2 * x) class C(B): def m1(self, x): print('in C', x) Super(B, C).m1(self, 3 * x) class D(C): def m1(self, x): print('in D', x) Super(C, self).m1(4 * x) # TESZT d = D() d.m1(10) # Eredmény: # in D 10 # in B 40 # in A 80 c = C() c.m1(10) # Eredmény: # in C 10 # in A 30 b = B() b.m1(10) # Eredmény: # in B 10 # in A 20 Super(C, d).m1(10) # Eredmény: # in B 10 # in A 20 |
A kiírt eredmények helyességét ellenőrizhetjük, ha itt a Super helyett super-t írunk.
Megjegyezzük, hogy a megalkotott Super osztály korlátja a beépített super-hez képest, hogy metódusban használva is mindig meg kell adni a két argumentumot, vagyis a fentebb említett automatikus argumentumátadás ennél nem működik.
E bejegyzés témájához a Python tudásépítés lépésről lépésre című e-könyv következő részei kapcsolódnak különösen: az „Öröklődés” fejezet „Származáskutatás és bizonyítás” és „Minden szülő szuper! – az ősök segítségül hívása” alfejezetei, valamint az „Attribútumműveletek befolyásolása” című fejezet.