DevToolBoxGRATIS
Blog

Decoradores Python: De Básico a Avanzado

15 minpor DevToolBox

What Are Python Decorators?

A decorator in Python is a function that takes another function as input and extends its behavior without modifying it. Decorators are one of Python's most powerful features, enabling clean separation of concerns, code reuse, and elegant metaprogramming. They are used extensively in frameworks like Flask, Django, FastAPI, and pytest.

At its core, a decorator is syntactic sugar for higher-order functions. When you write @decorator above a function definition, Python passes that function to the decorator and replaces it with the return value.

# These two are equivalent:

# Using decorator syntax
@my_decorator
def say_hello():
    print("Hello!")

# Manual decoration
def say_hello():
    print("Hello!")
say_hello = my_decorator(say_hello)

Creating Your First Decorator

import functools

def my_decorator(func):
    @functools.wraps(func)  # Preserves original function metadata
    def wrapper(*args, **kwargs):
        print(f"Before calling {func.__name__}")
        result = func(*args, **kwargs)
        print(f"After calling {func.__name__}")
        return result
    return wrapper

@my_decorator
def greet(name):
    """Greet someone by name."""
    print(f"Hello, {name}!")
    return f"Greeted {name}"

# Usage
result = greet("Alice")
# Output:
# Before calling greet
# Hello, Alice!
# After calling greet

# Metadata is preserved thanks to @functools.wraps
print(greet.__name__)  # "greet" (not "wrapper")
print(greet.__doc__)   # "Greet someone by name."

Why functools.wraps Matters

Without @functools.wraps, the decorated function loses its original name, docstring, and other metadata. This causes problems with debugging, documentation generation, and introspection tools. Always use @functools.wraps(func) in your decorators.

Practical Decorator Examples

1. Timing Decorator

import functools
import time

def timer(func):
    """Measure execution time of a function."""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} took {elapsed:.4f}s")
        return result
    return wrapper

@timer
def fetch_data(url):
    """Simulate fetching data."""
    time.sleep(1.2)
    return {"status": "ok"}

fetch_data("https://api.example.com")
# Output: fetch_data took 1.2003s

2. Retry Decorator

import functools
import time
import random

def retry(max_attempts=3, delay=1.0, backoff=2.0, exceptions=(Exception,)):
    """Retry a function on failure with exponential backoff."""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            last_exception = None
            current_delay = delay

            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    last_exception = e
                    if attempt < max_attempts:
                        # Add jitter to prevent thundering herd
                        jitter = random.uniform(0, current_delay * 0.1)
                        sleep_time = current_delay + jitter
                        print(f"Attempt {attempt} failed: {e}. "
                              f"Retrying in {sleep_time:.1f}s...")
                        time.sleep(sleep_time)
                        current_delay *= backoff
                    else:
                        print(f"All {max_attempts} attempts failed")

            raise last_exception
        return wrapper
    return decorator

@retry(max_attempts=3, delay=0.5, exceptions=(ConnectionError, TimeoutError))
def call_api(endpoint):
    """Call an external API."""
    if random.random() < 0.7:
        raise ConnectionError("Connection refused")
    return {"data": "success"}

# Will retry up to 3 times with exponential backoff
result = call_api("/users")

3. Caching / Memoization Decorator

import functools
from typing import Any

def memoize(func):
    """Cache function results based on arguments."""
    cache = {}

    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # Create a hashable key from args and kwargs
        key = (args, tuple(sorted(kwargs.items())))

        if key not in cache:
            cache[key] = func(*args, **kwargs)

        return cache[key]

    wrapper.cache = cache  # Expose cache for inspection
    wrapper.clear_cache = cache.clear  # Allow cache clearing
    return wrapper

@memoize
def fibonacci(n):
    """Calculate fibonacci number (recursive)."""
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(100))  # Instant, without memoization this takes forever

# Python has a built-in alternative:
from functools import lru_cache

@lru_cache(maxsize=128)
def fibonacci_builtin(n):
    if n < 2:
        return n
    return fibonacci_builtin(n - 1) + fibonacci_builtin(n - 2)

# Check cache stats
print(fibonacci_builtin.cache_info())
# CacheInfo(hits=98, misses=101, maxsize=128, currsize=101)

4. Authentication / Authorization Decorator

import functools

def require_auth(func):
    """Ensure user is authenticated before calling function."""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # In a real app, check session/token
        request = kwargs.get('request') or (args[0] if args else None)
        if not request or not getattr(request, 'user', None):
            raise PermissionError("Authentication required")
        return func(*args, **kwargs)
    return wrapper

def require_role(*roles):
    """Ensure user has one of the specified roles."""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            request = kwargs.get('request') or (args[0] if args else None)
            user_role = getattr(request, 'user_role', None)
            if user_role not in roles:
                raise PermissionError(
                    f"Requires one of {roles}, got {user_role}"
                )
            return func(*args, **kwargs)
        return wrapper
    return decorator

# Stack multiple decorators
@require_auth
@require_role('admin', 'moderator')
def delete_user(request, user_id):
    """Delete a user (admin/moderator only)."""
    print(f"Deleting user {user_id}")
    return {"deleted": user_id}

5. Rate Limiting Decorator

import functools
import time
from collections import defaultdict

def rate_limit(max_calls, period):
    """Limit function calls to max_calls per period (seconds)."""
    calls = defaultdict(list)

    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            now = time.time()
            key = func.__name__

            # Remove expired timestamps
            calls[key] = [t for t in calls[key] if now - t < period]

            if len(calls[key]) >= max_calls:
                wait = period - (now - calls[key][0])
                raise RuntimeError(
                    f"Rate limit exceeded. Try again in {wait:.1f}s"
                )

            calls[key].append(now)
            return func(*args, **kwargs)
        return wrapper
    return decorator

@rate_limit(max_calls=5, period=60)
def send_email(to, subject, body):
    """Send an email (max 5 per minute)."""
    print(f"Sending email to {to}: {subject}")
    return True

Decorators with Arguments

When a decorator needs arguments, you add an extra level of nesting. The outer function takes the decorator arguments, the middle function takes the function being decorated, and the inner function is the actual wrapper.

import functools

def repeat(n=2):
    """Call the function n times and return results as a list."""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            results = []
            for _ in range(n):
                results.append(func(*args, **kwargs))
            return results
        return wrapper
    return decorator

@repeat(n=3)
def roll_dice():
    import random
    return random.randint(1, 6)

print(roll_dice())  # [4, 2, 6]

# Flexible decorator (works with and without arguments)
def flexible_decorator(func=None, *, debug=False):
    """Can be used as @flexible_decorator or @flexible_decorator(debug=True)."""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            if debug:
                print(f"Calling {func.__name__}({args}, {kwargs})")
            result = func(*args, **kwargs)
            if debug:
                print(f"{func.__name__} returned {result}")
            return result
        return wrapper

    if func is not None:
        return decorator(func)  # Called without arguments
    return decorator  # Called with arguments

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

@flexible_decorator(debug=True)
def multiply(a, b):
    return a * b

Class-Based Decorators

Decorators can also be implemented as classes using the __call__ method. This is useful when the decorator needs to maintain state.

import functools
import time

class CountCalls:
    """Track how many times a function is called."""

    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"{self.func.__name__} called {self.count} times")
        return self.func(*args, **kwargs)

@CountCalls
def process_item(item):
    return item.upper()

process_item("hello")   # process_item called 1 times
process_item("world")   # process_item called 2 times
print(process_item.count)  # 2

# Class decorator with arguments
class Throttle:
    """Ensure minimum interval between calls."""

    def __init__(self, min_interval=1.0):
        self.min_interval = min_interval
        self.last_call = 0

    def __call__(self, func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            now = time.time()
            elapsed = now - self.last_call

            if elapsed < self.min_interval:
                wait = self.min_interval - elapsed
                time.sleep(wait)

            self.last_call = time.time()
            return func(*args, **kwargs)
        return wrapper

@Throttle(min_interval=0.5)
def rapid_fire():
    print(f"Called at {time.time():.2f}")

for _ in range(3):
    rapid_fire()  # Each call separated by at least 0.5s

Decorating Classes

Decorators are not limited to functions. You can decorate classes to modify their behavior, add methods, or register them in a registry.

import functools

# Add methods to a class
def add_repr(cls):
    """Auto-generate __repr__ from __init__ parameters."""
    original_init = cls.__init__

    @functools.wraps(original_init)
    def new_init(self, *args, **kwargs):
        self._init_args = args
        self._init_kwargs = kwargs
        original_init(self, *args, **kwargs)

    def __repr__(self):
        args = [repr(a) for a in self._init_args]
        kwargs = [f"{k}={v!r}" for k, v in self._init_kwargs.items()]
        params = ", ".join(args + kwargs)
        return f"{self.__class__.__name__}({params})"

    cls.__init__ = new_init
    cls.__repr__ = __repr__
    return cls

@add_repr
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

print(Point(3, 4))  # Point(3, 4)

# Plugin/handler registry
class Registry:
    """Registry decorator for plugins."""

    def __init__(self):
        self._handlers = {}

    def register(self, name=None):
        def decorator(cls):
            key = name or cls.__name__
            self._handlers[key] = cls
            return cls
        return decorator

    def get(self, name):
        return self._handlers.get(name)

    def list(self):
        return list(self._handlers.keys())

handlers = Registry()

@handlers.register("json")
class JsonHandler:
    def parse(self, data):
        import json
        return json.loads(data)

@handlers.register("csv")
class CsvHandler:
    def parse(self, data):
        return data.split(",")

# Use the registry
handler = handlers.get("json")()
result = handler.parse('{"key": "value"}')

Built-in Python Decorators

# @property - Create managed attributes
class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        if value < 0:
            raise ValueError("Radius cannot be negative")
        self._radius = value

    @property
    def area(self):
        import math
        return math.pi * self._radius ** 2

circle = Circle(5)
print(circle.area)    # 78.54
circle.radius = 10    # Uses setter
# circle.radius = -1  # Raises ValueError

# @staticmethod - No access to instance or class
class MathUtils:
    @staticmethod
    def add(a, b):
        return a + b

# @classmethod - Access to the class, not instance
class Date:
    def __init__(self, year, month, day):
        self.year = year
        self.month = month
        self.day = day

    @classmethod
    def from_string(cls, date_string):
        year, month, day = map(int, date_string.split('-'))
        return cls(year, month, day)

    @classmethod
    def today(cls):
        import datetime
        d = datetime.date.today()
        return cls(d.year, d.month, d.day)

date = Date.from_string('2026-02-22')

# @abstractmethod - Define interface contracts
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

    @abstractmethod
    def perimeter(self):
        pass

Real-World Framework Examples

# Flask route decorators
from flask import Flask, request, jsonify
app = Flask(__name__)

@app.route('/api/users', methods=['GET'])
def get_users():
    return jsonify({"users": []})

@app.route('/api/users/<int:user_id>', methods=['GET'])
def get_user(user_id):
    return jsonify({"id": user_id})

# FastAPI with type hints
from fastapi import FastAPI, Depends
app = FastAPI()

@app.get("/items/{item_id}")
async def read_item(item_id: int, q: str = None):
    return {"item_id": item_id, "q": q}

# pytest fixtures and markers
import pytest

@pytest.fixture
def sample_data():
    return {"name": "test", "value": 42}

@pytest.mark.parametrize("input,expected", [
    ("hello", 5),
    ("world", 5),
    ("", 0),
])
def test_string_length(input, expected):
    assert len(input) == expected

# Django decorators
from django.contrib.auth.decorators import login_required
from django.views.decorators.cache import cache_page

@login_required
@cache_page(60 * 15)  # Cache for 15 minutes
def dashboard(request):
    return render(request, 'dashboard.html')

Advanced: Decorator Stacking

When you stack multiple decorators, they are applied bottom-up. Understanding this order is crucial for correct behavior.

@decorator_a    # Applied second (outermost)
@decorator_b    # Applied first (innermost)
def my_function():
    pass

# Equivalent to:
# my_function = decorator_a(decorator_b(my_function))

# Example: logging + timing + auth
@require_auth          # 3rd: Check auth before anything
@timer                 # 2nd: Time the authenticated operation
@retry(max_attempts=3) # 1st: Retry on failure
def fetch_protected_data(request, resource_id):
    return api.get(f"/resources/{resource_id}")

# Execution flow:
# 1. require_auth checks authentication
# 2. timer starts measuring
# 3. retry wrapper catches exceptions and retries
# 4. fetch_protected_data executes

Common Pitfalls

  • Forgetting @functools.wraps: Always use it to preserve the original function's metadata
  • Not returning the result: The wrapper must return func(*args, **kwargs), not just call it
  • Mutable default arguments in decorators: Use factory functions or instance variables instead of mutable defaults
  • Decorator order matters: Stack decorators carefully -- the bottom decorator is applied first
  • Performance overhead: Each decorator adds a function call. For hot code paths, consider alternatives
  • Testing decorated functions: Access the original function via func.__wrapped__ when needed

Summary

Python decorators are a powerful metaprogramming tool that enables clean, reusable, and composable code. Start with simple function decorators for cross-cutting concerns like logging, timing, and caching. Progress to decorators with arguments for configurable behavior, and class-based decorators for stateful decorators. Always use @functools.wraps, understand the stacking order, and leverage Python's built-in decorators (@property, @classmethod, @staticmethod) in your class designs. Decorators are used throughout the Python ecosystem, so mastering them makes you more effective with every Python framework.

𝕏 Twitterin LinkedIn
¿Fue útil?

Mantente actualizado

Recibe consejos de desarrollo y nuevas herramientas.

Sin spam. Cancela cuando quieras.

Artículos relacionados

Python vs JavaScript: Cual aprender en 2026?

Comparacion completa de Python y JavaScript: sintaxis, rendimiento, ecosistemas, mercado laboral y casos de uso. Decide que lenguaje aprender primero en 2026.

Guia de Rate Limiting en APIs: estrategias, algoritmos e implementacion

Guia completa de rate limiting en APIs. Token bucket, sliding window, leaky bucket con ejemplos de codigo. Middleware Express.js, Redis distribuido y mejores practicas.