Ojasa Mirai

Ojasa Mirai

Python

Loading...

Learning Level

🟢 Beginner🔵 Advanced
Why Functions?Parameters & ArgumentsReturn StatementsScopeDefault ParametersVariable Arguments (*args)Lambda FunctionsDecoratorsFunctional ProgrammingBest Practices
Python/Functions/Decorators

🎨 Decorators — Enhance Function Behavior

Decorators are functions that modify other functions without changing their original code.


🎯 What is a Decorator?

A decorator is a function that:

1. Takes a function as input

2. Wraps it with additional behavior

3. Returns the enhanced function

def my_decorator(func):
    def wrapper():
        print("Something before the function")
        func()
        print("Something after the function")
    return wrapper

@my_decorator
def say_hello():
    print("Hello!")

say_hello()
# Output:
# Something before the function
# Hello!
# Something after the function

📝 Decorator Syntax

Using @ Symbol

@decorator_name
def function_name():
    pass

Equivalent to:

def function_name():
    pass

function_name = decorator_name(function_name)

💡 Basic Decorator Example

Step-by-Step

# Step 1: Define the decorator
def simple_decorator(func):
    def wrapper():
        print("Before")
        func()
        print("After")
    return wrapper

# Step 2: Apply the decorator
@simple_decorator
def my_function():
    print("During")

# Step 3: Call the decorated function
my_function()
# Output:
# Before
# During
# After

🔧 Decorators with Arguments

Decorator with Parameters

@simple_decorator
def greet(name):
    print(f"Hello {name}!")

greet("Alice")  # ❌ Error! wrapper() doesn't accept arguments

Problem: The wrapper doesn't accept arguments. Fix with `*args` and `**kwargs`:

def simple_decorator(func):
    def wrapper(*args, **kwargs):
        print("Before")
        result = func(*args, **kwargs)
        print("After")
        return result
    return wrapper

@simple_decorator
def greet(name):
    return f"Hello {name}!"

print(greet("Alice"))
# Output:
# Before
# After
# Hello Alice!

🎨 Practical Decorators

Logging Decorator

def log_calls(func):
    """Log when a function is called."""
    def wrapper(*args, **kwargs):
        print(f"[LOG] Calling {func.__name__} with args={args}, kwargs={kwargs}")
        result = func(*args, **kwargs)
        print(f"[LOG] {func.__name__} returned {result}")
        return result
    return wrapper

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

add(5, 3)
# Output:
# [LOG] Calling add with args=(5, 3), kwargs={}
# [LOG] add returned 8

Timing Decorator

import time

def timing_decorator(func):
    """Measure how long a function takes."""
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - start
        print(f"{func.__name__} took {elapsed:.4f} seconds")
        return result
    return wrapper

@timing_decorator
def slow_function(delay):
    time.sleep(delay)
    return "Done!"

slow_function(0.5)
# Output:
# slow_function took 0.5001 seconds

Error Handling Decorator

def handle_errors(func):
    """Catch and handle errors gracefully."""
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except Exception as e:
            print(f"Error in {func.__name__}: {e}")
            return None
    return wrapper

@handle_errors
def divide(a, b):
    return a / b

divide(10, 2)   # Returns 5.0
divide(10, 0)   # Error: division by zero, returns None

Caching Decorator

def cache_result(func):
    """Cache function results to avoid recomputation."""
    cache = {}

    def wrapper(*args):
        if args not in cache:
            print(f"Computing {func.__name__}{args}...")
            cache[args] = func(*args)
        else:
            print(f"Using cached result for {func.__name__}{args}")
        return cache[args]

    return wrapper

@cache_result
def expensive_function(n):
    return n ** n

print(expensive_function(5))  # Computing expensive_function(5)... 3125
print(expensive_function(5))  # Using cached result... 3125

🔗 Stacking Multiple Decorators

You can apply multiple decorators to the same function:

def decorator_a(func):
    def wrapper(*args, **kwargs):
        print("A1")
        result = func(*args, **kwargs)
        print("A2")
        return result
    return wrapper

def decorator_b(func):
    def wrapper(*args, **kwargs):
        print("B1")
        result = func(*args, **kwargs)
        print("B2")
        return result
    return wrapper

@decorator_a
@decorator_b
def my_function():
    print("Function")

my_function()
# Output:
# A1
# B1
# Function
# B2
# A2

📊 Real-World Examples

Authentication Decorator

def require_login(func):
    """Decorator to check if user is logged in."""
    def wrapper(*args, **kwargs):
        user = kwargs.get("user")
        if not user:
            print("Access denied: Not logged in")
            return None
        print(f"Access granted for {user}")
        return func(*args, **kwargs)
    return wrapper

@require_login
def view_profile(**kwargs):
    return f"Profile data: {kwargs['user']}"

view_profile(user="Alice")  # Access granted... Profile data: Alice
view_profile()              # Access denied

Rate Limiting Decorator

import time

def rate_limit(max_calls, time_window):
    """Limit how often a function can be called."""
    def decorator(func):
        calls = []

        def wrapper(*args, **kwargs):
            now = time.time()
            calls[:] = [c for c in calls if c > now - time_window]

            if len(calls) >= max_calls:
                print(f"Rate limit exceeded for {func.__name__}")
                return None

            calls.append(now)
            return func(*args, **kwargs)

        return wrapper
    return decorator

@rate_limit(max_calls=3, time_window=10)
def api_call():
    print("API call successful")
    return "data"

# Can call 3 times per 10 seconds

⚠️ Advanced: Preserving Function Metadata

When you decorate a function, you lose its metadata:

def my_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def my_function():
    """This is my function."""
    pass

print(my_function.__name__)      # ❌ wrapper (not my_function!)
print(my_function.__doc__)       # ❌ None (not the docstring!)

Solution: Use `functools.wraps`:

from functools import wraps

def my_decorator(func):
    @wraps(func)  # ← Preserves metadata
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def my_function():
    """This is my function."""
    pass

print(my_function.__name__)      # ✅ my_function
print(my_function.__doc__)       # ✅ This is my function.

🎯 When to Use Decorators

✅ Good Use Cases

  • Cross-cutting concerns (logging, timing, caching)
  • Validation and authentication
  • Input/output processing
  • Behavior modification without changing original code

❌ Avoid When

  • Logic is complex and hard to trace
  • Only used once
  • Better served by a regular function

🔑 Key Takeaways

ConceptRemember
DecoratorFunction that wraps another function
Syntax`@decorator_name` above function
PurposeAdd behavior without changing original code
ParametersUse `*args, **kwargs` for flexibility
StackingCan apply multiple decorators
MetadataUse `@wraps` to preserve function info

🔗 What's Next?

Now explore functional programming with map, filter, and reduce to work with data in elegant ways.

Next: Functional Programming →


Practice: Decorator challenges


Resources

Python Docs

Ojasa Mirai

Master AI-powered development skills through structured learning, real projects, and verified credentials. Whether you're upskilling your team or launching your career, we deliver the skills companies actually need.

Learn Deep • Build Real • Verify Skills • Launch Forward

Courses

PythonFastapiReactJSCloud

© 2026 Ojasa Mirai. All rights reserved.

TwitterGitHubLinkedIn