Python Class Decorators

Introduction

Python class decorators extend the functionality of classes in a manner similar to how function decorators extend functions. They allow you to modify or enhance the behavior of classes without directly altering their source code. Class decorators are particularly useful for adding features like validation, caching, or logging to classes in a flexible and reusable way.

Understanding Python Class Decorators

In Python, a class decorator is a function that takes a class as input and returns a modified version of that class. This modified class typically includes additional attributes, methods, or behavior alongside the original class definition.

Other than modifying a class rather than a function, class decorators are very similar to regular decorators that modify functions. Like those decorators, class decorators can be applied to classes using the @ syntax, and can also be take arguments when implemented with factory functions.

Modifying a Class Using Inheritance

A class decorator simply alters a class and then returns it, however there are multiple ways of modifying a class. This section will demonstrate how to do so using class inheritance.

Here is sample code that demonstrates the basic structure of a class decorator that uses inheritance:

def class_decorator(original_class):
    class ModifiedClass(original_class):
        # Add additional attributes or methods here
        ...

    return ModifiedClass

These are the key elements:

  • class_decorator: The decorator function that takes the original class as its argument.
  • original_class: The class to be modified or decorated.
  • ModifiedClass: An inner class that creates the modified version of the original class. It inherits from original_class, and may include additional attributes or methods.

In the code above, the class_decorator() function is the decorator (line 1). It accepts a class in the original_class parameter and defines a modified version of that class, ModifiedClass (line 2), which inherits from original_class. Inside ModifiedClass, you can add or modify any attributes or methods, as needed. The decorator then returns this modified class. class_decorator() returns the modified class at the end (line 6).

The following example creates a Python class decorator that adds a function that prints the names of all the functions in a class.

Example:

def add_function_name_printer(original_class):
    class ClassWithFunctionNamePrinter(original_class):
        def print_function_names(self):
            function_names = [func for func in dir(self) if callable(getattr(self, func)) and not func.startswith("__")]
            print(f"The names of my functions are: {', '.join(function_names)}")

    return ClassWithFunctionNamePrinter

@add_function_name_printer
class MyClass:
    def func1(self):
        pass

    def func2(self):
        pass

    def func3(self):
        pass

obj = MyClass()
obj.print_function_names()

Output:

The names of my functions are: func1, func2, func3

In the example above, the add_function_name_printer() function is a class decorator that takes the original class as input (line 1). Within this decorator, a new class ClassWithFunctionNamePrinter is defined (line 2), inheriting from original_class. Inside ClassWithFunctionNamePrinter, the method print_function_names() is added, which prints the names of all callable functions within the class except those starting with double underscores (__). Finally, the modified class is returned.

Using the @ syntax, the add_function_name_printer decorator is applied to the MyClass definition (line 9). After instantiation, the instance obj of MyClass is created (line 17). Invoking obj.print_function_names() runs the added method, printing the names of all functions within MyClass.

While code responsible for getting the function names may not be essential for understanding class decorators, it is interesting nonetheless, and therefore, let’s go over it.

The code is [func for func in dir(self) if callable(getattr(self, func)) and not func.startswith("__")] (line 4). It is a list comprehension that dynamically retrieves the names of callable functions (methods) within a class instance while excluding any special methods (those starting with double underscores, which are typically reserved for built-in or special methods in Python classes).

Here are the components of that code:

  • dir(self): This function returns a list of names of attributes and methods of the object self. (Each name is a string.)
  • getattr(self, func): This function retrieves the value of the attribute whose name is stored in func from the object self.
  • callable(getattr(self, func)): This condition checks whether the value obtained from getattr(self, func) is callable, meaning it is a function or method.
  • func.startswith("__"): This condition checks whether the name of the function starts with double underscores, indicating it’s a special method (such as __init__, __str__, etc.).

The list comprehension iterates over all attributes and methods returned by dir(self). For each name, it checks whether it’s attribute is callable and not a special method. If both conditions are met, the name is included in the resulting list.

Modifying a Class By Setting Its Attributes With setattr()

In addition to using class inheritance, another approach to modifying a class with decorators involves directly setting attributes of the class with the setattr() function. This method offers an alternative suitable for adding simple functionalities or attributes to a class. It also preserves the metadata of the modified class, as the wraps decorator does for functions.

Here is sample code that demonstrates the basic structure of a class decorator that uses setattr():

def class_decorator(class_to_modify):
    def function_to_add(self, param1, param2, ...)
        # Function code
        ...

    setattr(class_to_modify, 'function_to_add', function_to_add)
    return class_to_modify

These are the key elements:

  • class_decorator: The decorator function that takes the original class as its argument.
  • class_to_modify: The class to be modified or decorated.
  • function_to_add(self, param1, param2, ...)): An inner function, which has parameters as needed, to be added to the class. Since it is to become an instance function of a class, it must take the object itself for its first parameter, self.
  • setattr(class_to_modify, 'function_to_add', function_to_add): The code that adds function_to_add() to the class. The first argument is the class, the second is the name (a string) the function will have in the class, and the third is the inner function to be added. Note that the name of the function in the class is not required to match the name of the inner function.

In the next example, the decorator from the previous example is re-written to use setattr() to modify the class.

Example:

def add_function_name_printer(class_to_modify):
    def print_function_names(self):
        function_names = [func for func in dir(self) if callable(getattr(self, func)) and not func.startswith("__")]
        print(f"Functions in class {self.__class__.__name__}: {function_names}")

    setattr(print_function_names, 'print_function_names', print_function_names)
    return class_to_modify

In the example above, the add_function_name_printer() class decorator is re-implemented to utilize setattr() for modifying the class. Within this decorator, an inner function print_function_names() is defined to print the names of callable functions within the class instance (line 2). Then, the setattr() function is employed to add this inner function to the class (line 6).

Using Class Decorators for Metaprogramming

Class decorators are useful tools for metaprogramming in Python. They allow you to dynamically modify class behavior based on runtime conditions or external factors. This can be utilized in frameworks, libraries, or APIs where you want to extend or customize class behavior without modifying the original classes themselves.

For example, you can use class decorators to implement features such as:

  • Automatic property generation
  • Method wrapping for logging or profiling
  • Attribute validation or type checking
  • Dependency injection or aspect-oriented programming

Summary & Reference for Python Class Decorators

Python class decorators offer a way to modify or enhance the behavior of classes without directly altering their source code. Similar to function decorators, class decorators allow developers to add features such as validation, caching, or logging in a flexible and reusable manner.


Class decorators are defined as functions that take a class as input and return a modified version of that class. They can be applied using the @ syntax and can also accept arguments when implemented with factory functions.

@class_decorator(arg)
class MyClass:
    ...

There are multiple approaches to implementing class decorators. One method involves modifying a class using inheritance, where a modified version of the original class is created within the decorator function.

def class_decorator(original_class):
    class ModifiedClass(original_class):
        ...
    return ModifiedClass

Alternatively, class decorators can modify a class by directly setting attributes using the setattr() function. This approach is suitable for adding simple functionalities or attributes to a class, and it also preserves the metadata of the decorated class.

def class_decorator(class_to_modify):
    def function_to_add(self, param1, param2, ...):
        ...
    setattr(class_to_modify, 'function_to_add', function_to_add)
    return class_to_modify

Class decorators are useful tools for metaprogramming in Python, allowing dynamic modification of class behavior based on runtime conditions or external factors. They find applications in frameworks, libraries, or APIs for features like automatic property generation, method wrapping, attribute validation, and dependency injection.