DMarlon Studio

Lesson 5 · 5 of 20

Decorators · Python

Add logging and permission checks to Python functions without touching them: decorators, decorators with arguments and functools.wraps.

Python · Decorators in DMarlon Studio: the example's code and its output in the terminal

A decorator is a function that takes a function and returns another one that wraps it. log_calls(func) defines wrapper, which calls the original, prints its name and result, and returns it. It is a closure: wrapper remembers func.

Writing @log_calls above a definition is the same as refund = log_calls(refund): from then on, the name refund points to the wrapper. @functools.wraps(func) copies the original name and docstring onto the wrapper; without it, refund.__name__ would be "wrapper".

require_role("admin") is a decorator with arguments: a function that returns the real decorator. That is why there are three levels: require_role takes the role, decorator takes the function and wrapper takes the arguments of each call. The wrapper checks user["roles"] and, if the role is missing, never calls the function at all.

Decorators stack, and the bottom one is applied first. refund is guarded by require_role, and the whole thing is wrapped by log_calls, so the log also records denied calls, as you can see with Luis.

This separates what a function does (work out a refund) from what many functions repeat (logging, permission checks, timing, retries).

Code · main.py

import functoolsdef log_calls(func):    @functools.wraps(func)          # keep the original name and docstring    def wrapper(*args):        result = func(*args)        print(f"log: {func.__name__} -> {result}")        return result    return wrapperdef require_role(role):             # a decorator with arguments    def decorator(func):        @functools.wraps(func)        def wrapper(user, *args):            if role not in user["roles"]:                return f"denied: {user['name']} is not {role}"            return func(user, *args)        return wrapper    return decorator@log_calls                          # decorators stack: bottom one first@require_role("admin")def refund(user, amount):    return f"{user['name']} refunded {amount:.2f} EUR"ana = {"name": "Ana", "roles": ["admin", "sales"]}luis = {"name": "Luis", "roles": ["sales"]}print(refund(ana, 30))print(refund(luis, 30))print("Name kept:", refund.__name__)

Output · python3 main.py

log: refund -> Ana refunded 30.00 EUR
Ana refunded 30.00 EUR
log: refund -> denied: Luis is not admin
denied: Luis is not admin
Name kept: refund

Key points

  • @deco above def f means f = deco(f).
  • The wrapper usually takes *args and **kwargs and returns the result.
  • Always use @functools.wraps(func) on the wrapper.
  • A decorator with arguments is a function that returns a decorator.
  • Stacked decorators are applied from the bottom up.

Exercise

Write an @uppercase decorator that turns the string returned by the function into capitals, and put it on refund above the other two.