A dict szótárból öröklés problematikája, megoldása és alkalmazása

Szeretnénk egy olyan speciális szótárobjektumot létrehozni, amelyben a str típusú kulcsok egy aláhúzásjellel kezdődjenek függetlenül a kulcs-érték párok szótárba kerülésének módjától /a szótár létrehozásakor, közvetlen értékadással, vagy az update(), illetve setdefault() metódushívásokkal/ és attól, hogy a bevitelkor milyen karakterlánc lett megadva kulcsként.

Elsőre ez nem tűnik nehéz feladatnak, mert úgy gondolhatjuk, hogy egyszerűen célt érünk azzal, ha a dict beépített szótártípusnak egy olyan alosztályát hozzuk létre, amelyben a __setitem__ metódust – amely minden alkalommal meghívódik, amikor közvetlen értékadás történik, vagyis a kulcshoz értéket rendelünk a [] operátorral – felülírjuk úgy, hogy ha az argumentumként kapott kulcs str típusú, akkor azt egy megelőző aláhúzásjellel összefűzzük és egy az így módosított kulccsal hívjuk meg a szülő, azaz a dict __setitem__ metódusát. Ez kódban így néz ki:

A teszteléskor kulcs-érték párokat veszünk fel a Dict példányba a lehetséges módokon. Azonban, némi meglepetést okozva, az eredményből látható, hogy csak az utolsó esetben, vagyis a közvetlen értékadás esetén lesz meghívva a __setitem__ metódus, hiszen csak ekkor került a kulcs elé az aláhúzás karakter. A jelenség azért is váratlan, mert a dict a collections.abc modulban található MutableMapping alosztálya /az issubclass(dict, MutableMapping) True értéket ad/, amely pedig a kulcs-érték párok felvételét lehetővé tevő opciókban a közvetlen értékadást használja (ez a MutableMapping forráskódjában az update() és a setdefault() definíciójában is látható).

A dict működési furcsasága nem csak a __setitem__ estén mutatkozik meg, hanem például az értékkinyerésnél is. Vagyis a __getitem__ metódus sem hívódik meg akkor, amikor esetleg várnánk. Ennek bemutatásához az osztálydefinícióban a __getitem__ metódust is felülírtuk annak érdekében, hogy lássuk amikor meghívásra kerül. A tesztek nem eredményeznek kiírást, ami azt mutatja, hogy sem a get(), sem a setdefault() esetén nincs a __getitem__ meghívva.

A dict típusú szótár egy nagyon hatékony, gyors működésű objektum. A tapasztalt meglepő viselkedésnek az alapvető oka éppen ez, mert nagyon kifinomultan optimalizált a megvalósítása. A beépített típusok pedig általában a nyitott-zárt elvre (open-closed principle) figyelemmel lettek tervezve, vagyis nem módosításra szántak, viszont egy alosztályban attribútumokkal kiterjeszthetők.

Ugyanakkor, nem lettek figyelmen kívül hagyva az olyan speciális igényeket sem, mint a fenti, ahol a __setitem__, __getitem__, sőt esetleg a __delitem__ metódusok felülírása szükséges lehet. Az ilyen esetekben a collections modul UserDict osztálya használható, amelyből örökölve az említett dunder metódusokat felülírva az elvárt működés biztosítható. Ezt mutatja a következő osztálydefiníció, ahol csak annyit kellett az előzőhöz képest módosítani, hogy a dict helyett a szülőosztály a UserDict lett, amit természetesen előtte be kellett importálni.

A teszteredményekből látható, hogy az így létrehozott Dict szótárpéldány már minden kulcs-érték pár felvételi lehetőség esetén a kulcsokat a követelményeknek megfelelően alakítja át, és az értékkikéréseknél a __getitem__ is meghívásra kerül, vagyis ez is felülírható szükség esetén.

A dict szótárból való öröklés problematikájának szemléltetetésére szolgáló fenti egyszerű példánál több gyakorlati hasznossággal bíró alkalmazási esetet mutatunk alább, ahol olyan szótárak létrehozása a cél, amelyek csak előre meghatározott típusú kulcsokat és értékeket fogadnak el és tárolnak. Ha a példányosítás után nem a feltételeknek megfelelő elemekkel akarjuk a szótárt feltölteni, akkor egy kivételdobással hibaüzenetet kapunk. Ami a megvalósítást illeti, mivel a __setitem__ a konkrét előírt típusok ellenőrzésének közös logikáját valósítja meg minden ilyen egyéni szótárban, ezért kiszerveztük egy mixin osztályba. A specializált szótárosztály ezt és a UserDict osztályt örökli. Az előírt kulcs- és értéktípusokat pedig egy osztálydekorátor függvény argumentumaiként lehet megadni, amely törzsében a megadott típusok általános ellenőrzése történik.

E bejegyzésben a szótárobjektumok voltak a fókuszban, amelyek fontos és gyakori építőkockái a programoknak. Az alapvető jelentőségű dict típusú szótárról a Python tudásépítés lépésről lépésre című e-könyvben a „Beépített konténerobjektumok”, a „Beépített típusok nyilvános metódusai”, és a „Speciális metódus és adat attribútumok” fejezetekben lehet részletesen olvasni. A beépített dict típusú szótár mellett egyéb, specializált szótár konténereket a szabványos könyvtár is tartalmaz, amelyeket a „Készétel fogyasztás a szabványos könyvtár moduljainak használata” fejezet „Speciális konténer típusok” alfejezetében találhatjuk. A mixin osztályokat az „Öröklődés” fejezeten belül a „Viselkedések elegyítése – mixin osztályok” alfejezetben ismerhetjük meg. Osztálydekorátorokkal pedig az „Osztályok dekorálása” fejezet foglalkozik.

É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.