Hogyan írjunk felül egy alaposztálybeli tulajdonságot (property) egy alosztályban?

A tulajdonság (property) egy olyan osztályattribútum, amelynek értéke egy speciális, property típusú, ú.n. leíró (descriptor) objektum. Ennek működése a normál attribútumokhoz képest különleges, mert attól függően, hogy ezen osztályattribútumra vonatkozóan értékkikérési vagy értékadási műveletet kezdeményezünk, az értékkinyerést végző (getter) vagy az értékadást megvalósító (setter) metódusok fognak lefutni. Ezeket a tulajdonság létrehozásához biztosítani kell.

A fentiek okán egy tulajdonság felülírása egy alosztályban nem olyan magától értetődő, mint egyéb attribútumok esetén. Ennek megvalósítását nézzük meg a következő illusztratív példán.

Az ábrán látható Ancestor nevű osztály példányosításkor egy x értéket fogad, amelyet egy privát adatattribútumban eltárolunk. Ennek felhasználásával, a szokásos módon, dekorátoros szintaxissal egy tulajdonságot definiálunk úgy, hogy annak értéke egyaránt írható és kiolvasható legyen.

Az Ancestor osztály számos más metódussal rendelkezik még, amelyek a témánk szempontjából nem relevánsak, ezért nem tüntettük fel azokat. Annyit kell csak tudni, hogy ezek némelyike igényli az x aktuális értékét is felhasználó _computationally_expensive() metódus eredményét. E metódus neve is utal rá, hogy ennek végrehajtása számításigényes, futási időben költséges műveletet (az egyszerűség kedvéért a példában nem). Ezért az inicializálásakor az __init__ metódusban, amikor az x értékét már tudjuk, meghívjuk ezt a _computationally_expensive() metódust és visszatérési értékét eltároljuk egy _z privát attribútumban. Ezek után, mindaddig, amíg az x értéke nem változik a többi metódus ezt az értéket tudja használni, ahelyett, hogy újra és újra meghívná a költséges metódust. Ha azonban az x változik, akkor gondoskodni kell arról, hogy _z értéke is aktualizálódjon. Ezt úgy tesszük meg, hogy az x setter metódusában hívjuk meg _computationally_expensive() metódust és eredményét hozzárendeljük a _z attribútumhoz. Az így kialakított Ancestor osztály definícióját láthatjuk alább:

A következő lépésben létrehozzuk az Ancestor osztály egy alosztályát Descendant néven. Tehát a Descendant örökli az Ancestor osztályt, annak minden attribútumát, beleértve az x tulajdonságot is. Továbbá, a Descendant is igényli, egy y nevű tulajdonságához, a _computationally_expensive() metódus eredményét. Ezért az ezt tároló, az Ancestor osztályból rendelkezésre álló _z attribútum értékét az inicializáláskor fel is használja az y tulajdonság kezdő értékének meghatározásához.

A lényegi munka most jön igazán. Ugyanis az x az Descendant példányain keresztül is változtatható. Ha értéket adunk egy Descendant példányon keresztül az x-nek, akkor az Ancestor-ben a _z érték is változik. A gond, hogy ez a változás nem jelenik meg az y tulajdonságban, holott mint előbb szó volt róla, az y értéke is _z értékétől kell, hogy függjön. Ahhoz, hogy ez működjön, az x tulajdonságot felül kell írni az Descendant osztályban.

Mint az elején említettük, az x egy különleges működési mechanizmussal rendelkező osztályattribútum, amelyhez még definiálni kell egy getter és egy setter metódust, ha mind olvasni, mind írni szeretnénk. Ezért az Descendant osztályban felülírjuk az x osztályattribútumot úgy, hogy mellette definiáljuk a megfelelő getter és setter metódusokat. Ez utóbbit úgy, hogy benne az _z értékének felhasználásával meghatározzuk az y attribútum értékét. Ezzel tehát azt érjük majd el, hogy ha az Descendant példányon értéket adunk x-nek, akkor mind a _z mind az y értéke módosulni fog.

Az x tulajdonság felülírására három változatot mutatunk.

Az 1. változatban az x tulajdonságot mint osztályattribútumot hozzuk létre, amelyhez egy property példányt rendelünk. Ennek konstruktorában megadjuk a setter és getter függvényeket. Ezekben név szerint hivatkozunk a szülőosztályra és ezen keresztül annak x osztályváltozójára, amelynek értéke egy property példány. E property objektum fget attribútuma által hivatkozott metódust hívjuk meg az Descendant getter metódusában, illetve e property objektum fset attribútuma által hivatkozott metódust hívjuk meg az Descendant setter metódusában ahhoz, hogy az x, valamint a _z értéke megváltozzon. Ezt követően pedig az Descendant setter metódusában az y tulajdonság mögöttes privát attribútumának adunk értéket a _z – most már aktualizált – értékét felhasználva. Ezt a megoldást mutatja a Descendant alábbi definíciója.

Ennek a megoldásnak hátránya, hogy az Ancestor osztályra annak konkrét nevével hivatkozunk. Ez rugalmatlan kódot eredményez, mert ha az Ancestor neve megváltozik, akkor az Descendant-ban is változtatni kell.

Ezen segít a 2. változat, amelyben nem az Ancestor neve alapján hivatkozunk annak x tulajdonságára, hanem a super() objektum megfelelő meghívásával. Egyéb tekintetben a megvalósítás az 1. változatra hasonlít.

A 3. változat annyiban tér el a 2. változattól, hogy itt az Descendant-ban definiált x tulajdonságot a dekorátoros szintaxissal valósítjuk meg.

Ha a három változat bármelyikével futtatjuk a következő tesztsorokat ugyanazokat az eredményeket kapjuk.

E bejegyzéshez kapcsolódó ismeretek a Python tudásépítés lépésről lépésre című e-könyvben különösen a következő fejezetekben találhatók: „Képességfejlesztés-függvénydekorátorok”, „Osztály vigyázz!-típuslétrehozás osztályokkal”, „Öröklődés”, „Attribútumműveletek kontrollált végrehajtása”, valamint „Attribútumleírók és használatuk”.

Érdekel a Python tudásépítés lépésről lépésre az alapoktól az első asztali alkalmazásig című e-könyv.