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

19aug.

9 Comment for Egy kis dekoráció

  1. Attila Forgacs says:

    Szép munka, jól összefoglaltad. Kudos!

    Említsük meg azért dekorátorok árnyoldalait is:

    • mélyíti a call-stacket
      • emiatt nehezebb debuggolni, mert mindenféle köztes függvényeken megyünk keresztül (van rá tool ami kiszűri ezeket a tracebackből, természetesen :) )
      • rekurziónál tágra tárt pupillákat okozhat.
    • "kvázi" lassít: függvényhívás nem olcsó művelet, olyan függvényt lehetőleg ne dekoráljunk amit tényleg gyakran hívunk meg, pl egy cikluson belül. Normál használat alatt nem érezhető sok különbség.

    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.

  2. Attila Forgacs says:
    There goes my significant whitespace... Again... :) Ugrott a formázás de totál. Láttátok volna milyen szépen behúztam PEP8 4 es spacekkel be volt tabbelve az egész. Lehet ReST-et irni a comment fieldbe ?
    • RePa says:

      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.

      • RePa says:

        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:

        Error in markup: System Message: ERROR/3 , line 5, Unknown directive type "mokus".
  3. Attila says:

    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 ? :)

  4. Balint says:

    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