The boilerplate without parameters
A decorator is a higher-order function that takes a function and returns a new function. It is used to modify the behavior of a function or a method. Decorators are used to add functionality to an existing function without modifying its structure.
This is the pattern used by decorators in Python. The @decorator
syntax is just a shorthand for calling decorator
with the decorated function as an argument.
import functools
def decorator(func): @functools.wraps(func) # Preserve info about the original function. def wrapper(*args, **kwargs): # Do something before. value = func(*args, **kwargs) # Do something after. return value return wrapper
πΆβπ«οΈ Without the decorator sugar:
- This way of decorating may not seem inmediately useful, but it is useful when applied to functions that you donβt call directly yourself.
def sum_a_b(a: int, b: int) -> int: return a + b
decorator(my_func)(a=2, b=3)
π With the decorator syntax:
@decoratordef sum_a_b(a: int, b: int) -> int: return a + b
my_func(a=2, b=3)
The boilerplate with parameters π
This is the more generic use case for decorators.
- If youβve called
@decorator_handler
without arguments, then the decorated function will be passed in as _func. - If youβve called it with arguments, then _func will be None, and some of the keyword arguments may have been changed from their default values.
- The asterisk (
*
) in the argument list means that you canβt call the remaining arguments as positional arguments. _func
is a positional argument.
import functools
def decorator_handler(_func=None, *, arg1=None, arg2=None): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): # Do something before. # Use arg1, arg2, or more... value = func(*args, **kwargs) # Do something after. return value return wrapper
if _func is None: # Decorated func WITH arguments. # @decorator_handler(repeat=2) return decorator else: # Decorated func WITHOUT arguments. # @decorator_handler return decorator(_func)
Example
import functools
def decorator_handler(_func=None, *, repeat=1): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): print(f"\n{func.__name__}; {repeat = }; {_func = }") for _ in range(repeat): value = func(*args, **kwargs) print(value) return value return wrapper
if _func is None: # Decorated func without arguments. return decorator else: # Decorated func with arguments. return decorator(_func)
def echo(s: str) -> str: return f"echo: {s} {s}"
@decorator_handler # _func decorated_echo_nonedef decorated_echo_none(s: str) -> str: return echo(s)
@decorator_handler() # _func Nonedef decorated_echo_1(s: str) -> str: return echo(s)
@decorator_handler(repeat=2) # _func Nonedef decorated_echo_2(s: str) -> str: return echo(s)
if __name__ == "__main__": # Decorating explicitly the echo function: # @decorator_handler decorator_handler(echo)("hola") # @decorator_handler() decorator_handler()(echo)("hola") # @decorator_handler(repeat=2) decorator_handler(repeat=2)(echo)("hola")
# Calling decorated functions: # @decorator_handler decorated_echo_none("bye") # @decorator_handler() decorated_echo_1("bye") # @decorator_handler(repeat=2) decorated_echo_2("bye")
Output:
echo; repeat = 1; _func = <function echo at 0x1041f6660>echo: hola hola
echo; repeat = 1; _func = Noneecho: hola hola
echo; repeat = 2; _func = Noneecho: hola holaecho: hola hola
decorated_echo_none; repeat = 1; _func = <function decorated_echo_none at 0x1041f7380>echo: bye bye
decorated_echo_1; repeat = 1; _func = Noneecho: bye bye
decorated_echo_2; repeat = 2; _func = Noneecho: bye byeecho: bye bye
Measuring time
Given the following problem:
Write a function format_number that takes a non-negative number as its only parameter. Your function should convert the number to a string and add commas as a thousand separators. For example, calling format number(1000000) should return β1,000,000β
Implement a HOF that measures the time performance and run a profiler to check the performance of the function:
import functoolsimport timeimport cProfile
def performance(func): @functools.wraps(func) def wrapper(*args, **kwargs): """Wrapper function""" func_case = kwargs.get("func_case") print(f"{50*'='} Performance Profile: {func_case} {50*'='}") start = time.perf_counter() result = func(*args, **kwargs) cProfile.run(f"{func(*args, **kwargs)}") end = time.perf_counter() elapsed = end - start print(f"Time performance: {elapsed:.5f} seconds") print(f"{50*'='} End Perfor. Profile: {func_case} {50*'='}\n") return result return wrapper
Solving the problem:
@performancedef format_number(number: int) -> str: """2s approach: backwards.""" result = [] number_list = list(str(number)) count = 0 for item in reversed(number_list): if count == GROUP_LENGTH: count = 0 result.append(",") result.append(item) count += 1 return "".join(reversed(result))
Adding tests:
from main import format_number
def test_default_format(): assert format_number(100000000) == "100,000,000" assert format_number(10000000) == "10,000,000" assert format_number(1000000) == "1,000,000" assert format_number(1000) == "1,000" assert format_number(100) == "100"
Slowing down code
import functoolsimport time
def slow_down(func): """Sleep 1 second before calling the function""" @functools.wraps(func) def wrapper_slow_down(*args, **kwargs): time.sleep(1) return func(*args, **kwargs) return wrapper_slow_down
@slow_downdef countdown(from_number): if from_number < 1: print("Done!") else: print(from_number) countdown(from_number - 1)
Output:
>>> countdown(3)321Done!
Registering Plugins
Decorator:
PLUGINS = dict()
def register(func): """Register a function as a plug-in""" PLUGINS[func.__name__] = func return func
Main:
from decorators import register, PLUGINS
@registerdef say_hello(name): return f"Hello {name}"
@registerdef be_awesome(name): return f"Yo {name}, together we're the awesomest!"
Output:
>>> PLUGINS{'say_hello': <function say_hello at 0x7f768eae6730>, 'be_awesome': <function be_awesome at 0x7f768eae67b8>}