Decorators¶
What is a decorator?¶
A decorator is an object (e.g. a function) that can be invoked by passing it the object to be decorated (another function) as an argument. The value returned by this call is used in place of the decorated object.
The decorator is used by inserting a line starting with @ in front of the definition of the decorated object.
Decorators in Python are based primarily on two assumptions:
- a function may take another function as an argument,
- inside a function you can create another function.
Examples¶
Simple decorator¶
from datetime import datetime
def disable_at_night(func):
def wrapper():
if 7 <= datetime.now().hour < 22:
func()
return wrapper
@disable_at_night
def say_something():
print("Hello world")
say_something()
By decorating, the function say_something will only be called when the current time is between 7 and 22.
First, we create a function and name it what we want the decorator to be (disable_at_night in this case), passing the function as an argument. Then inside we create another function, in this case wrapper, in the body of which we call the function previously passed as an argument, adding a time constraint in the if statement. We must also remember to include the name of the internal function after the return clause. We precede the function we want to decorate with @disable_at_night.
A decorator with arguments¶
from datetime import datetime
def run_only_between(from_=7, to_=22):
def dec(func):
def wrapper():
if from_ <= datetime.now().hour < to_:
func()
return wrapper
return dec
@run_only_between(10, 15)
def say_something():
print("Hello world")
say_something()
As we can see, this example is a generalization of the previous one - the run_only_between decorator takes two arguments, on the basis of which it determines whether the function being decorated should be run, depending on what time it is.
First, we create a run_only_between function that takes two arguments (decorator arguments). Then inside we create a function, in this example called dec, which we pass as an argument to the function. Then inside we create another function, wrapper here, in whose body we call the previously passed function as argument, adding a time constraint in the if statement. We need to remember to return all inner functions, so first, after the return clause, we give the name of the innermost function, which is wrapper, and then return dec, remembering about the appropriate indentation. We precede the function that we want to decorate with @run_only_between and specify the hours in the arguments (but this is not required, because in the function definition we can see that default values have been specified).
Decorator with arguments and function with arguments¶
So far, we've only decorated functions that take no arguments themselves.
@run_only_between(10, 15)
def hello(name):
print(f"Hello, {name}")
hello("Mark")
Calling the above function, which we pass something in the argument, will return:
Traceback (most recent call last):
File "program.py", line 26, in <module>
hello("Mark")
TypeError: wrapper() takes 0 positional arguments but 1 was given
The error message tells us that we need to make a modification to the decorator definition. If we want to decorate functions that take arguments, we must remember that the decorator takes them as well, in the innermost function - in this case wrapper. In order not to give specific argument names and to make the decorator work for any function, we can use passing all positional and named arguments by using *args and **kwargs.
from datetime import datetime
def run_only_between(from_=7, to_=22):
def dec(func):
def wrapper(*args, **kwargs):
if from_ <= datetime.now().hour < to_:
func(*args, **kwargs)
return wrapper
return dec
@run_only_between(10, 15)
def say_something():
print("Hello world")
say_something()