Master

Metaprogramming and Decorators

Write code that manipulates code with decorators, closures, and metaclasses.

What you will see

9 9 16

About 2 min of reading

Try It

Wrap functions with decorators to add behaviour without changing their bodies. Each step defines a decorator, applies it, and prints the result.

1

Write a decorator that counts calls

def counter(func): def wrapper(*args, **kwargs): wrapper.calls += 1 return func(*args, **kwargs) wrapper.calls = 0 return wrapper @counter def ping(): return "pong" ping() ping() print(ping.calls)

Before you run it, say out loud what it will print. Then open the answer.

What it prints
pong pong 2
2

Pass arguments to a decorator with parentheses

def times(n): def decorator(func): def wrapper(*a, **k): for _ in range(n): result = func(*a, **k) return result return wrapper return decorator @times(2) def shout(word): print(word) shout("hi")

Before you run it, say out loud what it will print. Then open the answer.

What it prints
hi hi
3

Chain two decorators on one function

def loud(func): def wrapper(*a, **k): print("start") result = func(*a, **k) print("end") return result return wrapper def greet(func): def wrapper(*a, **k): result = func(*a, **k) return result.upper() return wrapper @greet @loud def hi(): return "hey" hi()

Before you run it, say out loud what it will print. Then open the answer.

What it prints
start hey end
4

Preserve metadata with functools.wraps

from functools import wraps def deco(func): @wraps(func) def wrapper(*a, **k): return func(*a, **k) return wrapper @deco def greet(): """greet someone" return "hello" print(greet()) print(greet.__name__) print(greet.__doc__)

Before you run it, say out loud what it will print. Then open the answer.

What it prints
hello greet greet someone
5

Build a decorator that caches results

def memoize(func): cache = {} def wrapper(n): if n not in cache: cache[n] = func(n) return cache[n] return wrapper @memoize def square(n): return n * n print(square(3)) print(square(3)) print(square(4))

Before you run it, say out loud what it will print. Then open the answer.

What it prints
9 9 16

Data Flow

A decorator receives a function, returns a wrapper that intercepts calls, and the wrapper runs the original logic.
1 Decorator called with the function
↓
2 Returns a wrapper closure capturing the function
↓
3 Call wrapper -> runs pre-logic
↓
4 Calls original -> captures post-logic -> returns
Why this works
How decorators compose

When stacking decorators, the bottom one wraps first:

@a
@b
@c
def f(): ...
# equals a(b(c(f)))

Each layer adds behaviour. Order matters: an outer decorator sees the already-wrapped inner function.

Metaclasses overview

A class is an instance of its metaclass; the default is type. Rarely needed, but metaclasses control class creation globally:

class Meta(type):
    def __new__(cls, name, bases, ns):
        ns["tag"] = True
        return super().__new__(cls, name, bases, ns)

class Foo(metaclass=Meta):
    pass

print(Foo.tag)   # True

Prefer classmethods or simple decorators over metaclasses unless you must change class creation itself.

Examples

Writing a decorator

Wrap a function

import time

def timer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        print(f"{func.__name__} took {time.time()-start:.3f}s")
        return result
    return wrapper

@timer
def slow():
    time.sleep(0.1)

slow()

A decorator is a function that takes a function and returns a new one. @timer is sugar for slow = timer(slow).

Parameterised decorators

Decorators that take args

def repeat(times):
    def decorator(func):
        def wrapper(*a, **k):
            for _ in range(times):
                result = func(*a, **k)
            return result
        return wrapper
    return decorator

@repeat(3)
def hi():
    print("hi")

Adding parentheses makes a decorator factory returning the actual decorator.

Inspecting with functools.wraps

Preserve metadata

from functools import wraps

def log(func):
    @wraps(func)
    def wrapper(*a, **k):
        print(f"calling {func.__name__}")
        return func(*a, **k)
    return wrapper

@wraps copies the original name and docstring so tools and help() still see the real function.

Words this lesson uses

Each one is defined here on this site. Open the entry for the full definition and the other words it points at.

← Advanced Class Design
Previous Lesson
Advanced Class Design
Next Lesson
Design Patterns in Python
Design Patterns in Python →