Python Decorators

Definition
A decorator takes in a function, adds some functionality and returns it. 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.

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.

def make_pretty(func):
    def inner():
        print("I got decorated")
        func()
    return inner
def ordinary():
    print("I am odinary")

>>> ordinary()
I am ordinary
>>> # let's decorate this ordinary function
>>> pretty = make_pretty(ordinary)
>>> pretty()
I got decorated
I am ordinary

In the example shown above, make_pretty() is a decorator.
@make_pretty
def ordinary():
    print("I am ordinary")

is equivalent to

def ordinary():
    print("I am ordinary")
ordinary = make_pretty(ordinary)

Decorating Functions with Parameters:
def smart_divide(func):
   def inner(a,b):
      print("I am going to divide",a,"and",b)
      if b == 0:
         print("Whoops! cannot divide")
         return
      return func(a,b)
   return inner
@smart_divide
def divide(a,b):
    return a/b

>>> divide(2,5)
I am going to divide 2 and 5
0.4
>>> divide(2,0)
I am going to divide 2 and 0
Whoops! cannot divide

A keen observer will notice that parameters of the nested inner() function inside the decorator is same as the parameters of functions it decorates. Taking this into account, now we can make general decorators that work with any number of parameter.

In Python, this magic is done as function(*args, **kwargs). In this way, args will be the tuple of positional arguments and kwargs will be the dictionary of keyword arguments. An example of such decorator will be.

def works_for_all(func):
    def inner(*args, **kwargs):
        print("I can decorate any function")
        return func(*args, **kwargs)
    return inner


Use Cases
1. Analytics, logging, and instrumentation
from myapp.log import logger
def log_order_event(func):
    def wrapper(*args, **kwargs):
        logger.info("Ordering: %s", func.__name__)
        order = func(*args, **kwargs)
        logger.debug("Order result: %s", order.result)
        return order
    return wrapper

@log_order_event
def order_pizza(*toppings):
    # let's get some pizza!


2. Validation and runtime checks
Imagine this: you have a set of functions, each returning a dictionary, which (among other fields) includes a field called "summary." The value of this summary must not be more than 80 characters long; if violated, that’s an error. Here is a decorator that raises a ValueError if that happens:

def validate_summary(func):
    def wrapper(*args, **kwargs):
        data = func(*args, **kwargs)
        if len(data["summary"]) > 80:
            raise ValueError("Summary too long")
        return data
    return wrapper

@validate_summary
def fetch_customer_data():
    # ...

@validate_summary
def query_orders(criteria):
    # ...

@validate_summary
def create_invoice(params):
    # ...



References
https://www.programiz.com/python-programming/decorator
https://www.oreilly.com/ideas/5-reasons-you-need-to-learn-to-write-python-decorators


Comments

Popular posts from this blog

Python Closures

Python Higher Order Functions