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:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class Ancestor: def __init__(self, x): self._x = x self._z = self._computationally_expensive() def _computationally_expensive(self): return self._x * 10 @property def x(self): return self._x @x.setter def x(self, v): self._x = v self._z = self._computationally_expensive() |
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.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class Descendant(Ancestor): def __init__(self, x): super().__init__(x) self._y = self._z / 2 @property def y(self): return self._y def get_x(self): return Ancestor.x.fget(self) def set_x(self, v): Ancestor.x.fset(self, v) self._y = self._z / 2 x = property(get_x, set_x) |
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.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class Descendant(Ancestor): def __init__(self, x): super().__init__(x) self._y = self._z / 2 @property def y(self): return self._y def get_x(self): return super(type(self), type(self)).x.fget(self) def set_x(self, v): super(type(self), type(self)).x.fset(self, v) self._y = self._z / 2 x = property(get_x, set_x) |
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.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class Descendant(Ancestor): def __init__(self, x): super().__init__(x) self._y = self._z / 2 @property def y(self): return self._y @property def x(self): return super(type(self), type(self)).x.fget(self) @x.setter def x(self, v): super(type(self), type(self)).x.fset(self, v) self._y = self._z / 2 |
Ha a három változat bármelyikével futtatjuk a következő tesztsorokat ugyanazokat az eredményeket kapjuk.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
# TESZT print('--- Ős példány létrehozása ---') a = Ancestor(123) print('a.x={}'.format(a.x)) a.x = 56 print('a.x={}'.format(a.x)) print('--- Utód példány létrehozása ---') b = Descendant(987) print('b.x={}, b.y={}'.format(b.x, b.y)) b.x = 222 print('b.x={}, b.y={}'.format(b.x, b.y)) # Eredmény: # --- Ős példány létrehozása --- # a.x=123 # a.x=56 # --- Utód példány létrehozása --- # b.x=987, b.y=4935.0 # b.x=222, b.y=1110.0 |
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”.