Tegyük fel, hogy egy családot akarunk modellezni, és ehhez létrehozzuk az apa, anya, gyerek és unoka objektumokhoz a Father, Mother, Child és GrandChild osztályokat. Ezek az osztályok öröklési kapcsolatban vannak: a Child osztálynak mind a Father, mind a Mother osztály szülőosztálya, a GrandChild osztály pedig a Child osztályból örököl.
A Father és Mother osztályok az adott feladathoz és modellhez illeszkedő, az apára és anyára jellemző metódusokat tartalmaznak. A példa kedvéért legyen a Father osztályban csak két metódus: a mow_lawn() és az open_the_safe(). Az előbbivel az apát fűnyírásra tudjuk kérni, amit az apa a maga módján, a saját stílusában fog végrehajtani. Az utóbbi metódus egy széf nyitását valósítja meg. Ugyanis az apa a saját megtakarításait egy széfben tárolja, amit csak adott számok adott sorrendben történő bevitelével lehet kinyitni.
A Mother osztályban szintén két metódust definiálunk. Az egyik a cook(), ami az anya kiváló főzési képességét valósítja meg. A másik a make_herbal_tea(), merthogy az anya képes egy különleges, gyógyító hatású teát készíteni.
A Child és GrandChild osztályok e metódusokat öröklik. A kérdés az, hogy ezek közül melyiket logikus, célszerű vagy szabad felülírniuk?
A fűnyírás mikéntjére vonatkozóan a gyerek és unoka dönthet úgy, hogy megfigyeli az apát és pontosan ugyanúgy fogja végezni a fű lenyírását mint az apa. Ez azonban nem gyakori eset, hanem inkább az jellemző, hogy a gyerekek és unokák a saját egyéni módjukon nyírják a füvet, ami nem zárja ki, hogy a szülő technikáit is felhasználják. Tehát a mow_lawn() metódus felülírása a Child és GrandChild osztályokban logikus lehet.
Még inkább ez a helyzet az cook() metódussal. Az utódok bár alkalmazhatják a szülők vagy nagyszülők főzési fortélyait, de jellemzően a saját stílusukban fognak főzni. Ezért tehát a cook() metódus felülírása a Child és GrandChild osztályokban szinte természetes.
De mi a helyzet a páncélszekrény kinyitásával? Mivel ez csak egy adott számkombináció megfelelő sorrendben történő megadásával tehető meg, ezért az utódoknak az open_the_safe() metódust nem szabad (de legalábbis nem célszerű) felülírni, mert akkor a széf nem nyitható. Ugyanez igaz a gyógytea készítésére. A kívánt hatás ugyanis csak akkor jelentkezik, ha adott gyógynövények meghatározott arányban vannak jelen a keverékben. Ha az összetevők aránya ettől eltér, vagy akár csak egy is hiányzik, akkor a gyógyhatás nem érvényesül. Tehát a make_herbal_tea() metódust nem szabad felülírni a Mother semelyik alosztályában, mert akkor a meghívás után nem a várt eredményt kapják az utódok.
Most, hogy a Father és Mother osztályokban azonosítottunk olyan metódusokat, amelyek alosztályokban történő felülírása nem kívánatos, a kérdés az, hogy hogyan jelezzük ezt az osztályok felhasználói számára.
Nos, erre szolgál a szabványos könyvtár typing moduljának final nevű függvénye. A nem felülírható metódust ezzel kell dekorálni. Ezt mutatjuk alább:
|
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 |
# Python 3.11+ from typing import final class Father: def mow_lawn(self): """Az apa a saját stílusában nyírja a füvet.""" pass @final def open_the_safe(self): """Az apa a saját megtakarításait egy széfben tárolja. Ezt csak adott számok adott sorrendben történő bevitelével lehet kinyitni. Ezért ezt a metódust nem szabad felülírni, mert akkor a széf nem nyitható. """ pass class Mother: def cook(self): """Az anya a saját stílusában főz.""" pass @final def make_herbal_tea(self): """Az anya képes egy különleges, gyógyító hatású teát készíteni. A hatás csak akkor jelentkezik, ha adott gyógynövények meghatározott arányban vannak jelen a keverékben. Ha az összetevők aránya ettől eltér, vagy akár csak egy is hiányzik, akkor a gyógyhatás nem érvényesül. """ class Child(Father, Mother): def mow_lawn(self): ... def cook(self): ... class GrandChild(Child): def mow_lawn(self): ... def cook(self): ... |
Ha ezt megtesszük, akkor nem csak a felhasználók tudják beazonosítani a nem felülírható metódusokat, hanem a statikus típusellenőrző alkalmazások is. Ezek hibajelzést fognak adni, ha találnak olyan alosztályt, amely felülír egy final dekorátorral ellátott metódust.
A final dekorátor hasznos, de nem kényszeríti ki futási időben a felülírhatatlanság követelményét. Vagyis, ha egy alosztály mégis felülír egy final metódust, akkor a kód akár hiba nélkül is lefuthat, és a szemantikai gond csak akkor fog kiderülni, ha a metódus az alosztály példányán meg lesz hívva.
Jó lenne valahogy elérni, hogy a nem megengedett felülírás futási időben idejekorán észlelhető legyen.
A Python 3.11-tól ez megvalósítható. Ugyanis ettől a verziótól a final dekorátor a dekorált metódusobjektumon létrehozza a __final__ speciális metódust, amit True értékre be is állít. Ez nem azt jelenti, hogy a final dekorátorral nem érintett metódusobjektumoknak is lesz __final__ attribútumuk és az False értékű. Nem, ezek a metódusobjektumok változatlanok maradnak.
Azt tudjuk tehát, hogy minden final dekorátorral ellátott metódusnak lesz egy __final__ speciális attribútuma. Ennek alapján össze tudjuk szedni egy adott osztály azon metódusneveit, amelyeket nem szabad alosztályban felülírni. Ha futási időben az alaposztályban valamilyen automatizmussal össze tudnánk gyűjteni az alosztályban definiált metódusneveket is, akkor ezeket összevetve az előbbi nevekkel meg lehetne tudni, hogy történt-e tiltott felülírás vagy sem. Ha igen, akkor kivételdobással lehetne ezt jelezni. A kérdés, hogy van-e olyan automatizmus, ami az alaposztályban rendelkezésre bocsátja az alosztályok metódusait vagy magát az alosztályobjektumot.
Igen, van ilyen lehetőség, ha az alaposztályban definiáljuk az __init_subclass__(cls) metódust. E metódus automatikusan meghívódik, valahányszor az adott osztályból újabb alosztályt hozunk létre. A cls nevű paraméter az új alosztályra fog hivatkozni, azaz az __init_subclass__ metódus törzsében rendelkezésre áll az alosztályobjektum. Ennek ismeretében az alábbiakat fogjuk tenni:
- Egy halmazban összegyűjtjük az alaposztályok azon metódusainak neveit, amelyek nem felülírhatónak vannak megjelölve, azaz a typing modul final függvényével vannak dekorálva.
- Ezt úgy tesszük, hogy végighaladunk az alosztály öröklési láncában szereplő összes osztályon, és azok attribútumai közül kiválogatjuk azon metódusokat, amelyek rendelkeznek a __final__ attribútummal.
- Egy halmazban összegyűjtjük az alosztályban definiált metódusok neveit.
- Képezzük a nem felülírhatónak megjelölt és az alosztályban definiált metódusok metszetét.
- Ha nincs tiltott felülírás, akkor ez egy üres halmaz lesz. Ha viszont nem az, akkor a halmaz elemei azon metódusnevek, amelyeket az adott alosztály felülírt. Ekkor egy kivétel dobunk felsorolva, hogy mely metódusok lettek felülírva.
Ezt láthatjuk alább kódban, ahol a fenti lépéseknek megfelelő kódsorok a Father osztályban kommentekkel beazonosíthatók. Mivel a Mother osztályban is ugyanez a kód szerepel, ezért ott a kommentek nem jelennek meg.
|
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 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
# Python 3.11+ from typing import final class FinalMethodOverrideError(Exception): """A nem felülírható metódusok alosztályban történő felülírása esetén kelthető kivétel.""" pass class Father: def __init_subclass__(cls): # E metódus automatikusan meghívódik, valahányszor az adott osztályból újabb alosztályt hozunk létre. # A cls az új alosztályra hivatkozik. # Egy halmazban összegyűjtjük azon metódusok neveit, amelyek nem felülírhatónak vannak megjelölve, azaz # a typing modul final függvényével vannak dekorálva. nonoverridable_methods = set() # Végighaladunk az alosztály öröklési láncában szereplő összes osztályon, és azok attribútumai közül # kiválogatjuk azon metódusokat, amelyek rendelkeznek a __final__ attribútummal. for base in cls.mro()[1:]: nonoverridable_methods |= {attr_name for attr_name, attr_obj in vars(base).items() if callable(attr_obj) if getattr(attr_obj, "__final__", False) } # Egy halmazban összegyűjtjük az alosztályban definiált metódusok neveit. subclass_methods = {attr_name for attr_name, attr_obj in vars(cls).items() if callable(attr_obj)} # Képezzük a nem felülírhatónak megjelölt és az alosztályban definiált metódusok metszetét. overrided_methods = subclass_methods & nonoverridable_methods # Jó esetben ez egy üres halmaz. Ha nem az, akkor az elemek azon metódusnevek, amelyeket az adott # alosztály felülírt. Ekkor egy kivétel dobunk felsorolva, hogy mely metódusok lettek felülírva. if overrided_methods: overridden_method_names = ', '.join(sorted([f'"{method}"' for method in overrided_methods])) raise FinalMethodOverrideError(f'A következő metódusok felülírása a(z) {cls.__name__} ' f'osztályban nem megengedett: {overridden_method_names}.') def mow_lawn(self): """Az apa a saját stílusában nyírja a füvet.""" pass @final def open_the_safe(self): """Az apa a saját megtakarításait egy széfben tárolja. Ezt csak adott számok adott sorrendben történő bevitelével lehet kinyitni. Ezért ezt a metódust nem szabad felülírni, mert akkor a széf nem nyitható. """ pass class Mother: def __init_subclass__(cls): print(cls, cls.mro()[1:]) nonoverridable_methods = set() for base in cls.mro()[1:]: nonoverridable_methods |= {attr_name for attr_name, attr_obj in vars(base).items() if callable(attr_obj) if getattr(attr_obj, "__final__", False) } print('A szülők final metódusai: ', nonoverridable_methods) subclass_methods = {attr_name for attr_name, attr_obj in vars(cls).items() if callable(attr_obj)} print('Az aktuális alosztály metódusai', subclass_methods) overrided_methods = subclass_methods & nonoverridable_methods print(f'A {cls.__name__} alosztály által felülírt metódusok', overrided_methods) if overrided_methods: overridden_method_names = ', '.join(sorted([f'"{method}"' for method in overrided_methods])) raise FinalMethodOverrideError(f'A következő metódusok felülírása a(z) {cls.__name__} ' f'osztályban nem megengedett: {overridden_method_names}.') def cook(self): """Az anya a saját stílusában főz.""" pass @final def make_herbal_tea(self): """Az anya képes egy különleges, gyógyító hatású teát készíteni. A hatás csak akkor jelentkezik, ha adott gyógynövények meghatározott arányban vannak jelen a keverékben. Ha az összetevők aránya ettől eltér, vagy akár csak egy is hiányzik, akkor a gyógyhatás nem érvényesül. """ pass class Child(Father, Mother): def mow_lawn(self): ... def cook(self): ... class GrandChild(Child): def mow_lawn(self): ... def cook(self): ... def make_herbal_tea(self): ... # Eredmény: # Traceback (most recent call last): # ... # FinalMethodOverrideError: A következő metódusok felülírása a(z) GrandChild osztályban nem megengedett: "make_herbal_tea". |
A működés teszteléséhez a GrandChild osztályban szándékosan felülírtuk a make_herbal_tea() metódust. Ha lefuttajuk az osztálydefiníciókat tartalmazó szkriptet, akkor FinalMethodOverrideError kivétel keletkezik, amelynek hibaüzenete megmondja, hogy mely alosztály mely nem felülírható metódusokat írta felül.
Mondhatnánk, hogy készen vagyunk, hiszen van egy működőképes mechanizmusunk a tiltott felülírások futási időben, de még a definíciós fázisban történő kiszűrésére. De, ahogy látjuk minden alaposztályban ugyanazt az ellenőrző kódsort meg kell ismételni. Mint tudjuk, a forráskód másolással történő ismétlése karbantartási nehézséget okoz és hibalehetőséget hordoz. Ezért ismétlődő forráskód esetén meg kell vizsgálni, hogy lehet-e és hogyan kiemelni az ismétlődő kódsorokat úgy, hogy azokat csak egyszer kelljen megírni.
Jelen esetben igen, ki lehet szervezni az ismétlődő __init_subclass__ metódust mégpedig egy mixin osztályba. Ha így teszünk, akkor minden olyan alaposztályban, amely final metódust tartalmaz és örökli a mixin osztályt, az alosztályban történő felülírás ellenőrzésre kerül. Így egy rugalmas osztálystruktúrát kapunk, amely a következő:
|
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 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
# Python 3.11+ from typing import final class FinalMethodOverrideError(Exception): """A nem felülírható metódusok alosztályban történő felülírása esetén kelthető kivétel.""" pass class FinalOverrideCheckMixin: """A final dekorátorral megjelölt metódusok alosztályban történő felülírását ellenőrző mixin osztály. Amelyik osztály ezt örökli, annak alosztályaira az ellenőrzés megtörténik. """ def __init_subclass__(cls): nonoverridable_methods = set() for base in cls.mro()[1:]: nonoverridable_methods |= {attr_name for attr_name, attr_obj in vars(base).items() if callable(attr_obj) if getattr(attr_obj, "__final__", False) } subclass_methods = {attr_name for attr_name, attr_obj in vars(cls).items() if callable(attr_obj)} overrided_methods = subclass_methods & nonoverridable_methods if overrided_methods: overridden_method_names = ', '.join(sorted([f'"{method}"' for method in overrided_methods])) raise FinalMethodOverrideError(f'A következő metódusok felülírása a(z) {cls.__name__} ' f'osztályban nem megengedett: {overridden_method_names}.') class Father(FinalOverrideCheckMixin): def mow_lawn(self): """Az apa a saját stílusában nyírja a füvet.""" pass @final def open_the_safe(self): """Az apa a saját megtakarításait egy széfben tárolja. Ezt csak adott számok adott sorrendben történő bevitelével lehet kinyitni. Ezért ezt a metódust nem szabad felülírni, mert akkor a széf nem nyitható. """ pass class Mother(FinalOverrideCheckMixin): def cook(self): """Az anya a saját stílusában főz.""" pass @final def make_herbal_tea(self): """Az anya képes egy különleges, gyógyító hatású teát készíteni. A hatás csak akkor jelentkezik, ha adott gyógynövények meghatározott arányban vannak jelen a keverékben. Ha az összetevők aránya ettől eltér, vagy akár csak egy is hiányzik, akkor a gyógyhatás nem érvényesül. """ pass class Child(Father, Mother): def mow_lawn(self): ... def cook(self): ... def make_herbal_tea(self):... def open_the_safe(self):... class GrandChild(Child): def mow_lawn(self): ... def cook(self): ... # Eredmény: # Traceback (most recent call last): # ... # FinalMethodOverrideError: # A következő metódusok felülírása a(z) Child osztályban nem megengedett: "make_herbal_tea", "open_the_safe". |
Ebben a bejegyzésben elsősorban a typing modul final dekorátorát, az egyszeres és többszörös öröklést, az egyéni kivételek definiálását és a mixin osztályokat érintettük. Ezekről a Python tudásépítés lépésről lépésre című e-könyvben a „Típusutalások és statikus típusellenőrzés támogatása”, „Öröklődés” és „Egyéni kivételtípusok megvalósítása” című fejezetekben lehet részletesen olvasni.