def measure_time(func):           # func receives slow_function
    def wrapper(*args, **kwargs): # This is a new function that "wraps around" the original
        start_time = time.time()  
        result = func(*args, **kwargs)  # Calls the original slow_function
        end_time = time.time()    
        print(f"Function {func.__name__} took {end_time - start_time} seconds to run")
        return result
    return wrapper  

@measure_time
def slow_function():
    time.sleep(1)
    print("Function completed")

# When you call slow_function(), this is what happens:
slow_function()  # This actually calls wrapper()

start_time = time.time()           # Records start time

result = func(*args, **kwargs)     # Calls original slow_function which:
                                  # - Waits 1 second (time.sleep(1))
                                  # - Prints "Function completed"

end_time = time.time()            # Records end time

# Prints something like:
# "Function slow_function took 1.001 seconds to run"

return result                     # Returns whatever the original function returned

More examples:

# Example 1: Logging decorator for multiple functions
def add_logging(func):
    def wrapper(*args, **kwargs):
        print(f"Calling function: {func.__name__}")
        print(f"Arguments: {args}, {kwargs}")
        result = func(*args, **kwargs)
        print(f"Function {func.__name__} finished with result: {result}")
        return result
    return wrapper

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

@add_logging
def multiply(a, b):
    return a * b

# Now both functions automatically get logging without repeating code
add(2, 3)        # Logs: Calling function: add
                 #       Arguments: (2, 3), {}
                 #       Function add finished with result: 5

multiply(4, 5)   # Logs: Calling function: multiply
                 #       Arguments: (4, 5), {}
                 #       Function multiply finished with result: 20

# Example 2: Error handling decorator
def handle_exceptions(func):
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except Exception as e:
            print(f"Error in {func.__name__}: {str(e)}")
            return None
    return wrapper

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

@handle_exceptions
def get_item(list_data, index):
    return list_data[index]

# Both functions now have error handling without repeating try-except
divide(10, 0)           # Prints: Error in divide: division by zero
get_item([1,2,3], 5)   # Prints: Error in get_item: list index out of range

# Example 3: Authentication decorator
def require_auth(func):
    def wrapper(*args, **kwargs):
        if not check_user_logged_in():  # Assume this function exists
            return "Please log in first"
        return func(*args, **kwargs)
    return wrapper

@require_auth
def view_profile():
    return "Here's your profile"

@require_auth
def edit_settings():
    return "Edit your settings"

# Both functions now check for authentication without repeating code

# 1. Basic Decorator Structure
def my_decorator(func):
    def wrapper(*args, **kwargs):
        # Code before function
        result = func(*args, **kwargs)
        # Code after function
        return result
    return wrapper

# 2. Ways to Use Decorators
# Method 1: Using @ syntax
@my_decorator
def my_function():
    pass

# Method 2: Direct assignment (same as above)
def my_function():
    pass
my_function = my_decorator(my_function)

# 3. Common Use Cases with Examples
# Timing Decorator
def measure_time(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"Time taken: {end_time - start_time} seconds")
        return result
    return wrapper

# Logging Decorator
def add_logging(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        result = func(*args, **kwargs)
        print(f"Finished {func.__name__}")
        return result
    return wrapper

# Argument Handling Decorator
def validate_args(func):
    def wrapper(*args, **kwargs):
        # Validate arguments
        for arg in args:
            if arg is None:
                raise ValueError("None not allowed")
        return func(*args, **kwargs)
    return wrapper

Common use cases

  1. Cross-cutting concerns:
  2. Performance optimization:
def cache_result(func):
    cache = {}
    def wrapper(*args):
        if args in cache:
            return cache[args]
        result = func(*args)
        cache[args] = result
        return result
    return wrapper

@cache_result
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

@cache_result
def expensive_api_call(url):
    # Expensive API call code here
    pass
  1. Input/Output modification:
def convert_to_uppercase(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        if isinstance(result, str):
            return result.upper()
        return result
    return wrapper

@convert_to_uppercase
def get_name():
    return "john"

@convert_to_uppercase
def get_title():
    return "developer"

The main benefits are:

  1. Write the common functionality once
  2. Apply it to multiple functions
  3. Keep the code DRY
  4. Make the code more maintainable
  5. Separate concerns