Decorators are functions that take another function as an argument for the purpose of extending or replacing the behavior of the passed-in function.
If function A
is a decorator, and function B
is its argument, then function A
modifies, extends, or replaces function B
's behavior without modifying function B
's code.
We say that the decorator function A
wraps function B
.
While we talk about "modifying" behavior, the wrapped function is not actually changed.
Behavior is either added around the wrapped function (and what it returns), or the wrapped function's behavior is substituted for some other behavior.
A higher-order function is a function that accepts one or more functions as arguments and/or returns one or more functions. A function, used as an argument or returned from another function, is a first-class function. A Python function, as a callable object, is a first-class function which can be stored in a variable or used as an argument, much like any other value or object. Higher-order functions and first-class functions work together to make decorators possible.
The @
symbol is prepended to the name of the decorator function and placed just above the function to be decorated, like so:
@decorator
def decorated_function():
pass
Some decorators accept arguments:
@decorator_with_arg(name="Bob")
def decorated_function2():
pass
If a decorator has defined default arguments, you must use parenthesis in the @decorator()
call for the decorator to work, as you would in calling any function:
@decorator_with_default_arg()
def decorated_function3():
pass
If a decorator takes a positional arguments, not supplying the arguments will result in an error which will look something like:
TypeError: decorator_with_pos_arg() missing 1 required positional argument: 'name'
The @decorator
syntax is syntactic sugar or a shorthand for calling the decorating function and passing the decorated function to it as an argument.
Following are examples of alternative ways for calling a decorator:
def function():
pass
function = decorator(function)
def function2():
pass
function2 = decorator_with_arg(name="Bob")(function2)
def function3():
pass
function3 = decorator_with_default_arg()(function3)
Most decorators are intended to extend or replace the behavior of another function, but some decorators may do nothing but return the functions they are wrapping.
Decorators are functions which take at least one argument - the function which they are wrapping. They usually return either the wrapped function or the result of an expression that uses the wrapped function.
A simple decorator - one that simply returns its wrapped function - can be written as follows:
>>> def do_nothing(func):
... return func
...
... @do_nothing
... def function4():
... return 4
...
>>> print(function4())
4
A decorator may only add side effects, such as additional information used for logging:
>>> def my_logger(func):
... print(f"Entering {func.__name__}")
... return func
...
... @my_logger
... def my_func():
... print("Hello")
...
>>> my_func()
Entering my_func
Hello
A decorator does not return itself. It may return its function arguments, another function, or one or more values that replace the return from the passed-in or decorated function. If a decorator returns another function, it will usually return an inner function.
A function can be defined within a function. Such a nested function is called an inner function. A decorator may use an inner function to wrap its function argument. The decorator then returns its inner function. The inner function may then return the original function argument.
Following is an example of a decorator being used for validation:
>>> def my_validator(func):
... def my_wrapper(world):
... print(f"Entering {func.__name__} with {world} argument")
... if ("Pluto" == world):
... print("Pluto is not a planet!")
... else:
... return func(world)
... return my_wrapper
...
... @my_validator
... def my_func(planet):
... print(f"Hello, {planet}!")
...
>>> my_func("World")
Entering my_func with World argument
Hello, World!
...
>>> my_func("Pluto")
Entering my_func with Pluto argument
Pluto is not a planet!
On the first line, we have the definition for the decorator with its func
argument.
On the next line is the definition for the decorators inner function, which wraps the func
argument.
Since the inner function wraps the decorator's func
argument, it is passed the same argument that is passed to func
.
Note that the wrapper doesn't have to use the same name for the argument that was defined in func
.
The original function uses planet
and the decorator uses world
, and the decorator still works.
The inner function returns either func
or, if planet
equals Pluto
, it will print that Pluto is not a planet.
It could be coded to raise a ValueError
instead.
So, the inner function wraps func
, and returns either func
or does something that substitutes what func
would do.
The decorator returns its inner function.
The inner_function may or may not return the original, passed-in function.
Depending on what code conditionally executes in the wrapper function or inner_function, func
may be returned, an error could be raised, or a value of func
's return type could be returned.
Decorators can be written for functions that take an arbitrary number of arguments by using the *args
and **kwargs
syntax.
Following is an example of a decorator for a function that takes an arbitrary number of arguments:
>>> def double(func):
... def wrapper(*args, **kwargs):
... return func(*args, **kwargs) * 2
... return wrapper
...
... @double
... def add(*args):
... return sum(args)
...
>>> print(add(2, 3, 4))
18
>>> print(add(2, 3, 4, 5, 6))
40
This works for doubling the return value from the function argument. If we want to triple, quadruple, etc. the return value, we can add a parameter to the decorator itself, as we show in the next section.
Following is an example of a decorator that can be configured to multiply the decorated function's return value by an arbitrary amount:
>>> def multi(factor=1):
... if (factor == 0):
... raise ValueError("factor must not be 0")
...
... def outer_wrapper(func):
... def inner_wrapper(*args, **kwargs):
... return func(*args, **kwargs) * factor
... return inner_wrapper
... return outer_wrapper
...
... @multi(factor=3)
... def add(*args):
... return sum(args)
...
>>> print(add(2, 3, 4))
27
>>> print(add(2, 3, 4, 5, 6))
60
The first lines validate that factor
is not 0
.
Then the outer wrapper is defined.
This has the same signature we expect for an unparameterized decorator.
The outer wrapper has an inner function with the same signature as the original function.
The inner wrapper does the work of multiplying the returned value from the original function by the argument passed to the decorator.
The outer wrapper returns the inner wrapper, and the decorator returns the outer wrapper.
Following is an example of a parameterized decorator that controls whether it validates the argument passed to the original function:
>>> def check_for_pluto(check=True):
... def my_validator(func):
... def my_wrapper(world):
... print(f"Entering {func.__name__} with {world} argument")
... if (check and "Pluto" == world):
... print("Pluto is not a planet!")
... else:
... return func(world)
... return my_wrapper
... return my_validator
...
... @check_for_pluto(check=False)
... def my_func(planet):
... print(f"Hello, {planet}!")
...
>>> my_func("World")
Entering my_func with World argument
Hello, World!
>>> my_func("Pluto")
Entering my_func with Pluto argument
Hello, Pluto!
This allows for easy toggling between checking for Pluto
or not, and is done without having to modify my_func
.