Decorators in Python

code

Decorators

Decorators are used to modify or extend behaviour of a function (or a class). One of the commonly used decorators that can be found in standard library is functools.cache which can be used to cache returned values from a function.

Example 0: Using cache decorator

from functools import cache


@cache
def add_numbers(a: int, b: int) -> int:
    print(f"summing {a} + {b}")
    return a + b


add_numbers(5, 7)
add_numbers(5, 7)  # add_numbers won't actually be called here - because result is cached

# Output: summing 5 + 7

Decorators are just syntactic sugar - the @ syntax is a shorthand for passing the decorated function into the decorator and getting a different (modified) function as a response.

Example 1: Using cache decorator - alternative syntax

from functools import cache


def add_numbers(a: int, b: int) -> int:
    print(f"summing {a} + {b}")
    return a + b


add_numbers = cache(add_numbers)


add_numbers(5, 7)
add_numbers(5, 7)  # add_numbers won't actually be called here - because result is cached

# Output: summing 5 + 7

Write your own decorator

Let’s write a simple timeit decorator that will take in a function, and print out how long the function call took. Notice that our decorator takes a function as an input and returns a different function as an output.

Example 2: timeit decorator

from functools import wraps
from typing import Callable, Any
import time


def time_it(f: Callable[[Any], Any]) -> Callable[[Any], Any]:
    @wraps(f)
    def wrap(*args, **kw):
        time_start = time.time()
        result = f(*args, **kw)
        time_end = time.time()

        print(f"function call took: {time_end - time_start} seconds")
        return result

    return wrap


@time_it
def sleep_for(seconds: int) -> None:
    time.sleep(seconds)


sleep_for(1)
sleep_for(3)
sleep_for(5)

# Output:
# function call took: 1.0010316371917725 seconds
# function call took: 3.00309419631958 seconds
# function call took: 5.005037546157837 seconds

Some commonly used decorators in standard library are:

Example 3: Use of @property and @staticmethod

"""@property and @staticmethod are builtins, you don't need to import them."""


class Color:

    red: int
    green: int
    blue: int

    def __init__(self, red: int, green: int, blue: int) -> None:
        self.red = red
        self.blue = blue
        self.green = green

    @staticmethod
    def clamp(x: int) -> int:
        return max(0, min(x, 255))

    @property
    def hex(self) -> str:
        return "#{0:02x}{1:02x}{2:02x}".format(self.clamp(self.red), self.clamp(self.green), self.clamp(self.blue))


white = Color(255, 255, 255)
print(white.hex)

# Output: #ffffff

Other examples

Other use cases for decorators include:

Example 4: deprecated decorator

from functools import wraps
from typing import Callable, Any


def deprecated(f: Callable[..., Any]) -> Callable[..., Any]:
    @wraps(f)
    def wrap(*args, **kw):
        print(f"function {f.__name__} has been deprecated.")
        result = f(*args, **kw)

        return result

    return wrap


@deprecated
def add(a: int, b: int) -> int:
    return a + b


add(4, 3)
# Output: function add has been deprecated.