Effectively using python decorators and function overloading

Ankush Kumar Singh
Nerd For Tech

--

Decorators is one of the most used features in python. In this blog we will discuss,

  • decorators
  • how python executes decorators
  • various ways of defining decorators
  • singledispatch which is a decorator used for function overloading defined in the python standard library.

In this blog, we will use the example of baking a pizza to understand the concepts mentioned above.

Decorator Basics

A decorator is a callable which works as a wrapper, taking a function as an argument and perform some processing with the decorated function.

Sample output

Just adding more cheese here...
Just baking pizza here...
And Done!

In the code above, we have nested functions adding_cheese and inner_function. Function adding_cheese takes a function as an argument and returns inner_function. In inner_function, it executes print statements before and after calling function passed as argument to adding_cheese. Adding @adding_cheese on top of function baking_pizza, decorates baking_pizza with adding_cheese in other words adding_cheese is decorator and baking_pizza is decorated function.

As you can see in the sample output, we see print statements which are defined in both baking_pizza and inner_function.

Decorators Execution

It is interesting to see how decorators are executed by python. Following is a program to demonstrate that.

Sample Output

Adding topping <function adding_olives at 0x10c9cad08>...
Adding topping <function adding_onions at 0x10c9cad90>...
Adding topping <function adding_tomatoes at 0x10c9cae18>...
Stated adding toppings
Toppings added so far [<function adding_olives at 0x10c9cad08>, <function adding_onions at 0x10c9cad90>, <function adding_tomatoes at 0x10c9cae18>]
Added Olives...
Added Onions...
Added tomatoes...
This is getting out of hand now. GET OUT!!!

In the code above, decorator adding_toppings adds decorated functions to list toppings. In main function we print contents of list toppings and call functions adding_olives, adding_onions, adding_tomatoes and adding_apple. Function adding_toppings prints which function is getting decorated and then added to the list toppings.

Sample output shows an interesting outcome. Decorators are run as soon as the module is loaded while decorated functions run when they are invoked. In sample output first three lines are for adding_toppings decorator execution that is added on top of functions adding_olives, adding_onions and adding_tomatoes. It’s interesting to see that toppings doesn’t list function adding_apple, that’s because function adding_apple is not decorated with @adding_toppings.

Stacked Decorators

We can stack multiple decorators to a function. Let’s recall the example of adding cheese to pizza that was used to introduce the decorators. In function baking_pizza let’s add two stacked decorators instead of one.

Sample Output

Just adding more cheese here...
Just adding some more cheese here...
Just baking pizza here...
And Done!

When two or more decorators are added to a function, outer decorator is called first followed by other stacked decorators. From the sample output we can see that outer decorator adding_cheese is called first, followed by second decorator, adding_more_cheese.

Function Parameter And Return Value

Let’s consider the case of decorated function which takes function parameters and returns a value.

Sample Output

Just adding more cheese here...
Blah Blah baking pizza here...
And done!

In the code sample above, function baking_pizza takes name as the argument and returns a string. To handle function parameter we use args and kwargs to accept input arguments in input_function. Also, inner_function returns variable ret_val after the function execution which returns the value return by the function baking_pizza.

Decorator In A Class

Let’s consider when decorator is defined inside a class,

Sample Output

Just adding more cheese here...
Blah Blah baking pizza here...
And Done!

In the code sample above, class pizza has a decorator function adding_cheese which decorates function baking_pizza. In this case, we decorate using @pizza.adding_cheese.

Parametrized Decorator

Parameter in a decorator gives programmer more flexibility in using decorators effectively based on the conditions.

Sample output

Baking supreme_veggie_pizza pizza with thin crust
Just baking pizza here...
And Done!
Baking farmhouse_pizza pizza with normal crust
Just baking pizza here...
And Done!

In the sample code above, decorator pizza_crust takes crust as parameter. In function supreme_veggie_pizza decorator is set with crust ‘thin’, while in function farmhouse_pizza decorator is called with no arguments so decorator picks default value of crust i.e. ‘normal’.

Decorator Demo

With all the basic information about the decorators let’s try to implement a function that will calculate the maximum discount that a user can get based on the pizza order. This will illustrate how we can take advantage of decorators in a program.

Sample output

discount = 0.2

In the sample code above, order_dict mocks the input from the user ordering pizza. Functions seasonal_discount, buy_2_pizza and new_customer read order_dict to determine if the discount is applicable. At any point if we have to remove the discount for a category then just removing decorator from the function. Thus, with minimum change we can change add/remove a function from the program execution.

Function Overloading Using Singledispatch

Python defines a few decorators in standard library like property, staticmethod, classmethod, lru_cache, singledispatch etc. In this section we will take an example of singledispatch which is used to override functions. Following is an example program in which we override get_cost function to return cost of different crust types.

Sample output

thin_crust_cost = 25

In the sample code above, we have defined three dataclasses ThinCrust, NormalCrust and ThickCrust. Function get_cost is decorated with singledispatch and is overridden by functions decorated with @get_cost.register. Depending on the object type in get_cost function call, cost returned will be different. This is a good way to avoid multiple if/elseif/else conditions.

I hope this was useful to you. We can expand this knowledge in understanding factory class pattern in python, which I will cover in future blogs. Thank you for reading the blog.

--

--