Python Class Decorators: Construction and Evaluation
Class decorators in Python provide a clean, declarative syntax for modifying or enhancing class definitions dynamically. This article explores how Python constructs class decorators under the hood, how they alter or replace class objects, and the precise timing of their evaluation during a program's lifecycle.
What Is a Class Decorator?
A class decorator is a callable—typically a function or another class—that accepts a class object as its sole argument and returns either the modified class, an entirely new class, or a wrapper object.
Syntactically, applying a class decorator looks like this:
@my_decorator
class MyClass:
passBehind the scenes, the @ syntax is syntactic sugar.
Python translates the code above into an explicit function call
immediately after the class body executes:
class MyClass:
pass
MyClass = my_decorator(MyClass)How Python Constructs Class Decorators
To understand how decorators operate on classes, it helps to understand how Python builds a class in the first place:
- Body Execution: Python executes the class body as a code block within a dedicated namespace dictionary.
- Class Creation: Python invokes the metaclass (by
default,
type) using the class name, base classes, and the populated namespace to construct the class object in memory. - Decorator Application: Once the class object is instantiated, Python immediately passes that class object into the decorator callable.
- Rebinding: The original identifier
(
MyClass) is rebound to whatever object the decorator returns.
Implementing a Basic Class Decorator
A decorator can mutate the class directly and return it:
def add_greeting(cls):
cls.greeting = "Hello, World!"
return cls
@add_greeting
class User:
pass
print(User.greeting) # Outputs: Hello, World!Alternatively, a decorator can wrap the original class in an entirely new class or proxy, intercepting instantiation or method calls:
def singleton(cls):
instances = {}
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton
class DatabaseConnection:
passIn this case, DatabaseConnection no longer points to the
class itself, but to the get_instance closure.
When Are Class Decorators Evaluated?
Class decorators are evaluated at definition time (often referred to as import time), not at runtime when instances of the class are created.
When Python imports a module or encounters a class block during execution:
- The class body runs from top to bottom.
- The class object is constructed.
- The decorator function runs immediately.
Demonstrating Evaluation Timing
Consider this example showing the exact execution order:
def track_decorator(cls):
print("Decorator function evaluated.")
return cls
print("Before class definition.")
@track_decorator
class Order:
print("Inside class body.")
print("After class definition.")
# Creating instances later
order1 = Order()
order2 = Order()Output:
Before class definition.
Inside class body.
Decorator function evaluated.
After class definition.
Notice that track_decorator runs before any
Order instance is created and before the code following the
class definition executes. Instantiating Order() does not
re-trigger the decorator.
Decorators with Arguments
When a decorator takes arguments, it requires an extra layer of construction known as a decorator factory.
def set_tag(tag_name):
print(f"1. Factory called with tag: {tag_name}")
def decorator(cls):
print(f"2. Decorator applied to {cls.__name__}")
cls.tag = tag_name
return cls
return decorator
@set_tag("admin")
class Account:
passEvaluation Order with Arguments:
- Python encounters
@set_tag("admin")and evaluatesset_tag("admin")first, producing the actual decorator function. - Python executes the body of
Accountand creates the class object. - Python calls the resulting decorator with
Accountas the argument. Accountis rebound to the decorator's return value.