Python's decorators are a significant part of the language, and one of the features that made me like Python more. A decorator is a function or a class that extends the behavior of the another function or class without the need to explicitly modify it. Decorators act as wrappers to other existing services. This is perfect for many reasons, and keeping our code clean is one of them.
What makes a decorator act this way is that functions are treated as first-class objects in Python, which basically means that functions can be passed around as arguments just like any other value, can be defined inside of other functions, and can be returned from other functions.
Let's take a quick example first to understand how decorators work.
Using Python3 for the examples.
def my_decorator(function_parameter): def function_wrapper(): print("This will be called before the function execution.") function_parameter() return function_wrapper @my_decorator def hello_world(): print("Hello, World!") hello_world() # This will be called before the function execution. # Hello, World!
We start by defining a function that will act as the decorator; naming it
my_decorator, a very original name by the way. The decorator function accepts a function parameter
function_parameter that will be the function that the decorator will extend. The in the body of the decorator, we define a function wrapper that will wrap the functionality of the decorator, we name it
function_wrapper, again, very original. The feature basically is just printing a text before the function execution.
Finally, we define a function,
hello_world, that the decorator will extend. Now most likely, the function will be already defined in the real case, and the decorator will come to prolong the functionality. Let's take a real world example that I faced before.
I had a function that sends a request to an external API, and that API was unstable for some reason and returning
503 Service Unavailable sometimes. I had to send the request two or three times repeatedly to get the results back. Now I could obviously modify that function to send the request if I get that HTTP status back, but we already know that is bad coding, especially if we had multiple functions to change. And so the decorator helped in this case where I created a general one that will retry depending on a defined error and another one that
extends the previous to send the request when getting the
503 Service Unavailable.
I am creating the decorator as a class this time to show the power of it.
# The standard retry decorator. class Retry: MAX_TRIES = 5 def is_valid(self, response): # By default, the 200 code status is the only accepted one, # thus only retry if a status code is not 200. return response.status_code == 200 def __call__(self, func): def retried_func(*args, **kwargs): tries = 0 while True: response = func(*args, **kwargs) if self.is_valid(response) or tries >= self.MAX_TRIES: break tries += 1 return response return retried_func # Retry class that will recall the API class RetryUnstableAPICall(Retry): def is_valid(self, response): return not response['code'] == 503 retry_unstable_api_request = RetryUnstableAPICall() @retry_unstable_api_request def send_request(): #... pass
Retry class is the one that will act as the retry base decorator, where
MAX_TRIES is the number of the tries that the request should be sent until it fails. Then the
is_valid() function is the one that will validate the response according to the HTTP response code. The function call operator,
__call__, will keep executing the function as long as the
is_valid() condition is not fulfilled.
RetryUnstableAPICall is just extending the
Retry class and redefining the
is_valid() function with a new condition that fit my needs for retrying when the HTTP status is
503 Service Unavailable.