|$ curl https://forge-ai.dev/api/markdown?path=docs/python/decorators
$cat docs/python-—-decorators.md
updated Recently·18 min read·published

Python — Decorators

PythonAdvanced
Introduction

Decorators are functions that modify the behavior of other functions or classes without changing their source code. They are a powerful metaprogramming tool that enables aspect-oriented programming patterns.

Decorator Basics

A decorator is a function that takes a function and returns a function. The @decorator syntax is syntactic sugar for func = decorator(func).

basic.py
Python
1# A decorator is just a function that takes a function
2def my_decorator(func):
3 def wrapper():
4 print("before function call")
5 func()
6 print("after function call")
7 return wrapper
8
9# Applying a decorator
10@my_decorator
11def say_hello():
12 print("hello!")
13
14# This is equivalent to:
15# say_hello = my_decorator(say_hello)
16
17say_hello()
18# Output:
19# before function call
20# hello!
21# after function call
Preserving Function Metadata

Without care, decorators clobber the original function's name, docstring, and signature. Use functools.wraps to preserve them.

wraps.py
Python
1from functools import wraps
2
3def timer(func):
4 """Print how long the function takes."""
5 @wraps(func) # ← critical! preserves func's metadata
6 def wrapper(*args, **kwargs):
7 import time
8 start = time.perf_counter()
9 result = func(*args, **kwargs)
10 elapsed = time.perf_counter() - start
11 print(f"{func.__name__} took {elapsed:.4f}s")
12 return result
13 return wrapper
14
15@timer
16def slow_function():
17 """A function that takes a while."""
18 return sum(range(10_000_000))
19
20# Without @wraps:
21print(slow_function.__name__) # → "wrapper" (wrong!)
22
23# With @wraps:
24print(slow_function.__name__) # → "slow_function" (correct)
25print(slow_function.__doc__) # → "A function that takes a while."

best practice

Always use @functools.wraps in your decorators. Without it, debugging becomes painful because function names and docstrings are replaced.
Decorators with Arguments
with_args.py
Python
1# Decorator with arguments = three levels of nesting
2def repeat(n: int):
3 """Run the decorated function n times."""
4 def decorator(func):
5 @wraps(func)
6 def wrapper(*args, **kwargs):
7 for _ in range(n):
8 result = func(*args, **kwargs)
9 return result
10 return wrapper
11 return decorator
12
13@repeat(3)
14def greet(name: str):
15 print(f"Hello, {name}!")
16
17greet("Alice")
18# Output:
19# Hello, Alice!
20# Hello, Alice!
21# Hello, Alice!
22
23# Equivalent to: greet = repeat(3)(greet)
24
25# Decorator with optional arguments
26def cache(func=None, *, ttl: int = 60):
27 """Cache results with optional TTL."""
28 def decorator(fn):
29 @wraps(fn)
30 def wrapper(*args, **kwargs):
31 key = (args, tuple(kwargs.items()))
32 if key not in wrapper._cache:
33 wrapper._cache[key] = fn(*args, **kwargs)
34 return wrapper._cache[key]
35 wrapper._cache = {}
36 return wrapper
37
38 if func is not None:
39 return decorator(func) # used as @cache (no args)
40 return decorator # used as @cache(ttl=120)
41
42@cache
43def expensive1(x): ...
44
45@cache(ttl=300)
46def expensive2(x): ...
Real-World Decorator Patterns
real_world.py
Python
1# 1. Timing / Profiling
2import time
3from functools import wraps
4
5def timer(func):
6 @wraps(func)
7 def wrapper(*args, **kwargs):
8 start = time.perf_counter()
9 result = func(*args, **kwargs)
10 elapsed = time.perf_counter() - start
11 print(f"{func.__name__}: {elapsed:.4f}s")
12 return result
13 return wrapper
14
15# 2. Logging
16import logging
17logger = logging.getLogger(__name__)
18
19def log_call(func):
20 @wraps(func)
21 def wrapper(*args, **kwargs):
22 logger.info(f"Calling {func.__name__} with args={args} kwargs={kwargs}")
23 try:
24 result = func(*args, **kwargs)
25 logger.info(f"{func.__name__} returned {result}")
26 return result
27 except Exception as e:
28 logger.error(f"{func.__name__} raised {e}")
29 raise
30 return wrapper
31
32# 3. Retry on failure
33def retry(max_attempts: int = 3, delay: float = 1.0):
34 def decorator(func):
35 @wraps(func)
36 def wrapper(*args, **kwargs):
37 import time
38 for attempt in range(max_attempts):
39 try:
40 return func(*args, **kwargs)
41 except Exception as e:
42 if attempt == max_attempts - 1:
43 raise
44 time.sleep(delay)
45 return None
46 return wrapper
47 return decorator
48
49@retry(max_attempts=5, delay=0.5)
50def fetch_data(url: str) -> dict:
51 ...
52
53# 4. Rate limiting
54from time import sleep
55
56def rate_limit(calls: int, period: float):
57 def decorator(func):
58 last_reset = time.time()
59 call_count = 0
60
61 @wraps(func)
62 def wrapper(*args, **kwargs):
63 nonlocal last_reset, call_count
64 now = time.time()
65 if now - last_reset > period:
66 call_count = 0
67 last_reset = now
68 if call_count >= calls:
69 raise RuntimeError("Rate limit exceeded")
70 call_count += 1
71 return func(*args, **kwargs)
72 return wrapper
73 return decorator
74
75# 5. Deprecation warning
76import warnings
77
78def deprecated(message: str = ""):
79 def decorator(func):
80 @wraps(func)
81 def wrapper(*args, **kwargs):
82 warnings.warn(
83 f"{func.__name__} is deprecated. {message}",
84 DeprecationWarning,
85 stacklevel=2,
86 )
87 return func(*args, **kwargs)
88 return wrapper
89 return decorator
90
91@deprecated("Use new_function() instead")
92def old_function():
93 pass
Class-based Decorators
class_decorators.py
Python
1# Decorator as a class (implement __call__)
2class CountCalls:
3 """Count how many times a function is called."""
4
5 def __init__(self, func):
6 self.func = func
7 self.count = 0
8
9 def __call__(self, *args, **kwargs):
10 self.count += 1
11 print(f"Call {self.count} of {self.func.__name__}")
12 return self.func(*args, **kwargs)
13
14@CountCalls
15def say_hi():
16 print("hi!")
17
18say_hi() # → Call 1 of say_hi / hi!
19say_hi() # → Call 2 of say_hi / hi!
20print(say_hi.count) # → 2
21
22# Class decorators (decorating a class)
23def add_repr(cls):
24 """Add __repr__ to any class."""
25 original_init = cls.__init__
26
27 def __repr__(self):
28 args = ', '.join(
29 f"{k}={v!r}" for k, v in self.__dict__.items()
30 )
31 return f"{cls.__name__}({args})"
32
33 cls.__repr__ = __repr__
34 return cls
35
36@add_repr
37class Person:
38 def __init__(self, name: str, age: int):
39 self.name = name
40 self.age = age
41
42p = Person("Alice", 30)
43print(p) # → Person(name='Alice', age=30)
Built-in Decorators
builtin.py
Python
1# @property — method as attribute
2class Circle:
3 def __init__(self, radius):
4 self._radius = radius
5
6 @property
7 def radius(self):
8 return self._radius
9
10 @radius.setter
11 def radius(self, value):
12 if value < 0:
13 raise ValueError("Radius can't be negative")
14 self._radius = value
15
16 @property
17 def area(self):
18 return 3.14159 * self._radius ** 2
19
20# @staticmethod — no self or cls
21class Math:
22 @staticmethod
23 def add(a, b):
24 return a + b
25
26Math.add(2, 3) # → 5 (no instance needed)
27
28# @classmethod — receives cls instead of self
29class Config:
30 settings = {}
31
32 @classmethod
33 def from_file(cls, path: str):
34 import json
35 cls.settings = json.load(open(path))
36 return cls()
37
38# @dataclass — auto-generate boilerplate
39from dataclasses import dataclass
40
41@dataclass
42class Point:
43 x: float
44 y: float
45
46# @lru_cache — memoization
47from functools import lru_cache
48
49@lru_cache(maxsize=128)
50def fib(n: int) -> int:
51 if n < 2:
52 return n
53 return fib(n - 1) + fib(n - 2)
54
55# @singledispatch — function overloading by type
56from functools import singledispatch
57
58@singledispatch
59def to_string(obj):
60 raise NotImplementedError
61
62@to_string.register(int)
63def _(obj):
64 return str(obj)
65
66@to_string.register(list)
67def _(obj):
68 return ", ".join(str(x) for x in obj)
69
70# @contextmanager — create context managers easily
71from contextlib import contextmanager
72
73@contextmanager
74def temporary_change(obj, attr, value):
75 original = getattr(obj, attr)
76 setattr(obj, attr, value)
77 try:
78 yield
79 finally:
80 setattr(obj, attr, original)
$Blueprint — Engineering Documentation·Section ID: PYTHON-DEC·Revision: 1.0