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:
|
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 |
class Dict(dict): def __setitem__(self, key, value): if type(key) is str: key = '_' + key super().__setitem__(key, value) def __getitem__(self, key): value = super().__getitem__(key) print(f'__getitem__({repr(key)}) ' f'-> {value}') return value # TESZT # Minden lehetséges módon beviszünk kulcs-érték párokat. d = Dict(a=1, b=2) d.setdefault('c', 3) d.update(d=4) d['e'] = 5 print(d) # {'a': 1, 'b': 2, 'c': 3, 'd': 4, '_e': 5} # Látható, hogy csak az utolsó esetben, a közvetlen értékadás esetén lesz meghívva a __setitem__. # Az értékkikéréskor pedig nem hívódik meg a __getitem__, hiszen az ott szereplő kiíratás eredménye nem látszik. d.get('a') d.setdefault('_e') |
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.
|
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 |
from collections import UserDict class Dict(UserDict): def __setitem__(self, key, value): if type(key) is str: key = '_' + key super().__setitem__(key, value) def __getitem__(self, key): value = super().__getitem__(key) print(f'__getitem__({repr(key)}) ' f'-> {value}') return value # TESZT # Minden lehetséges módon beviszünk kulcs-érték párokat. d = Dict(a=1, b=2) d.setdefault('c', 3) d.update(d=4) d['e'] = 5 print(d) # {'_a': 1, '_b': 2, '_c': 3, '_d': 4, '_e': 5} # Látható, hogy minden kulcs-érték pár felvételi lehetőség esetén a kulcsokat a követelményeknek megfelelően alakítja át. # Értékkikéréskor a __getitem__ meg lesz hívva. d.get('_a') d.setdefault('_e') # __getitem__('_a') -> 1 # __getitem__('_e') -> 5 |
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.
|
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 |
from collections.abc import Hashable from collections import UserDict def key_value_types(KeyType, ValueType): """Osztálydekorátor, amellyel az egyéni szótárosztályt kell dekorálni, és amelynek argumentumaival lehet meghatározni a szótár kulcsainak és értékeinek típusát. A kulcsok csak azonos típusúak lehetnek és ezért csak egy típust lehet megadni. Az értékekre egy vagy több típust lehet előírni. Több típus esetén azokat egy tuple konténerben kell felsorolni. """ def dekor(cls): if type(KeyType) is not type: raise TypeError('A kulcstípus argumentum nem típus') if not issubclass(KeyType, Hashable): raise TypeError('A kulcstípus hashelhető típus kell, hogy legyen.') if type(ValueType) is not type and type(ValueType) is not tuple: raise TypeError('A szótár értékeinek típusára megadott argumentum nem típus vagy ' 'a típusok nem tuple konténerben vannak felsorolva.') Vtypes = ValueType if type(ValueType) is tuple else (ValueType,) if not all(type(e) is type for e in Vtypes): raise TypeError('A szótár értékeinek lehetséges típusára felsorolt argumentumok valamelyike nem típus.') cls.KeyType = KeyType cls.ValueTypes = Vtypes return cls return dekor class DictMixin: """A __setitem__ felülírt definícióját tartalmazza, amely közös a létrehozandó egyéni szótárakban, ezért annak ezt az osztályt is kell örökölnie a UserDict mellett. """ def __setitem__(self, key, value): if not isinstance(key, type(self).KeyType): raise TypeError(f"A szótár kulcsai csak '{type(self).KeyType.__name__}' típusúak lehetnek.") if not isinstance(value, type(self).ValueTypes): raise TypeError(f'A szótár értékei csak {str([vt.__name__ for vt in type(self).ValueTypes]).strip("[]")} ' f'típusúak lehetnek.') super().__setitem__(key, value) # Példák egyéni szótárak definíciójára, amelyek csak a megadott típusú kulcsokat és értékeket fogadják el. @key_value_types(str, int) class DictStrInt(DictMixin, UserDict): ... @key_value_types(str, (int, float, complex)) class DictStrNumber(DictMixin, UserDict): ... @key_value_types(tuple, (bytes, bytearray)) class DictTupleBytes(DictMixin, UserDict): ... # TESZT dint = DictStrInt(a=1) dint['b'] = 2 dint.update(c=3) dint.setdefault('d', 4) print(dint) # -> {'a': 1, 'b': 2, 'c': 3, 'd': 4} # Érvénytelen elemfelvételek. dint.update({123:6}) # -> TypeError: A szótár kulcsai csak 'str' típusúak lehetnek. dint.setdefault('e', 6.6) # -> TypeError: A szótár értékei csak 'int' típusúak lehetnek. dnum = DictStrNumber(a=1) dnum['b'] = 2.0 dnum.update(c=3 + 0j) dnum.setdefault('d', 4) print(dnum) # -> {'a': 1, 'b': 2.0, 'c': (3+0j), 'd': 4} # Érvénytelen elemfelvételek. dnum.setdefault((1,2,3), 6.6) # -> TypeError: A szótár kulcsai csak 'str' típusúak lehetnek. dnum.update(e='x') # -> TypeError: A szótár értékei csak 'int', 'float', 'complex' típusúak lehetnek. dbytes = DictTupleBytes() dbytes[(0, 0)] = b'abc' dbytes.update({(0, 1): bytearray(b'efg')}) dbytes.setdefault((1,0), bytearray([110, 112, 115])) print(dbytes) # -> {(0, 0): b'abc', (0, 1): bytearray(b'efg'), (1, 0): bytearray(b'nps')} # Érvénytelen elemfelvételek. dbytes[[0, 0]] = b'xyz' # -> TypeError: A szótár kulcsai csak 'tuple' típusúak lehetnek. dbytes.setdefault((1,1), [130, 140]) # -> TypeError: A szótár értékei csak 'bytes', 'bytearray' típusúak lehetnek. |
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.