🎓 A1Learner
🔍

Python Decorator Patterns

Decorator patterns are reusable wrappers for common jobs.

Time a call

The variable elapsed holds the seconds between the two clock reads. The code does not print elapsed.

import time


def timed(func):
    def wrapper():
        start = time.perf_counter()
        result = func()
        elapsed = time.perf_counter() - start
        print("Finished:", func.__name__)
        return result
    return wrapper

Log each call

A log is a written record of what happened. This wrapper prints one line before the call and one after it.

from functools import wraps


def logged(func):
    @wraps(func)
    def wrapper(name):
        print("Calling:", func.__name__)
        result = func(name)
        print("Done:", func.__name__)
        return result
    return wrapper


@logged
def greet(name):
    return "Hello, " + name


print(greet("Ada"))

Retry after a failure

Some jobs fail once and then work. The counter tries counts each failure, and the loop stops after three.

def retry(func):
    def wrapper():
        tries = 0
        while tries < 3:
            try:
                return func()
            except ValueError:
                tries += 1
                print("Try again")
        raise ValueError("Still failing")
    return wrapper

Stack two decorators

Python applies a stack from the bottom up. The decorator nearest def runs first, then the one above it.

def add_a(func):
    def wrapper():
        return "A" + func()
    return wrapper


def add_b(func):
    def wrapper():
        return "B" + func()
    return wrapper


@add_a
@add_b
def base():
    return "x"


print(base())

Example

from functools import wraps


def retry(func):
    @wraps(func)
    def wrapper():
        tries = 0
        while tries < 3:
            try:
                return func()
            except ValueError:
                tries += 1
                print("Failed, tries so far:", tries)
        raise ValueError("Still failing after 3 tries")
    return wrapper


calls = 0


@retry
def load_report():
    global calls
    calls += 1
    if calls < 3:
        raise ValueError("Not ready yet")
    return "Weather report"


print(load_report())
print("Calls made:", calls)

Key points

  • Timing decorator: a wrapper that measures how long a call takes
  • Use time.perf_counter() to read a precise clock before and after a call
  • Logging decorator: a wrapper that prints before and after each call
  • Retry decorator: a wrapper that calls the function again after a failure
  • Counter: a variable that counts tries, so the wrapper can stop
  • Stacking: with two decorators, the one nearest the function applies first

In this example, retry wraps load_report. The variable calls counts every real try. The first call raises ValueError. The wrapper prints the try count. It then calls load_report again. The second call also raises ValueError. The wrapper prints again and tries once more. The third call returns the report text. The wrapper hands that text back unchanged. The last line proves the function ran three times.

Note

A measured time changes on every run.

Only the shape of a timing result is fixed, never its seconds.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Python Playground

Exercise

Two decorators sit above a function. Which one is applied first?