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:
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:
def song(times=3):
return "La" * times
print(song())
scream = song # song is boring! let's call it scream
print(scream(15))
del song # delete old variable
try:
song()
except NameError as 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:
def music(beats=3):
import random
def _random_music(times):
notes = "La", "Do", "Re", "Fa", "Si"
return ",".join(random.choice(notes) for i in range(times))
def _random_effects(times):
effects = "Toonc", "Tync", "Boom", "Beep", "Oooh"
return "-".join(random.choice(effects) for i in range(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)
except NameError as 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
def my_deco(func): # FINAL VERSION OF IDEAL DECORATOR
print("Init of decorator...")
import datetime
def wrapper(*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 != 1 else ""} (last run: {wrapper.last_run})'
return wrapper
@my_deco
def foo(x, y):
return x ** y
foo(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:
def timer(f):
import time
def inner(*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 res
return inner
@timer
def my_fnc(x=1):
return "Length: " + str(len([pow(i,10) for i in range(4000000)]))
@timer
def f():
a = [str(x)*10 for x in range(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:
def work(func):
def wrapper(*args, **kwargs):
import time
for _ in range(3):
print("Work is in progress....")
time.sleep(1)
return func(*args, **kwargs)
return wrapper
@work
def f():
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:
def work(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"
]
def wrapper(*args, **kwargs):
import time
import random
for _ in range(3):
print(random.choice(phrases) + "...")
time.sleep(1)
return func(*args, **kwargs)
return wrapper
@work
def calc_sum(x, y):
return x + y
calc_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:
def time_limit(func):
"Giving <func> 3 seconds to run"
import signal
def signal_handler(signum, frame):
raise TimeoutError
def wrapper(*args, **kwargs):
signal.signal(signal.SIGALRM, signal_handler)
signal.alarm(3) # <--- hardcoded Num of seconds
try:
return func(*args, **kwargs)
except TimeoutError:
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__.