Függvények dekorálásával korábban már foglalkoztunk a „Mi a függvénydekorátor és mikor használjuk?” című bejegyzésben. A dekorálást leginkább closure függvénnyel megvalósított dekorátorral szokták megvalósítani. Ez a megoldás akkor lesz egy kicsit kényelmetlen, ha paraméteres dekorátorra van szükség. Ilyenkor ugyanis több szinten beágyazott függvényeket kell definiálni, ami kevésbé könnyen átlátható kódot eredményez. Ezen tudunk valamelyest javítani, ha dekorátornak egy osztályt definiálunk. A dekorátor osztályban többszörös egymásba ágyazás helyett azonos szinten levő metódusokban különülnek el a dekorálás végrehajtásához szükséges kódegységek.
Ennek bemutatásához legyen az a feladat, hogy készítünk egy olyan dekorátort, amely függvények argumentumainak típusellenőrzését végzi. A dekoráláskor argumentumként lehet megadni a dekorálandó függvény egyes paramétereire megengedett egy vagy több típust a típusnévvel. Ha az adott függvényparaméter többfajta típusú argumentumot tud fogadni, akkor a dekorátornak argumentumként a típusneveket szolgáltató iterálható objektumot kell megadni. A dekorátort megvalósító ArgsTypeChecker osztály definícióját láthatjuk alább. A működés megértését a részletes kommentek segítik.
|
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 |
class ArgsTypeChecker: def __init__(self, *args_type_classnames): # Az egyes argumentumokra megengedett típusok. Ha egy argumentumra több típus is # megengedett, akkor a típusneveket egy iterálható objektum kell hogy szolgáltassa. self.args_types = args_type_classnames def __call__(self, function_to_be_decorated): # A dekorálandó függvény referenciáját eltároljuk. self.func = function_to_be_decorated # Visszaadja a dekorálást ténylegesen megvalósító belső függvényt, ami az # eredeti függvény dekorált változata lesz. return self._inner def _inner(self, *args): error = False # Sorban vesszük a dekorát függvény argumentumait és a hozzájuk tartozó megengedett típust vagy típusokat. # A sorszámozás csak a nem megfelelő típusú argumentum hivatkozásához kell. for i, arg_types_pair in enumerate(zip(args, self.args_types), 1): # Egy adott argumentum és a hozzá tartozó megengedett típus vagy típusok sorozata. arg, tp = arg_types_pair try: # Ha az argumentumhoz iterálható objektumban több megengedett típus van felsorolva, akkor # ellenőrizzük, hogy az argumentum típusa ezek valamelyikének megfelel-e. iter(tp) if not any([isinstance(arg, t) for t in tp]): error = True except TypeError: # Ha az argumentumhoz csak egy megengedett típus tartozik, akkor erre vonatkozóan végzünk ellenőrzést. if not isinstance(arg, tp): error = True # Ha valamelyik argumentum nem megengedett típusú, akkor kivételdobás történik jelezve, hogy # melyik argumentum típusa nem felel meg az előírtaknak. if error: raise TypeError(f'Az {i}. argumentum ({arg}) típusa nem megfelelő') return self.func(*args) |
Jól követhető, hogy az __init__ metódus a dekorátor paramétereit fogadja, a __call__ pedig a dekorálandó függvényt, és egy olyan harmadik metódusobjektummal tér vissza, amely a dekorálás logikai részét valósítja meg. Ezáltal a dekorátor kódjának olvashatósága jobb, mintha ugyanezt többszintű closure függvénnyel valósítottuk volna meg.
A következő három tesztfüggvényt az ArgsTypeChecker osztállyal dekoráltuk három eltérő paraméterezéssel.
|
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 |
@ArgsTypeChecker((int, float, complex), (int, float, complex)) def add(x, y): return x + y @ArgsTypeChecker((int, float, complex)) def multiply(x, y): return x * y @ArgsTypeChecker(str, int) def repeat_string(string, n): return string*n # TESZT print(add(2, 3)) # 5 print(add(2.2, 3.3)) # 5.5 print(add(2 + 2j, 3 + 3j)) # (5+5j) print(add('a', 'b')) # TypeError: Az 1. argumentum (a) típusa nem megfelelő print(multiply(2, 3)) # 6 print(multiply(2.2, 3.3)) # 7.26 print(multiply(2.2, 'r')) # TypeError: can't multiply sequence by non-int of type 'float' print(multiply('r', 3.3)) # TypeError: Az 1. argumentum (r) típusa nem megfelelő print(repeat_string('e', 3)) # eee print(repeat_string('c', 'd')) # TypeError: Az 2. argumentum (d) típusa nem megfelelő print(repeat_string(2, 3)) # TypeError: Az 1. argumentum (2) típusa nem megfelelő |
Az első esetben a kétváltozós függvény mindkét argumentumának lehetséges típusait megadtuk. Mivel bármelyik argumentum int, float és complex típusú lehet, ezért e lehetséges típusokat egy-egy tuple konténerben adtuk át.
A második függvénynél csak az első paraméter által fogadott értékek lehetséges típusait írtuk elő. Ennek következtében, ha a második paraméter helytelen típusú értéket kap, akkor azt a dekorátor nem jelzi ki. Ha szerencsénk van, akkor a típushiba miatt nem lesz a függvény törzsében a művelet elvégezhető, és ekkor az interpreter egy erre vonatkozó hibajelzést ad. De, ha a helytelen típusú argumentum ellenére is elvégezhető a művelet, akkor ez akár nehezen észrevehető szemantikai hibát is eredményezhet.
A harmadik függvény azt az esetet szemlélteti, amikor az argumentumok csak egyféle típusú értéket fogadhatnak.
Az osztállyal megvalósított dekorátorral a Python tudásépítés lépésről lépésre című e-könyv „Mágikus metódusok egyedi igényre szabott osztályokban” fejezet „Osztálypéldány hívhatóvá tétele” alfejezetében találkozhatunk mint tipikus használati eset. Az egyszerű és paraméterezhető dekorátorokkal, a dekorátorok láncolásával a „Képességfejlesztés – függvénydekorátorok” című fejezete foglalkozik, amelyben a fontosabb alkalmazásaikra adott példák ismertetése mellett arról is esik szó, hogy mire kell ügyelni a használatukkor.