Your Page Title
🔍

    Python Decorator

    A decorator in Python is the design pattern that enables one to modify or extend the behavior of functions or methods without permanently changing them. This is a powerful, flexible feature of Python, extensively applied for logging, access control enforcement, instrumentation, memoization, and much more.

    Key Concepts

    1. First-Class Functions:
    • Functions are first-class objects in Python; that means they can be passed as arguments to other functions, returned from functions, and assigned to variables.

    2. Higher-Order Functions:

      • A function that takes another function as an argument or returns a function is called a higher-order function.


      3. Closures:

      • A closure is a function object that has access to variables in its scope even after the outer function has finished executing.

      What is a Decorator?

      A decorator is essentially a higher-order function that takes a function as input and returns a new function with added functionality.

      Syntax

      The @decorator_name syntax is a shorthand for applying a decorator to a function. For example:

      @decorator_name
      def my_function():
          pass

      is equivalent to:

      def my_function():
          pass
      my_function = decorator_name(my_function)

      Basic Example

      def simple_decorator(func):
          def wrapper():
              print("Before the function call")
              func()
              print("After the function call")
          return wrapper
      
      @simple_decorator
      def say_hello():
          print("Hello!")
      
      say_hello()

      Output:

      Before the function call
      Hello!
      After the function call
      • simple_decorator: Takes say_hello as an argument and wraps its behavior in wrapper.
      • wrapper: Adds additional behavior before and after calling the original function.

      Decorators with Arguments

      To create a decorator that accepts arguments, you need a function that returns a decorator.

      def decorator_with_args(arg):
          def decorator(func):
              def wrapper(*args, **kwargs):
                  print(f"Decorator argument: {arg}")
                  return func(*args, **kwargs)
              return wrapper
          return decorator
      
      @decorator_with_args("Hello")
      def greet(name):
          print(f"Greetings, {name}!")
      
      greet("Alice")

      Output:

      Decorator argument: Hello
      Greetings, Alice!

      Common Use Cases

      1. Logging

      def log_decorator(func):
          def wrapper(*args, **kwargs):
              print(f"Calling {func.__name__} with arguments {args} and {kwargs}")
              result = func(*args, **kwargs)
              print(f"{func.__name__} returned {result}")
              return result
          return wrapper
      
      @log_decorator
      def add(a, b):
          return a + b
      
      add(2, 3)

      Output:

      Calling add with arguments (2, 3) and {}
      add returned 5

      2. Authentication

      def authenticate_decorator(func):
          def wrapper(user, *args, **kwargs):
              if user.get("is_authenticated"):
                  return func(user, *args, **kwargs)
              else:
                  print("Authentication failed!")
          return wrapper
      
      @authenticate_decorator
      def show_dashboard(user):
          print(f"Welcome, {user['name']}!")
      
      user = {"name": "Alice", "is_authenticated": True}
      show_dashboard(user)

      Output:

      Welcome, Alice!

      3. Memoization (Caching)

      def memoize(func):
          cache = {}
          def wrapper(*args):
              if args in cache:
                  return cache[args]
              result = func(*args)
              cache[args] = result
              return result
          return wrapper
      
      @memoize
      def factorial(n):
          if n == 0:
              return 1
          return n * factorial(n - 1)
      
      print(factorial(5))  # Calculated
      print(factorial(5))  # Cached

      Using functools.wraps

      To preserve the metadata of the original function (like its name, docstring, etc.), use functools.wraps.

      from functools import wraps
      
      def simple_decorator(func):
          @wraps(func)
          def wrapper(*args, **kwargs):
              print("Before the function call")
              result = func(*args, **kwargs)
              print("After the function call")
              return result
          return wrapper
      
      @simple_decorator
      def say_hello():
          """This is a greeting function."""
          print("Hello!")
      
      print(say_hello.__name__)  # Outputs: say_hello
      print(say_hello.__doc__)   # Outputs: This is a greeting function.

      Key Points to Remember

      1. Decorators are one of the ways to extend the functionality of functions.
      2. The @decorator syntax is a short form of passing a function to a decorator.
      3. Use *args and **kwargs to handle functions with varying arguments.
      4. Use functools.wraps to retain the original function’s metadata.
      5. Decorators can be stacked. Many decorators can be applied to one function.