## Define a decorator function
def my_decorator(func):
# decorator function has a inner function which calls the decorated function
def inner():
print("decorator did something")
# calling decorated function
output = func()
print(output)
# lastly the inner function is returned
return inner
## Define a decorated function, decorate with '@' prefix
@my_decorator
def my_decorated():
print("decorated did something")
return 42
# calling the decorated function
my_decorated() # decorator did something \
# decorated did something # 42
## Decorator is just as running a function and its nested function,
# The above functions without the '@' prefix are similar to this
def my_decorated():
print("decorated did something")
return 42
# here inner will be returned as 'decorated_func',
# and we call it next to get the similar output
decorated_func = my_decorator(my_decorated)
decorated_func() # decorator did something \
# decorated did something # 42
## Example: Create a decorator to extend the functionality of a function
# First define the decorator function with parameter as decorated function
def my_decorator(deco_func):
# Second define the inner function
# Note: Inner function can do something before/after calling our
# wrapped function
def my_inner(deco_func_para1, deco_func_para2):
# Notice: The parameters of a decorated function should be the
# parameters of inner function
# because this function is going to be returned and we're going to
# call it do something of inner function say printing
print(f"Product of two numbers is {deco_func_para1 * deco_func_para2}")
# now calling our decorated function, passing the required arguments
output = deco_func(deco_func_para1, deco_func_para2)
return output
# Finally return the inner function
return my_inner
def my_fun(a, b):
print(f"Sum of two numbers is {a+b}")
# calling 'my_fun()' gets sum of two numbers
my_fun(10, 20) # Sum of two numbers is 30
# to extend 'my_fun()' functionality without changing its previous code,
# pass the 'my_fun' as 'deco_func' to 'my_decorator()'
my_decorated_fun = my_decorator(my_fun)
# done, 'my_decorated_fun()' can now do both product as well as sum
my_decorated_fun(10, 20) # Product of two numbers is 200 \
# Sum of two numbers is 30
## Now doing the same using decorators, just add '@<decorator_function_name>'
@my_decorator
def my_fun(a, b):
print(f"Sum of two numbers is {a+b}")
# now calling 'my_fun()' will automatically call/invoke 'my_decorator()'
my_fun(10, 20) # Product of two numbers is 200 \
# Sum of two numbers is 30
## Example: Create a decorator function that replaces the decorated function
def my_adder(func):
def adder(para1, para2):
return para1 + para2
return adder
# Decorating 'subtr()' and 'mutpl()' with 'my_adder()'
@my_adder
def subtr(p1, p2):
return p1 - p2
@my_adder
def mutpl(p1, p2):
return p1 * p2
## Now calling 'subtr()' and 'mutpl()'
print(subtr(20, 10)) # 30
print(mutpl(2, 5)) # 7
# this is because we didn't call the 'func()'/decorated function inside
# 'my_adder()', so if a function is decorated with 'my_adder' it is going
# to be replaced by 'adder()' and so 'func()' won't be called
## Example: Create a function that prints/returns the output
# this outer function is called a decorator factory,
# because it returns a decorator
def result_fetcher(print_op=True):
# this is our decorator function
def resulter(func):
def inner(para1, para2):
output = func(para1, para2)
if print_op:
print(f"Output is {output}")
else:
return output
return inner
return resulter
## Passing parameters to our decorator
@result_fetcher(print_op=False)
def addr(p1, p2):
return p1 + p2
@result_fetcher(print_op=True)
def multpl(p1, p2):
return p1 * p2
# Now calling 'addr()' and 'multpl()'
print(addr(10, 20)) # 30
multpl(10, 20) # Output is 200
from functools import lru_cache
# simply add a decorator to our 'power_of()' function
@lru_cache
def power_of(num1, num2):
print("power_of was called")
return pow(num1, num2)
# when calling the function for the first time 'lru_cache' will store its result
v = power_of(342, 388) # power_of was called
# and next time for the same inputs results will be returned directly,
# without running our function, you can see no print happening here
v = power_of(342, 388)
## Set the size of lru_cache by passing a parameter
# if the memory gets full, the least recently used ones will be removed
@lru_cache(256)
def power_of(num1, num2):
print("power_of was called")
return pow(num1, num2)
## Checking function call counts of a fibonacci function with & without lru_cache
# Note: 'cache' is similar to 'lru_cache', only without the size limit,
# good for smaller programs
from functools import cache
## Checking without caching
call_counter = 0
def fib(num):
global call_counter
call_counter += 1
if num < 2:
return num
else:
return fib(num - 1) + fib(num - 2)
fib(10)
print(call_counter) # 177
## Checking with caching
call_counter = 0
@cache
def fib(num):
global call_counter
call_counter += 1
if num < 2:
return num
else:
return fib(num - 1) + fib(num - 2)
fib(10)
print(call_counter) # 11
# 177 vs 11, the difference is significant
## Check cache info like times hit/used, missed and current remaining size
print(fib.cache_info()) # CacheInfo(hits=8, misses=11, \
# maxsize=None, currsize=11)
# clearing the cache
fib.cache_clear()