Skip to content

Python Decorators

Links: 108 Python Index


Function Decorators

By definition, a decorator is a function that takes another function and extends the behaviour of the latter function without explicitly modifying it.

Decorators don't change anything inside the function they are decorating. They change and manipulate the return value of the function.

  • In Python, functions are first-class objects.
    • This means that functions can be passed around and used as arguments, just like any other object (string, int, float, list, and so on).
  • A decorator is a higher order function.
    • It takes an entity (a standalone function, a class function, or even a whole class) and returns a function or a class reference.
def say_hello(name):
    return f"Hello {name}"

def greet_bob(greeter_func):
    return greeter_func("Bob")

greet_bob(say_hello) # Hello Bob
  • In python we can define functions within other functions.

Simple decorators

from datetime import datetime

def not_during_the_night(func):
    def wrapper():
        if 7 <= datetime.now().hour < 22:
            func()
        else:
            pass  # Hush, the neighbors are asleep
    return wrapper

def say_whee():
    print("Whee!")

say_whee = not_during_the_night(say_whee)
say_whee()

@not_during_the_night
say_whee()
  • @ is just syntactic sugar.
  • The inner function (wrapper in the above example) can be named anything.

Passing argument to functions

def do_twice(func):
    def wrapper_do_twice(*args, **kwargs):
        func(*args, **kwargs)
        func(*args, **kwargs)
    return wrapper_do_twice

@do_twice
def greet(name):
    print(f"Hello {name}")

Returning values from decorators

def do_once(func):
    def wrapper_do_once(*args, **kwargs):
        return func(*args, **kwargs) # if I don't return from here then the print(return_value) will be None
    return wrapper_do_twice

@do_once
def greet(name):
    print(f"Hello {name}")
    return "test"

return_value = greet("function") # Hello function
print(return_value) # test

Decorated functions identity

def some_decorator(func):
    def wrapper():
        print("inside the some_decorator")
        func()
    return wrapper

@some_decorator
def learn_decorators():
    print("Inside learn decorators function")

learn_decorators()
print(learn_decorators.__name__)

# inside the some_decorator 
# Inside learn decorators function 
# The name of the function is wrapper
  • The function has forgotten its identity.
  • We can rectify this using functools
import functools

def some_decorator(func):
    @functools.wraps(func)
    def wrapper():
        print("inside the some_decorator")
        func()
    return wrapper

@some_decorator
def learn_decorators():
    print("Inside learn decorators function")

learn_decorators()
print("The name of the function is ", learn_decorators.__name__)

# inside the some_decorator 
# Inside learn decorators function 
# The name of the function is learn_decorators

Stacked decorators

def deco_1(original_func):
    def wrapper(*args, **kwargs):
        print(args, kwargs)
        print("Inside deco_1")
        original_func(*args, **kwargs)

    return wrapper

def deco_2(original_func):
    def wrapper(*args, **kwargs):
        print(args, kwargs)
        print("Inside deco_2")
        original_func(*args, **kwargs)

    return wrapper

@deco_1
@deco_2
def some_function(a: int, b: int) -> None:
    print(a * b)


some_function(4, 5)
  • These can be useful in performing some kind of validations on the input arguments there by separating the validation logic from the function.
    • Like checking if the input integer parameter is greater than 20 or not.
def check_int(original_func):
    def wrapper(*args, **kwargs):
        for argument in args:
            if not isinstance(argument, int):
                raise TypeError(f"{argument} is not of type Int")

        original_func(*args, **kwargs)

    return wrapper

def greater_than_20(original_func):
    def wrapper(*args, **kwargs):
        for argument in args:
            if argument < 20:
                raise ValueError(f"{argument} is not > 20")
        original_func(*args, **kwargs)

    return wrapper

@check_int
@greater_than_20
def some_function(a: int, b: int) -> None:
    print(a * b)

some_function(12, 40)

Using decorators for class methods

def check_int(original_func):
    def wrapper(self, *args, **kwargs):
        for argument in args:
            if not isinstance(argument, int):
                raise TypeError(f"{argument} is not of type Int")

        original_func(self, *args, **kwargs)

    return wrapper

def greater_than_20(original_func):
    def wrapper(self, *args, **kwargs): # notice the self argument
        for argument in args:
            if argument < 20:
                raise ValueError(f"{argument} is not > 20")
        original_func(self, *args, **kwargs) # notice the self argument

    return wrapper

class SimpleClass:
    @check_int
    @greater_than_20
    def __init__(self, a: int, b: int):
        self.a = a
        self.b = b

SimpleClass(10, 43)
# this will give a value error

Passing arguments to decorators

  • Nested functions (3)

Class Based Decorators

Decorators without arguments

class say_hi:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print("hello there")
        result = self.func(*args, **kwargs)
        return result

@say_hi
def some_function(a: int, b: int) -> int:
    return a * b

print(some_function(a=10, b=20))

# hello there
# 200

Decorators with arguments

  • Always use this. Even if you are not passing arguments.
    • Using the above example causes typing issues.
    • You can use the decorator as @some_decorator()
      import functools
      
      class say_hi:
          def __init__(
              self,
              *decorator_args,
              **decorator_kwargs,
          ):
              self.decorator_args = decorator_args
              self.decorator_kwargs = decorator_kwargs
      
          def __call__(self, original_func):
              @functools.wraps(original_func)
              def inner_function(*original_args, **original_kwargs):
                  print("hello there")
                  print(self.decorator_args)
                  print(self.decorator_kwargs)
                  result = original_func(*original_args, **original_kwargs)
                  return result
      
              return inner_function
      
      @say_hi(age=16, value=34)
      def some_function(a: int, b: int) -> int:
          return a * b
      
      print(some_function(a=10, b=20))
      print(some_function.__name__)
      
      # hello there
      # ()
      # {'age': 16, 'value': 34}
      # 200
      # some_function
      
The arguments shift down one level.

Summary

attachments/Pasted image 20221121162725.jpg

Use Cases

  • Caching
  • Timing

References


Last updated: 2022-11-21