Mastering Python Decorators: Creating and Using Custom Decorators

    By: Thad Mertz
    5 months ago

    Python is known for its simplicity and versatility, and one of the most fascinating features it offers is decorators. Decorators allow you to modify or enhance the behavior of functions or methods in a clean and reusable way. While Python provides a handful of built-in decorators like `@staticmethod`, `@classmethod`, and `@property`, you can take your Python programming skills to the next level by creating your own custom decorators. In this blog post, we'll dive deep into the world of Python decorators and demonstrate the usage of custom decorators for various practical scenarios.


    What Are Decorators?


    In Python, a decorator is a special type of function that can be used to modify another function. Decorators are applied using the "@" symbol just before the function you want to decorate. Decorators are extensively used for tasks such as logging, authentication, access control, and more.


    Here's a simple example of a decorator:

    def my_decorator(func):
      def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
      return wrapper
    
    @my_decorator
    def say_hello():
      print("Hello!")
    
    say_hello()
    



    Output:


    Something is happening before the function is called.

    Hello!

    Something is happening after the function is called.


    ### Creating Custom Decorators


    To create a custom decorator, you define a function that takes another function as an argument, modifies its behavior, and returns a new function. Here's a step-by-step guide on how to create and use custom decorators:


    Step 1: Define Your Custom Decorator Function

    def my_custom_decorator(func):
      def wrapper(*args, **kwargs):
        # Add custom behavior before calling the original function
        print("Custom behavior before calling the function.")
        result = func(*args, **kwargs) # Call the original function
        # Add custom behavior after calling the original function
        print("Custom behavior after calling the function.")
        return result
      return wrapper
    



    Step 2: Apply Your Custom Decorator

    @my_custom_decorator
    def my_function():
      print("Original function behavior")
    
    my_function()
    


    Output:


    Custom behavior before calling the function.

    Original function behavior

    Custom behavior after calling the function.



    Custom decorators can be tailored to your specific needs. You can perform actions like logging, measuring execution time, caching, and more.


    Use Cases for Custom Decorators


    Now that you know how to create custom decorators let's explore some practical use cases where they can be extremely valuable.


    1. Logging:


      You can create a decorator that logs function calls, their arguments, and their return values. This is useful for debugging and monitoring your code.


    2. Authentication


      Create a decorator to check if a user is authenticated before allowing access to specific routes or functions in a web application.


    3. Caching:


      Implement a caching decorator to store and retrieve function results, reducing computation time for repetitive calls.


    4. Validation:


      Use a decorator to validate input arguments before executing a function. This can help ensure data integrity and security.


    5. Rate Limiting:


      Implement a decorator to restrict the number of times a function can be called within a given timeframe, which is useful for controlling API usage.


    Custom decorators are a powerful feature in Python that can help you maintain clean, organized, and reusable code. By creating your own decorators, you can easily enhance the functionality of your functions and methods while keeping your codebase DRY (Don't Repeat Yourself). Experiment with different use cases, and you'll soon discover the endless possibilities of custom decorators in Python. So, go ahead and dive into the world of custom decorators, and elevate your Python programming skills to new heights!



    Code used in video


    def enforce_arg_types(*arg_types, **kwarg_types):
        def decorator(func):
            def wrapper(*args, **kwargs):
                for i, arg in enumerate(args):
                    if i < len(arg_types) and not isinstance(arg, arg_types[i]):
                        raise TypeError(f"Argument at position {i} must be of type {arg_types[i].__name__}")
                for key, value in kwargs.items():
                    if key in kwarg_types and not isinstance(value, kwarg_types[key]):
                        raise TypeError(f"Keyword argument '{key}' must be of type {kwarg_types[key].__name__}")
                return func(*args, **kwargs)
            return wrapper
        return decorator
    
    
    
    def modify_data(fn):
        def wrapper(name, age):
            if age >= 18:
                return fn(name, age)
            else:
                return f"{name} is not adult"
        return wrapper
    
    
    @enforce_arg_types(str, int)
    @modify_data
    def data(name, age):
        return f"The name is {name}. And age is: {age}"
        
    #data = modify_data(data)    
    
    
    print(data("roger", 13))