Decorator

Page content
  • A decorator is a function that takes another function as an argument, adds some functionality, and returns another function without altering the source code.
  • This is also called metaprogramming because a part of the program tries to modify another part of the program at compile time.
  • Decorators can serve to
    • shorten code
    • speed up code
    • change the way that codes acts

1. Function decorator

  • Function decorators are more common than class decorators.

  • If there is more than one decorator for a function, the order of the decorators is important.

def make_pretty(func): # decorator acts as a wrapper
  def inner():
    print("I got decorated")
    func()
  return inner

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

pretty = make_pretty(ordinary)
pretty() # I got decorated I am ordinary

## 
@make_pretty
def ordinary():
  print("I am ordinary")

# equals to

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

ordinary = make_pretty(ordinary)
##

Chaining decorators in python

def star(func):
  def inner(*args, **kwargs):
    print("*" * 30)
    func(*args, **kwargs)
    print("*" * 30)

def percent(func):
  # i.e. we don't know the arguments that func will take
  def inner(*args, **kwargs):
    print("%" * 30)
    func(*args, **kwargs)
    print("%" * 30)

@star
@percent
def printer(msg):
  print(msg)

printer("Hello") # printer = star(percent(printer))

Stateful decorators

  • We can use a decorator to keep track of the state.
"""
Counts the times for function calling
Output:
  Call 1 of 'say'
  Hello!
  Call 2 of 'say'
  Hello!
  2
"""
from functools import wraps

def count_calls(func):

  @wraps
  def wrapper(*args, **kwargs):
    wrapper.num_calls += 1
    print(f"Call {wrapper.num_calls} of {func.__name__!r}")
    return func(*args, **kwargs)

  wrapper.num_calls = 0
  return wrapper

@count_calls
def say():
  print("Hello!")

say()
say()
print(say.num_calls)

2. Class decorator


def decorator_function(original_function):
  def wrapper_function(*args, **kwargs):
    print('wrapper executed this before {}'.format(original_function.__name__))
    return original_function(*args, **kwargs)
  return wrapper_function

##

class decorator_class(object):

  def __init__(self, original_function):
    self.original_function = original_function

  def __call__(self, *args, **kwargs):
    print('call method executed this before {}'.format(original_function.__name__))
    return self.original_function(*args, **kwargs)


@decorator_class
def display():
  print('display function ran')


@decorator_class
def display_info(name, age):
  print('display_info ran with arguments ({}, {})'.format(name, age))


display_info('John', 25)

display()

classmethod

<class_method>(cls, <parameter>, <parameter>)

  • @classmethod decorator marks a method as a class method.
  • It takes the cls parameter that points to the class, not the object instance.
  • It can’t modify the object instance state.
  • Class methods are called via the Class containing it, rather than from an instance.
  • The cls parameter allows @classmethod to easily instantiate the class.
class ATM(Bank):
    def __init__(self, balance):
        super().__init__(balance)

class Bank():
  def __init__(self, balance):
    self.balance = balance
  
  @classmethod
  def find_interest_class_method(cls, load_money):
    return loan_money * 1.4

atm = ATM(1000)
Bank.find_interest_classmethod(atm.balance))
class Pizza:
    def __init__(self, ingredients):
        self.ingredients = ingredients

    def __repr__(self):
        return f'Pizza({self.ingredients!r})'

    @classmethod
    def margherita(cls):
        return cls(['mozzarella', 'tomatoes'])

    @classmethod
    def prosciutto(cls):
        return cls(['mozzarella', 'tomatoes', 'ham'])

Pizza.margherita() # Pizza(['mozzarella', 'tomatoes'])

Examples

# division
"""
It is going to divide 2 and 5
0.4
"""

def smart_divide(func):
    def inner(a, b):
        print(f"It is going to divide {a} and {b}")
        if b == 0:
            print("{b} is equal to 0.")
            return
        return func(a, b)
    return inner

@smart_divide
def divide(a, b):
    print(a/b)

if __name__ == "__main__":
    divide(2,5)
# print stars
"""
******************************
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
Hello
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
******************************
"""

def star(func):
    def inner(*args, **kwargs):
        print("*" * 30)
        func(*args, **kwargs)
        print("*" * 30)
    return inner


def percent(func):
    def inner(*args, **kwargs):
        print("%" * 30)
        func(*args, **kwargs)
        print("%" * 30)
    return inner


@star
@percent
def printer(msg):
    print(msg)


printer("Hello")

@property

  • It is used to customize getters, setters, and deleters for class attributes.
"""
Output:
  Setting value...
  Getting value...
  37
  Getting value...
  98.60000000000001
"""
class Celsius:
    def __init__(self, temperature=0):
        self.temperature = temperature

    def to_fahrenheit(self):
        return (self.temperature * 1.8) + 32

    @property
    def temperature(self):
        print("Getting value...")
        return self._temperature

    @temperature.setter
    def temperature(self, value):
        print("Setting value...")
        if value < -273.15:
            raise ValueError("Temperature below -273 is not possible")
        self._temperature = value

human = Celsius(37)
print(human.temperature)
print(human.to_fahrenheit())

register

  • Register a function as a handler for an event.
  • It allows two or more subsystems to communicate without knowing much about each other, decoupling design.
from atexit import register

@register
def goodbye():
    print('You are now leaving the Python sector.')

staticmethod

  • It can be called both a class instance and from a class without instantiating the class first.
"""
@staticmethod
def <function_name>():
    ...
"""

class Student():
    def __init__(self, mark):
        self.mark = mark
    
    @staticmethod
    def find_min(mark):
        return min(mark, 100)

print(Student.find_min(20))

wrapper

"""
wrapper executed this before display_info
display_info ran with arguments (John, 25)
wrapper executed this before the display
display function ran
"""

def decorator_function(original_function):
  def wrapper_function(*args, **kwargs):
    print(f"wrapper executed this before {original_function.__name__}")
    return original_function(*args, **kwargs)
  return wrapper_function


@decorator_function
def display():
  print('display function ran')


@decorator_function
def display_info(name, age):
  print(f"display_info ran with arguments ({name}, {age})")


display_info('John', 25)

display()