Decorators in Python
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:
- functools.lru_cache
- property
- staticmethod
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:
- marking that a function that is being called is deprecated
- counting how many times the function was called
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.