C
Python/Intermediate/Lesson 12

Decorators

1 hr·theory
This chapter
4/8
Python

Decorators

🎯 By the end of this lesson

After reading this lesson, you will be able to confidently do the following three things.

  • ✅ Understand how higher-order functions return other functions
  • ✅ Use the @decorator syntax and understand why functools.wraps matters
  • ✅ Apply @lru_cache, @property, and @staticmethod in practice

Use the learning goals as a checklist, and close the lesson once you can answer all of them.

Decorators — Code + Output

@decorator = wrap a function to add functionality. Separates cross-cutting concerns like logging, caching, and authentication.


1. The simplest decorator

python
def log(original_function):
    def wrapped(*args, **kwargs):
        print(f"Call: {original_function.__name__}({args})")
        result = original_function(*args, **kwargs)
        print(f"Result: {result}")
        return result
    return wrapped

@log
def add(a, b):
    return a + b

add(3, 5)
# Output:
# Call: add((3, 5))
# Result: 8

@log is equivalent to writing add = log(add).


2. Measuring execution time

python
import time

def measure_time(f):
    def wrapped(*args, **kwargs):
        start = time.time()
        result = f(*args, **kwargs)
        elapsed = time.time() - start
        print(f"{f.__name__}: {elapsed:.3f}s")
        return result
    return wrapped

@measure_time
def heavy_task():
    time.sleep(1)
    return "Done"

heavy_task()      # heavy_task: 1.001s

3. Decorator with arguments

python
def repeat(count):
    def deco(f):
        def wrapped(*args, **kwargs):
            for _ in range(count):
                result = f(*args, **kwargs)
            return result
        return wrapped
    return deco

@repeat(3)
def greet():
    print("Hello!")

greet()
# Hello!
# Hello!
# Hello!

4. Commonly used built-in decorators

python
from functools import lru_cache

@lru_cache(maxsize=100)         # Cache results — returns immediately if arguments are the same
def fibonacci(n):
    return n if n < 2 else fibonacci(n-1) + fibonacci(n-2)

fibonacci(50)                    # Instant due to cache (billions of calls without it)

@property (getter), @staticmethod, and @classmethod are also frequently used in classes.


One-line summary

PatternCode
Simple@decorator
With args@decorator(args)
Caching@lru_cache
Access@property
💻 Bad Example — Implemented without functools.wraps
# Decorator created without wraps — makes debugging difficult
def timer(func):
    def wrapper(*args, **kwargs):
        import time
        start = time.time()
        result = func(*args, **kwargs)
        print(f"Execution time: {time.time() - start:.4f}s")
        return result
    return wrapper  # The wrapper function name overwrites the original

@timer
def calculate(n):
    """Calculates the square of n"""
    return n ** 2

print(calculate.__name__)  # 'wrapper' — original name lost!
print(calculate.__doc__)   # None — docstring lost!
💻 Good Example — functools.wraps + Decorator with Arguments
import functools
import time
import logging

# Basic decorator — functools.wraps is essential
def timer(func):
    @functools.wraps(func)  # Preserves original metadata
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} execution time: {elapsed:.4f}s")
        return result
    return wrapper

# Decorator with arguments — triple nested
def retry(max_attempts=3, exceptions=(Exception,)):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    if attempt == max_attempts:
                        raise
                    print(f"Retry {attempt}/{max_attempts}: {e}")
        return wrapper
    return decorator

# Real-world: FastAPI-style authentication decorator
def require_auth(func):
    @functools.wraps(func)
    def wrapper(request, *args, **kwargs):
        if not request.get('user'):
            raise PermissionError("Login required")
        return func(request, *args, **kwargs)
    return wrapper

@timer
@retry(max_attempts=3, exceptions=(ConnectionError,))
def fetch_data(url: str) -> dict:
    """Fetches data from an external API"""
    import urllib.request
    with urllib.request.urlopen(url) as response:
        import json
        return json.loads(response.read())

print(fetch_data.__name__)  # 'fetch_data' — original name preserved
print(fetch_data.__doc__)   # 'Fetches data from an external API' — docstring preserved
💻 Practical Example — Class-based Decorator (Cache)
import functools
from typing import Callable, Any

# Class-based decorator — advantageous for state management
class Cache:
    """A simple memoization cache decorator"""
    
    def __init__(self, max_size: int = 128):
        self.max_size = max_size
        self._cache: dict = {}
    
    def __call__(self, func: Callable) -> Callable:
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            key = (args, tuple(sorted(kwargs.items())))
            if key not in self._cache:
                if len(self._cache) >= self.max_size:
                    # Simple LRU implementation: remove the oldest item
                    oldest = next(iter(self._cache))
                    del self._cache[oldest]
                self._cache[key] = func(*args, **kwargs)
            return self._cache[key]
        wrapper.cache_clear = lambda: self._cache.clear()
        wrapper.cache_info = lambda: {'size': len(self._cache), 'max': self.max_size}
        return wrapper

@Cache(max_size=64)
def fibonacci(n: int) -> int:
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(50))              # Calculates quickly
print(fibonacci.cache_info())     # {'size': 51, 'max': 64}
fibonacci.cache_clear()           # Clears cache

# Note: functools.lru_cache from the standard library serves the same purpose
from functools import lru_cache

@lru_cache(max_size=128)
def factorial(n: int) -> int:
    return 1 if n == 0 else n * factorial(n - 1)

🐍 Try It — Decorators

Run the concepts above as actual code. Changing values and observing the behavior directly is the fastest way to learn.
✏️ Python 코드
📟 Console output
▶ Press the Run button
🐍 Real Python via Pyodide — first run takes 3–5s to load

🤖 How to ask AI

Knowing the concepts from this lesson lets you give AI specific instructions — not a vague 'fix this,' but a request with vocabulary. That is the starting point for saving tokens.

  • 'Apply the functools.lru_cache decorator to this function'
  • 'Create a timing and logging decorator using functools.wraps'

Why this reduces tokens

Without the concepts, you have to ask 'What does that mean?' after every AI response. Those follow-up questions eat tokens. Learn the concept once and the conversation ends in a single exchange.

Decorators - Python