In general, a decorator is:
By definition, a decorator is a function that takes a function as an argument and it returns another function:
def netSalary(price,tax):
return price *(1+tax)
# decorator function
def currency(fn):
def wrapper(*args, **kwargs):
fn(*args, **kwargs)
return wrapper
The currency
function returns the wrapper
function. The wrapper
function has the *args
and **kwargs
parameters. These parameters allow you to call any fn function with any combination of positional and keyword-only arguments.
In this example, the wrapper function essentially executes the fn function directly and doesn’t change any behavior of the fn function.
In the wrapper function, you can call the fn function, get its result, and format the result as a currency string:
def currency(fn):
def wrapper(*args, **kwargs):
result = fn(*args, **kwargs)
return f'${result}'
return wrapper
The currency
function is now a decorator.
It accepts any function that returns a number and format that number as a currency string.
To use the currency
decorator, you need to pass the netSalary
function to it to get a new function and execute the new function as if it were the original function. For example:
netSalary = currency(netSalary)
print(netSalary(100, 0.05))
Output:
$105.0
In the previous example, the currency is a decorator. And you can decorate the netSalary function using the following syntax:
netSalary = currency(netSalary)
Generally, if decorate is a decorator function and you want to decorate another function fn, you can use this syntax:
fn = decorate(fn)
To make it more convenient, Python provides a shorter way like this:
@decorate
def fn():
pass
For example, instead of using the following syntax:
netSalary = currency(netSalary)
… you can decorate the netSalary
function using the @currency
as follows:
@currency
def netSalary(price, tax):
""" calculate the net price from price and tax
Arguments:
price: the selling price
tax: value added tax or sale tax
Return
the net price
"""
return price * (1 + tax)
Put it all together.
def currency(fn):
def wrapper(*args, **kwargs):
result = fn(*args, **kwargs)
return f'${result}'
return wrapper
@currency
def netSalary(price, tax):
""" calculate the net price from price and tax
Arguments:
price: the selling price
tax: value added tax or sale tax
Return
the net price
"""
return price * (1 + tax)
print(netSalary(100, 0.05))
When you decorate a function:
@decorate
def fn(*args,**kwargs):
pass
It’s equivalent:
fn = decorate(fn)
The decorate
function returns a new function, which is the wrapper function.
If you use the built-in help
function to show the documentation of the new function, you won’t see the documentation of the original function. For example:
help(netSalary)
Output:
wrapper(*args, **kwargs)
None
Also, if you check the name of the new function, Python will return the name of the inner function returned by the decorator:
print(netSalary.__name__)
Output:
wrapper
So when you decorate a function, you’ll lose the original function signature and documentation.
To fix this, you can use the wraps
function from the functools standard module. In fact, the wraps
function is also a decorator.
The following shows how to use the wraps
decorator:
from functools import wraps
def currency(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
result = fn(*args, **kwargs)
return f'${result}'
return wrapper
@currency
def netSalary(price, tax):
""" calculate the net price from price and tax
Arguments:
price: the selling price
tax: value added tax or sale tax
Return
the net price
"""
return price * (1 + tax)
help(netSalary)
print(netSalary.__name__)
Output:
netSalary(price, tax)
calculate the net price from price and tax
Arguments:
price: the selling price
tax: value added tax or sale tax
Return
the net price
netSalary
@
symbol to decorate a function.wraps
function from the functools built-in module to retain the documentation and name of the original function.The follwoing is an exmple of a decorator that prints the messages 5 times:
from functools import wraps
def repeat(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
for _ in range(5):
result = fn(*args, **kwargs)
return result
return wrapper
@repeat
def say(message):
''' print the message
Arguments
message: the message to show
'''
print(message)
say('Hello')
In the example above, it has a hardcoded value of 5 to print 5 times what if you want to make it parameterized so that it print n number of times.
The modified code should look as the following:
from functools import wraps
def repeat(times):
''' call a function a number of times '''
def decorate(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
for _ in range(times):
result = fn(*args, **kwargs)
return result
return wrapper
return decorate
@repeat(10)
def say(message):
''' print the message
Arguments
message: the message to show
'''
print(message)
say('Hello')