Function decorators allow us to modify the behavior of a function without having to change the source code of the decorated function.
They are used to wrap a function, modify its behavior, and return the modified function or a new function with the same or different name.
Decorators are quite common in Python and programming in general, Python offers an easier and more convenient syntax to work with them. The syntax as shown below:
@decorator_function def function_to_decorate(): <block>
A decorator function is a higher order function that takes a function as an argument and returns a new function commonly referred to as a wrapper . The wrapper function is used to modify or extend the behavior of the original function, it is also responsible of invoking the original function.
Without using the short hand syntax, we would need to explicitly pass the function to decorate as an argument to the decorator function, as shown below:
When you call a decorated function with arguments, the arguments are first passed to the wrapper function, which then forwards them to the original function.
If the wrapper function does not define any parameters and arguments are given to the decorated function, an error will be raised .
It is common to define wrapper functions which take arbitrary arguments as shown below:
A timing decorator function:
Decorator functions with arguments:
Decorator functions can also take additional arguments besides the function being decorated. The syntax of calling such a decorator is as shown below;
@my_decorator(arguments) def my_function(): <block>
Defining a decorator function that takes additional arguments requires a slightly different syntax. Instead of defining the decorator function to take a single function as an argument, we define it to take any number of arguments, including the function being decorated. Then we define an inner function that takes the function as its argument and returns a wrapper function that uses the additional arguments passed to the decorator. Example:
Class-based Decorators
Decorators are normally implemented as functions, but they can also be written using classes. Class-based decorators provide more flexibility, especially when you need to maintain state between function calls or to implement more complex behavior.
A class-based decorator should implement the __call__ method, which makes an instance of the class callable, just like a function. Inside __call__ is where you modify original function’s behavior.
In the following example, we implement the LogCall decorator for logging function calls.
class-based decorators with arguments
The implementation of a class-based decorator with arguments is significantly different to without(as in previous example). Instead of taking the function in __init__, the decorator arguments are passed first, and the function is passed later in __call__.
class Decorator: def __init__(arg1, arg2): ... def __call__(func): ...
Let's modify the LogCall decorator to accept arguments
Compare the above implementation with the previous one, you will observe that in the above implementation, the arguments to the decorator are passed through __init__ while the function to be decorated is passed through __call__. That is a very important detail to keep in mind.
Stateful Decorators
Class-based decorators are especially useful for maintaining state across multiple function calls. The state is maintained through instance attributes and methods.
In the following example we will implement a call counter decorator which can track how many times a function has been executed.
Multiple decorator functions
When multiple decorations are applied to a Python function, they are evaluated and applied in the order in which they appear. The first decoration will wrap the original function, and each successive decoration will wrap the result of the previous decoration. This means that the innermost decorator will be evaluated and applied first, followed by the decorators that wrap it from the outside in. Example:
Decorator for classes
We can also create decorators that operates on entire classes instead of individual functions. Class decorators make it possible to alter class behavior, add new functionality, or implement design patterns in a clean, reusable way.
A class decorator is simply a function that:
-
Takes a class as input
-
Modifies the class or creates a new one
-
Returns the modified class
The following snippet shows a very simple class decorator.
let us look at a more practical example:
Differences from Function Decorators
-
Operate on class definitions rather than functions
-
Can modify multiple methods at once
-
Have access to the entire class structure
-
Execute when the class is defined (not when instantiated)
Common Built-in Python Decorators
Python comes with several powerful built-in decorators that solve common problems. These decorators are readily available in the standard library.
@classmethod and @staticmethod
These decorators define methods that don't operate on instance data:
-
@classmethod: for implementing methods that are bound to the class instead of instances. -
@staticmethod: Utility functions logically related to the class but independent of instances
@property
The @property decorator transforms a method into a "getter" for a read-only attribute, while @<property>.setter and @<property>.deleter handle write and delete operations.
@functools.wraps
The @wraps decorator is used in decorators to preserve the original/decorated function's name, docstring, and other attributes.
@functools.lru_cache
Used for caching expensive function calls to avoid repeated computation.
@dataclasses.dataclass(Python 3.7+)
Automatically generates special methods for classes can be used to reduce boilerplate code.
When to use decorator Functions
Decorators can be used to add extra functionality to an existing function, such functionality can include:
- Logging
- Caching
- Rate limiting
- Authentication/Authorization
- Input/Output Sanitization
- Executing functions in parallel.
- Adding additional features to functions like argument binding, error handling and retry logic.
- Adding context managers to functions.
Python Decorator FAQS
Q.What exactly is a Python decorator?
A decorator is a design pattern in Python that allows you to modify or extend the behavior of functions or classes without permanently changing their source code. It's essentially a function that takes another function/class and returns a modified version.
Q.What's the difference between @ syntax and manual decoration?
Both approaches are equivalent, the @ syntax is just syntactic sugar that makes decorators cleaner and more readable.
# Using @ syntax (most common) @my_decorator def my_function(): pass # Manual decoration (what actually happens) def my_function(): pass my_function = my_decorator(my_function)
Q.How do decorators with arguments work?
Decorators with arguments require an extra level of nesting:
Q.Can decorators be stacked? How does order matter?
Yes, decorators can be stacked, and order is important - they execute from bottom to top:
@decorator1 @decorator2 @decorator3 def my_function(): pass # Equivalent to: my_function = decorator1(decorator2(decorator3(my_function)))
Q.How do class decorators differ from function
Class decorators operate on entire classes instead of individual functions.