Egy kis dekoráció
A python nyelvben a dekorátor egy "szintaktikai cukor" (szinte fáj az ilyet leírni magyarul), semmi újat nem ad a nyelvhez. A java annotációkhoz hasonló módon közvetlen a dekorálandó objektum elé helyezett, @ karakterrel kezdett kifejezés, amely például a klasszikus dekorátor tervezési minta használatához és sok más egyéb dologhoz jó. Lássuk hát mi is ez pontosan, illetve hogyan kell használni.
A dekorátor végül is egy függvény (illetve callable, azaz hívható objektum), ami egyetlen bemenő paraméterként a dekorálandó valamit kapja meg, visszatérési értéke pedig általában a dekorálandó valamihez hasonló működést produkáló valami - persze lehet valami teljesen más is, csak akkor már semmi köze nem lesz a dekorátor mintához a dolognak.
A fent leírt dologra mindig is képes volt a python, a dekorátor intézménye csak kicsit megszépíti, olvashatóbbá teszi a műveletet - esetleg pont ellenkezőleg, ha nem tudjuk miről van szó :) Függvényekre, metódusokra a 2.4-es, osztályokra a 2.6-os verziótól kezdve használható a kukacos írásmód. Nade hogy tiszta vizet öntsünk a pohárba, lássuk mi is ez:
def butaDekorator(func): func.buta = True return func def butaFv1(): pass butaFv1 = butaDekorator(butaFv1) @butaDekorator def butaFb2(): pass butaFv1.buta # True butaFv2.buta # True
A két függvénnyel pontosan ugyanaz a dolog történik, csak míg az első eset a kezdők számára is egészen érthető, a második eset kompaktabb, és pláne akkor szebb, ha egy függvényre több dekorátort aggatunk, pl. egy django view esetében ez:
@login_required @cache_page(60 * 15) def my_view(request): ...
sokkal szebb, mint ez:
my_view = login_required(cache_age(60 * 15)(my_view))
traceEntryExit
Nézzük meg egy - klasszikus - példán keresztül, hogyan is csinálhatunk mi magunk dekorátorokat. A példa a traceEntryExit nevű dekorátor lesz, ami a dekorált függvény meghívását és befejeződését írja majd ki. A kód, amit a dekorátor fejlődése közben tesztként meghívok, az legyen az alábbi:
from entryexit import traceEntryExit @traceEntryExit def proba(param): """proba fuggveny a traceEntryExit tesztelesehez""" print "a kapott parameter: %s" % param return "-> %s <-" % param proba("proba parameter")
Az entryexit.py fileba pedig elkezdhetjük a dekorátorunkat, első körben csináljunk egy olyat, ami nem csinál semmit, és nézzük meg, hogy műkődik-e a proba.py, ha lefuttatjuk:
def traceEntryExit(func): return func
$ python proba.py a kapott parameter: proba parameter
Amint látható a proba fuggvény működik, mintha misem történt volna, de mondjuk igazából nem is történt semmi :)
Azt szeretnénk tehát, hogy a dekorált függvény lefutása előtt, illetve után közvetlenül kiírnánk valamit. Ehhez nem elég a dekorátorunkban kiírni a valamit - mert az nem a függvény futásakor íródna ki, hanem a dekoráláskor, azaz a függvény deklarációjánál -, hanem egy másik függvényt kell visszaadnunk, ami annyit csinál, hogy kiírja a kívánt dolgokat, és közben lefuttatja a dekorált függvényt. Ezt a függvényt magán a dekorátoron belül deklaráljuk, elég ha csak ott áll a rendelkezésünkre. (Segítségképp meg csináltam egy "logger" függvényt is, csúnya dolog print utasításokat elhelyezni szerte a kódba, később így könnyebben logolhatunk fileba például.)
Az entryexit.py tartalma:
import time def dummyLogger(txt): print "[TraceEE] %s" % txt logger = dummyLogger def traceEntryExit(func): def inner(*args, **kwargs): logger("entering %s" % func.__name__) starttime = time.time() retVal = func(*args, **kwargs) endtime = time.time() logger("leaving %s [exec time: %s sec.]" % (func.__name__, endtime-starttime)) return retVal return inner
$ python proba.py [TraceEE] entering proba a kapott parameter: proba parameter [TraceEE] leaving proba [exec time: 2.59876251221e-05 sec.]
Ugye nem is olyan bonyolult! És még a dekorált függvény futási idejét is megmérte nekünk. A belső függvény mindenféle argumentumot elfogad, azokat továbbadja a dekoráltnak, és valóban nem csinál mást, mint hogy meghívja a függvényt, előtte és utána pedig lekéri a rendszeridőt, illetve kiírja, hogy ott járunk, a dekorátor pedig ezt a belső függvényt adja vissza. Tökéletes! Vagy mégsem? Nem teljesen:
>>> from proba import proba >>> proba.__name__ 'inner' >>> proba.__doc__ >>>
Tehát a függvényünk neve, illetve a docstring elveszett, illetve a belső függvényé van meg helyett. Természetesen a problémával nem mi találkozunk először, és 2.5-ös pythontól hivatalos megoldásunk is van, ez pedig a functools.wraps dekorátor, amivel ha a belső függvényünket megdekoráljuk, akkor az a dekorált függvény metaadatait átvarázsolja az eredményre.
from functools import wraps ... def traceEntryExit(func): @wraps(func) def inner(*args, **kwargs): ...
>>> proba.__name__ 'proba' >>> proba.__doc__ 'proba fuggveny a traceEntryExit tesztelesehez'
Szuper, már tudunk is magunknak dekorátort csinálni. Nade mivan, ha valami paramétert szeretnénk adni a dekorátorunknak, azt hogyan tehetjük meg, hiszen azt mondtam, hogy a dekorátornak egyetlen paramétere a dekorálni kívánt valami. Semmi gond, egy olyan függvény kell nekünk, ami több paramétert is fogad, és egy dekorátort ad vissza (ami pedig egy masik függvényt, tehát első ránézésre félelmetes, háromszorosan egymásba ágyazott valaminek tűnik a dolog, de ha szépen végiggondoljuk, és átírjuk a @-os megoldást a hagyományosra, akkor azért logikus a dolog).
Módosítsuk hát a dekorátorunkat, hogy paraméterként átadhassunk neki egy saját logger függvényt, illetve egy showParams paraméterrel állítani lehessen, hogy kiírjuk-e a bejövő paramétereket és a visszatérési értéket.
Lássuk a teljes entryexit.py file-t a módosítások után:
from functools import wraps import time def dummyLogger(txt): print "[TraceEE] %s" % txt def traceEntryExit(function=None, showParams=True, logger=dummyLogger): def wrapper(func): @wraps(func) def inner(*args, **kwargs): logger("entering %s" % func.__name__) if showParams: if args: logger("*args:") for arg in args: logger(" - %s" % arg) if kwargs: logger("**kwargs:") for kwarg in kwargs: logger(" - %s: %s" % (kwarg, kwargs[kwarg])) starttime = time.time() retVal = func(*args, **kwargs) endtime = time.time() logger("leaving %s [exec time: %s sec.]" % (func.__name__, endtime-starttime)) if showParams: logger("return value: %s" % retVal) return retVal return inner if function: return wrapper(function) return wrapper
Annyival egészítettem még ki, hogyha paraméter nélkül használjuk (azaz mintha csak sima dekorátor lenne), akkor úgy is viselkedik, mint egy sima dekorátor - így az eredeti proba.py-nk is működőképes marad:
$ python proba.py [TraceEE] entering proba [TraceEE] *args: [TraceEE] - proba parameter a kapott parameter: proba parameter [TraceEE] leaving proba [exec time: 2.09808349609e-05 sec.] [TraceEE] return value: -> proba parameter <-
Illetve működnek az új paraméterek is:
>>> def myLogger(txt): ... print "[%d] %s" % (time.time(), txt) ... >>> @traceEntryExit(showParams=False, logger=myLogger) ... def proba2(param): ... print "ez itt a proba2, parameter: %s" % param ... time.sleep(2) ... >>> proba2('nagyon proba') [1282314984] entering proba2 ez itt a proba2, parameter: nagyon proba [1282314986] leaving proba2 [exec time: 2.00963687897 sec.]
Frankó, ugye? A forrás letölthető változatban: entryexit.py
Posted
by
Szép munka, jól összefoglaltad. Kudos!
Említsük meg azért dekorátorok árnyoldalait is:
Tehát csak ésszel és mértékkel. "If all you have is a hammer, every problem looks like a nail." Ebbe a csapdába ne essünk.
Fontos: a dekorátorok sorrendje szignifikáns! Egymást csomagolják be mint a Matryoshka babák, a legkisebb, tehát a függvényhez legközelebb álló kezdi a munkát és az utolsó fejezi be.
Van még egy szerepe ennek a syntax sugarnak amit a Django használ ki, de úgy már eltér a dekorátor tervezési mintától, mégpedig amikor nem módosítjuk a dekorált függvényt, hanem pl. regisztráljuk valahova, vagy dobunk egy warningot.
Példa:
registered_functions=[] def register(fn): global registered_functions registered_functions += [fn] return fn @register def fuggveny(a,b,c): passÍgy néz ki nagyjából a Django custom tagek és filterek regisztrálása is.
Az osztályok dekorációja lemaradt (py 2.6+), de az külön cikket érdemel.
Igen, az osztaly dekoraciot nem akartam belekeverni, arrol majd talan kesobb valami hosszabb lelegzetvetelut :)
alapbol az volt, de valami gond van a rest pluginnel, es exceptionoket dobal, meg nem volt idom kidebuggolni (pl. az elozo kommenteden is elhasal) - igyekszem orvosolni. szerencsere semmi nem vesz el, amit beirt az ember az ugy marad, csak mashogy jelenik meg, egyelore atirtam html-re a kommentedet, ugy elvezhetobb, restesiteni nem sikerult, pedig a bejegyzesek abban vannak, a commentekkel van csak valami gond.
update: hegesztettem kicsit az rst parseren, mostmar elvileg a commentek is atjutnak rajta
persze ha valami nem "valid rst", akkor ott dobhat hibat, de elvileg mar az sem oli meg az egeszet, csak kiirja, pl:
Príma. Kössz! Lehet írok én is egy cikket, ha van még valamire kereslet :) A dekorátort szívesen megírtam volna, de FIFO az élet :D Metaprogramozás/__metaclasses__ ? kell az egyáltalán valakinek ? :)
Az szinten jo tema, bar valoban ritkabban fut ossze vele az ember. Viszont ha osszefut, akkor nem art tudni, hogy hogyan mukodik :)
Kell. Django pl. erősen épít a metaclassokra.
Sziasztok! Nagyon jó az oldal, örülök, hogy van ilyen. Én elég kezdő vagyok, és jól jön, hogy van. Érthető a leírás, lehet, hogy pont most épp nem kell nekem, de amikor legközelebb pl dekorátort látok, akkor már tudom mi az. (láttam már, és akkor nem magyarázták el) Kitartást a laphoz, jó ez!! Üdv:Bálint