Introduction
Python has an interesting feature called decorators to add functionality to an existing code. This is also called metaprogramming as a part of the program tries to modify another part of the program at compile time. Even though it is the same underlying concept, we have two different kinds of decorators in Python
- Function decorators
- Class decorators
In Decorators, functions are taken as the argument into another function and then called inside the wrapper function. Everything in Python are objects. Names that we define are simply identifiers bound to these objects. Functions are no exceptions, they are objects too (with attributes). Various different names can be bound to the same function object.
#1 Assigning Functions to Variables def first(msg): print(msg) first("Hello") second = first second("Hello") #2 Functions as Parameters def inc(x): return x + 1 def dec(x): return x - 1 def operate(func, x): result = func(x) return result # Invoking function operate(inc,3) operate(dec,3) #3 Function returning another function def is_called(): def is_returned(): print("Hello") return is_returned new = is_called() new() #4 Functions inside Functions def parent(): print("Parent() function") def first_child(): print("first_child() function") def second_child(): print("second_child() function") second_child() first_child()
With these prerequisites out of the way, coming back to decorator. Basically, a decorator takes in a function, adds some functionality and returns it.
def make_pretty(func): def inner(): print("I got decorated") func() return inner def ordinary(): print("I am ordinary") ordinary() # Decorate this ordinary function pretty = make_pretty(ordinary) pretty()
In the example shown above, make_pretty()
is a decorator. The function ordinary()
got decorated and the returned function was given the name pretty. We can see that the decorator function added some new functionality to the original function. This is similar to packing a gift. The decorator acts as a wrapper. The nature of the object that got decorated (actual gift inside) does not alter. But now, it looks pretty (since it got decorated).
Generally, we decorate a function and reassign it as,
ordinary = make_pretty(ordinary).
This is a common construct and for this reason, Python has a syntax to simplify this. We can use the @ symbol along with the name of the decorator function and place it above the definition of the function to be decorated. For example,
@make_pretty def ordinary(): print("I am ordinary") # Above is equivalent to def ordinary(): print("I am ordinary") ordinary = make_pretty(ordinary)
Multiple Decorators
We can use multiple decorators to a single function. However, the decorators will be applied in the order that we’ve called them. Below we’ll define two decorator, and then apply these decorator to a single function.
def uppercase_decorator(function): def wrapper(): func = function() make_uppercase = func.upper() return make_uppercase return wrapper def split_string(function): def wrapper(): func = function() splitted_string = func.split() return splitted_string return wrapper @split_string @uppercase_decorator def say_hi(): return 'hello there' print(say_hi())
Accepting Arguments
Arguments passing to a decorator can be achieved by passing the arguments to the wrapper function. The arguments will then be passed to the function that is being decorated at call time.
def decorator_with_arguments(function): def wrapper_accepting_arguments(arg1, arg2): print("My arguments are: {0}, {1}".format(arg1,arg2)) function(arg1, arg2) return wrapper_accepting_arguments @decorator_with_arguments def cities(city_one, city_two): print("Cities I love are {0} and {1}".format(city_one, city_two)) cities("Nairobi", "Accra")
General Purpose Decorators
To define a general purpose decorator that can be applied to any function we use args
and **kwargs
. args
and **kwargs
collect all positional and keyword arguments (arguments are passed using keywords) and stores them in the args and kwargs variables. args
and kwargs
allow us to pass as many arguments as we would like during function calls.
def a_decorator_passing_arbitrary_arguments(function_to_decorate): def a_wrapper_accepting_arbitrary_arguments(*args,**kwargs): print('Positional arguments', args) print('Keyword arguments', kwargs) function_to_decorate(*args) return a_wrapper_accepting_arbitrary_arguments @a_decorator_passing_arbitrary_arguments def function_with_no_argument(): print("Arguments here.") @a_decorator_passing_arbitrary_arguments def function_with_arguments(a, b, c): print(a, b, c) function_with_arguments(1,2,3) @a_decorator_passing_arbitrary_arguments def function_with_keyword_arguments(): print("This has shown keyword arguments") function_with_keyword_arguments(first_name="Hello", last_name="Welcome")
Returning Values
To return value from decorator, you need to make sure the wrapper function returns the return value of the decorated function.
def do_twice(func): def wrapper_do_twice(*args, **kwargs): func(*args, **kwargs) return func(*args, **kwargs) return wrapper_do_twice @do_twice def return_greeting(name): print("Creating greeting") return f"Hi {name}" print(return_greeting("Welcome"))
Class as Decorator
We can define a decorator as a class, we have to introduce the __call__ method of classes. We mentioned already that a decorator is simply a callable object that takes a function as an input parameter. A function is a callable object. A callable object is an object which can be used and behaves like a function but might not be a function. It is possible to define classes in a way that the instances will be callable objects. The __call__ method is called, if the instance is called “like a function”, i.e. using brackets.
class A: def __init__(self): print("An instance of A was initialized") def __call__(self, *args, **kwargs): print("Arguments are:", args, kwargs) x = A() x(3, 4, x=11, y=10) # Output An instance of A was initialized Arguments are: (3, 4) {'x': 11, 'y': 10}
Decorator implemented as a class
class decorator2: def __init__(self, f): self.f = f def __call__(self): print("Decorating", self.f.__name__) self.f() @decorator2 def foo(): print("inside foo()") foo()