Decorator is design pattern. It dynamically alters the functionality of a function, method, or class without having to directly use subclasses or change the source code of the function being decorated.
In Python decorators allow you to make simple modifications to callable objects like functions, methods, or classes.
Idea is to alter function or method without a mess and confusion:
defsome_transformation(func):# doing something with funcreturn func2deffoo(*args):# doing something...foo =some_transformation(foo)
Decorator will make this look really more readable and nice:
In a nutshell decorator is a function that take another (decorated) function as argument and returns transformed function. So it is:
Function(Function) ⟶ Function
1. Function is first-class object. Don't panic - it means that it is the same object as everything else. That's why we can assign function to another variable.
🪄 Code:
defsong(times=3): return"La"* timesprint(song())scream = song # song is boring! let's call it screamprint(scream(15))del song # delete old variabletry:song()exceptNameErroras e:print("Oops, Exception: ", e)scream.__qualname__="Super Song"print(scream)
📟 Output:
LaLaLa
LaLaLaLaLaLaLaLaLaLaLaLaLaLaLa
Oops, Exception: name 'song' is not defined
<function Super Song at 0x103fe2a60>
2. We can define function everywhere! It will exist in that namespace only. #easy!
🪄 Code:
defmusic(beats=3): import randomdef_random_music(times): notes ="La","Do","Re","Fa","Si"return",".join(random.choice(notes) for i inrange(times))def_random_effects(times): effects ="Toonc","Tync","Boom","Beep","Oooh"return"-".join(random.choice(effects) for i inrange(times))return"New pop-hit: {}\nNotes: {}".format(_random_effects(beats), _random_music(beats))print(music(10))
📟 Output:
New pop-hit: Tync-Tync-Boom-Oooh-Toonc-Beep-Beep-Boom-Beep-Toonc
Notes: Si,La,Do,Fa,Re,La,Do,La,Re,Do
Of course we can't access internal functions in any way...
🪄 Code:
try:_random_music(5)exceptNameErroras e:print("Oh, we can't acces this function outside:", e)
📟 Output:
Oh, we can't acces this function outside: name '_random_music' is not defined
3. We can even return function as function's result. After this we can use that object as new function itself.
New pop-hit: Toonc-Oooh-Oooh-Tync-Oooh-Oooh-Toonc-Oooh-Toonc-Oooh-Tync-Toonc-Boom-Oooh-Beep
Notes: Do,Fa,Si,Si,Do,Do,Do,Do,Fa,Do,La,Fa,Re,La,Si
New pop-hit: Oooh-Toonc
Notes: Si,Re
Decorator syntax and examples
Idea – function which returns processed result of another function
General format of usage:
@mydecoratordefmyfunc():pass
It is the absolutely the same as:
defmyfunc():passmyfunc =mydecorator(myfunc)
where mydecorator is some function...
Example of decorator realization
🪄 Code:
defmy_deco(func): defwrapper(): print(">>> Before running function")print(func())print(">>> After running function")return wrapper@my_decodefgreet():returnf"Hello!"greet()
📟 Output:
>>> Before running function
Hello!
>>> After running function
defmy_deco(func): # FINAL VERSION OF IDEAL DECORATORprint("Init of decorator...")import datetimedefwrapper(*args,**kwargs): wrapper.counter +=1 wrapper.last_run =str(datetime.datetime.now())print(">>> Before running!...") res =func(*args, **kwargs)if res:print(">>> Result is:", res)print(">>> After running....")return res wrapper.created_at =str(datetime.datetime.now()) wrapper.counter =0 wrapper.info =lambda: f'This function was decorated by IDEAL DECORATOR on {wrapper.created_at}'+\f' and ran {wrapper.counter} time{"s"if wrapper.counter !=1else""} (last run: {wrapper.last_run})'return wrapper@my_decodeffoo(x,y):return x ** yfoo(10, 40)foo(10, 40)foo.info()
📟 Output:
Init of decorator...
>>> Before running!...
>>> Result is: 10000000000000000000000000000000000000000
>>> After running....
>>> Before running!...
>>> Result is: 10000000000000000000000000000000000000000
>>> After running....
'This function was decorated by IDEAL DECORATOR on 2020-12-30 07:52:06.535243 and ran 2 times (last run: 2020-12-30 07:52:06.536808)'
You want more?...
Ok. It is very useful timer decorator
🪄 Code:
deftimer(f): import time definner(*args,**kwargs): start_time = time.time() res =f(*args, **kwargs) print(f'Function: {f.__name__}({(", ".join(map(str, args)) + ", ") if args else ""}{kwargs}), time spent: %.5s seconds' %(time.time() - start_time) )
return resreturn inner@timerdefmy_fnc(x=1): return"Length: "+str(len([pow(i,10) for i inrange(4000000)]))@timerdeff(): a = [str(x)*10for x inrange(10000000)]l =my_fnc(1)l2 =f()
📟 Output:
Function: my_fnc(1, {}), time spent: 3.666 seconds
Function: f({}), time spent: 7.063 seconds
"Fakely long runnning" decorator
🪄 Code:
defwork(func):defwrapper(*args,**kwargs):import timefor _ inrange(3):print("Work is in progress....") time.sleep(1)returnfunc(*args, **kwargs)return wrapper@workdeff():print("Done")f()
📟 Output:
Work is in progress....
Work is in progress....
Work is in progress....
Done
It is possible to add some random strings to show during "fake running" time window:
🪄 Code:
defwork(func): phrases = ["Work in progress....","Calculating shifts in raw data abstract vectors","Alligning matrixes of indexes for data frames","Reformatting data sources","Traversing trough raw data internals","Validating obtained subprocess results" ]defwrapper(*args,**kwargs):import timeimport randomfor _ inrange(3):print(random.choice(phrases) +"...") time.sleep(1)returnfunc(*args, **kwargs)return wrapper@workdefcalc_sum(x,y):return x + ycalc_sum(2, 2)
📟 Output:
Traversing trough raw data internals...
Validating obtained subprocess results...
Alligning matrixes of indexes for data frames...
4
Super cool decorator that controls the time of execution for decorated function and stops it in case of exceeding that time:
deftime_limit(func):"Giving <func> 3 seconds to run"import signaldefsignal_handler(signum,frame):raiseTimeoutErrordefwrapper(*args,**kwargs): signal.signal(signal.SIGALRM, signal_handler) signal.alarm(3)# <--- hardcoded Num of secondstry:returnfunc(*args, **kwargs)exceptTimeoutError:print("Timed out! ")return wrapper
Decorated function loosing it's name and docstring. In fact we are substituting one function with completely different one that just uses the first one.
Alternative - suggested method - to use functools.wraps decorator which will automatically assign wrapper function’s __module__, __name__, __qualname__, __annotations__ and __doc__.