Legends ofPythos
Claim your name
Pythonic Python

Decorators

Lesson 7 of 12

Watch the lesson1:05 · with Sigrid
A decorator is a function that takes another function and returns a new one. You use it to add behaviour—like logging or timing—to existing functions without modifying their code.

The basic structure

def my_decorator(func):
    def wrapper():
        print('Before')
        func()
        print('After')
    return wrapper

@my_decorator
def say_hi():
    print('Hi!')

say_hi()
@my_decorator is shorthand for: say_hi = my_decorator(say_hi)
The wrapper function runs the original code, plus your extra logic. The @ syntax simply reassigns the name to point at the wrapper.

Working with any arguments

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print('Before')
        result = func(*args, **kwargs)
        print('After')
        return result
    return wrapper
*args and **kwargs let the decorator handle any function signature.

Preserving metadata

import functools

def my_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper
@functools.wraps copies the original function's name and docstring to the wrapper.
Without @functools.wraps, reading __name__ on a decorated function would give 'wrapper'. With it, you get back the real name. This matters for debugging and introspection tools.

Keeping state on the wrapper

import functools

def counted(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        wrapper.calls += 1        # the wrapper can see itself
        return func(*args, **kwargs)
    wrapper.calls = 0             # set the starting value once
    return wrapper

@counted
def ping():
    return 'pong'

ping()
ping()
print(ping.calls)
# 2
A function is an object, so you can hang a value on it.
A function in Python is an ordinary object, so you can attach attributes to it. Set the starting value once, after wrapper is defined but before you return it, and update it inside the wrapper. Because the decorated name now points at wrapper, reading ping.calls from outside reads that same attribute.

Your turn

0 of 3 solved

Exercise 1

+45 XP
Write a decorator named hello that prints 'Hello' before calling the decorated function, and then returns whatever that function returned. Apply it to the provided greet function using @hello so that greet() still returns its original value, then call greet() once so 'Hello' is printed.
def hello(func):
    pass




@hello
def greet():
    return "World"

Run your code to check it against the tests.

Exercise 2

+45 XP
Write a decorator called counter that counts how many times the function it wraps has been called, storing the number on the wrapper as .call_count. Apply it to add, then call add twice.
import functools




def counter(func):
    pass

Run your code to check it against the tests.

Exercise 3

+45 XP
Write a decorator called timer that measures how long the function it wraps takes and prints one line in the form Took 0.00s — the word Took, the number of seconds with exactly two decimal places, then the letter s. Use time.time() before and after the call. Apply it to the provided slow_sum(n), and call slow_sum(50) so the timing line is printed once. The decorated function must still return its total.
import time


def slow_sum(n):
    total = 0
    for i in range(1, n + 1):
        total += i
    return total

Run your code to check it against the tests.