-
Notifications
You must be signed in to change notification settings - Fork 0
Decorators
It's a function which wraps other functions. This definition might not be clear, however, fret not and read on.
import functools
def some_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Doing something before
value = func(*args, **kwargs)
# Doing something after
return value
return wrapper
@some_decorator
def say_hi():
"""Python says hi to you! 😀"""
print('Hello there! 😀')
return {'H':0,'e':1,'l':2,'l':3,'o':4}
The name of the decorator is some_decorator
and it's decorating/wrapping another function say_hi
with @some_decorator
above say_hi
. This decorator can be used by using @some_decorator
. All that @some_decorator
is doing is wrapping say_hi
and returning what say_hi
is supposed to return. This is just for illustration purposes and might seem very useless at first, but if this decorator boilerplate template is used then you can create pretty impressive decorators.
Looking inside some_decorator
, there is another function wrapper
, which takes in any number of args
and kwargs
, assigns a value
to the function it's wrapping, in our case say_hi
, and returns the wrapped function and its values. If we don't return the value
parameter then it will not return the dictionary object of say_hi
. Try it by commenting return value
!
Finally we are returning the wrapper object without, which our decorator object would be defunct. Try this too by commenting return wrapper
!
The func
argument in some_decorator
is being accessed inside wrapper
by value
. This is called a closure.
The main feature of a closure is that it has full access to the variables and names defined in the local namespace where the closure was created, even though the enclosing function has returned and finished executing. E.g. wrapper
has access to the func
argument in some_decorator
even though that function has been executed, this is because a closure causes the inner function to retain state.
The structure of a closure is the same as the structure of some_decorator
.
- There's an inner function i.e.
wrapper
- A variable is being referenced from the enclosing function i.e
func
is being referenced fromsome_decorator
- At the end the inner function is being returned i.e.
return wrapper
Examples of static and dynamic closures are here.
Importing functools and using it as a decorator is the first use of decorators. @functools.wraps(func)
wraps any function passed to some_decorator
so that when we introspect func
i.e. help(func)
or func__name__
, it would be pointing to the name and help of func
instead of the wrapper function, wrapper
, that it's wrapped with. Try removing @functools.wraps(func)
and run help(say_hi)
and say_hi.__name__
.
Three main decorators will be looked into:
- @property
- @classmethod
- @staticmethod
The code, which will be used to demonstrate all of them is here and certain sections will be pasted below for illustration.
This decorator is used in the class to instantiate properties of the class. Since the class is a Circle, one of the properties is a radius and the other is an area.
Click to expand the code!
@property
def radius(self):
"""Get value of radius"""
return self._radius
@radius.setter
def radius(self, value):
"""Set radius, raise error if negative"""
if value >= 0:
self._radius = value
else:
raise ValueError("Radius must be positive")
@property
def area(self):
"""Calculate area inside circle"""
return self.pi() * self.radius**2
The radius of a circle must be given to calculate the area, however, one cannot set the area of a circle without the radius. Therefore, the radius is a property
of the class circle
.
As the radius can have a value, it will be set with a setter
method of the property
decorator, in our case @radius.setter
. More info on the getter, setter, & deleter
methods of property
here.
Note, how the radius method needs to have the same name for setting the @radius.setter
. If we were to use the getter & deleter
methods for the radius they would have the same method name as setter
.
The area doesn't need to be set and therefore doesn't have a setter
method like radius.
Since area & radius methods are properties, they can be retrieved as an attribute without (), e.g. Circle.area/Circle.radius
.
Classmethods are factory methods, which create a specific instance of the class. The unit_circle
is a specific instance of the class circle
, and therefore can be instantiated as below.
Click to expand the code!
@classmethod
def unit_circle(cls):
"""Factory method creating a circle with radius 1"""
return cls(1)
unit_circle
can access all the methods of Circle
and has access to all the other attributes as well.
Another such example and the entire code.
In this case there's an existing class to create employee records, however, this class requires first, last and pay
arguments in a certain format ('Bambino', 'Jones', 50000) to create a new employee. However, if the format changes i.e. 'John-Doe-70000', we can create a separate @classmethod to process the same. This class method takes a string as input and creates new employee records. The code below does the same.
Click to expand the code!
@classmethod
def from_string(cls, emp_str):
first, last, pay = emp_str.split('-')
return cls(first, last, pay)
Another example of class method is when we use to set the raise amount
. Refer to the code below.
Click to expand the code!
@classmethod
def set_raise_amount(cls, amount):
cls.raise_amount = amount
This is a method which is independent of the Circle
class and can be accessed by other methods in the class. This can be called on either an instance or on the class itself.
Click to expand the code!
@staticmethod
def pi():
"""Value of π, could use math.pi instead though"""
return 3.1415926535
Now that we have an idea about some decorators, let's look at some other cool ones. This directory contains the codes.
The first one is a timer function, which calculates how much time a function takes to execute.
Click to expand the code!
import functools
import time
def timing_func(func):
"""Prints the runtime of any decorated function."""
@functools.wraps(func)
def timer_wrapper(*args, **kwargs):
# perf_counter is used her to measure time intervals
start_time = time.perf_counter()
value = func(*args, **kwargs)
end_time = time.perf_counter()
# total run time
run_time = end_time - start_time
# printing the repr of the function name and
# the time in seconds
print(f"Finished {func.__name__!r} in {run_time:.4f} secs")
return value
return timer_wrapper
@timing_func # wrapping the function
def timer_testing(some_num_time):
"""Testing the timer decorator."""
for _ in range(some_num_time):
# creating a list of cubed numbers
# and adding them up
sum([i**3 for i in range(10000)])
This decorator is useful for checking which function is being called and the value being returned.
Click to the expand the code!
import math
import functools
def debug(func):
"""Print the function arguments and return value."""
@functools.wraps(func)
def debug_wrapper(*args, **kwargs):
# repr to get a list of the function arguments
args_repr = [repr(a) for a in args]
# repr to get a list of keyword arguments and their
# corresponding values
kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
signature = ", ".join(args_repr + kwargs_repr)
# Printing the function name and its arguments
print(f"Calling {func.__name__}({signature})")
value = func(*args, **kwargs)
# the function and their returned values
print(f"{func.__name__!r} returned {value!r}")
return value
return debug_wrapper
# initialising the debug wrapper
# for the math.factorial module.
math.factorial = debug(math.factorial)
# initialising the debug module
# for the exp approximation
@debug
def approximate_exp(num_terms=10):
"""Approximating exponential by adding
the inverse of n factorials."""
# returning the approximated exponential
return sum(1/math.factorial(num) for num in range(num_terms))
This decorator just repeats the function as many times as one wants.
Click to expand the code!
import functools
def repeat_func(_func=None, *, num_times=1):
"""Repeating a function once/any number of times.
This function works with or without the kwarg num_times.
"""
def repeat_decorator(func):
"""Wrapper function for repeating the function.
This will create function object for the repeat_func.
As we reserve the repeat_func for using it as a decorator,
and it requires arguments, this function is required."""
@functools.wraps(func)
def repeat_wrapper(*args, **kwargs):
# repeating function n number of times
# but returning only the function value once
for _ in range(num_times):
value = func(*args, **kwargs)
return value
return repeat_wrapper
# The parameter _func takes two values, either None, when there's
# num_times provided otherwise the decorator function being
# passed to _func.
if _func is None:
return repeat_decorator
else:
return repeat_decorator(_func)
@repeat_func
def say_hi_once():
"""Python says hi to you! 😀"""
print('Hello there! 😀')
return {'H': 0, 'e': 1, 'l': 2, 'l': 3, 'o': 4}
@repeat_func(num_times=3)
def say_hi_thrice():
"""Python says hi to you! 😀"""
print('Hello there! 😀')
return {'H': 0, 'e': 1, 'l': 2, 'l': 3, 'o': 4}
Calling the function say_hi_once()
and say_hi_thrice()
will yield the repeated print statements
but return only one time the function return statement
. If the return statement
were to be returned as many times as the repeated print statements
, then the code can be modified accordingly.
This uses the default cache of the Standard Library to store previously calculated results, which can be used to calculate future results. This type of problems can be solved with dynamic programming also, using a data structure such as heap to store the previously calculated results. For instance, to solve a fibonacci series one can perform the following:
Click to expand the code!
import functools
# usually the cache size is 128, however, we are setting it to 4 here
@functools.lru_cache(maxsize=4)
def fibonacci(num):
# print(f"Calculating fibonacci({num})")
if num < 2:
return num
return fibonacci(num - 1) + fibonacci(num - 2)