Apologies, I can only imagine that StackOverflow is full of people who are almost there, but still don't quite grasp decorators.
I am trying to decorate a series of os-related functions so that if there are any exceptions such as FileNotFoundError or PermissionError, the user can fix the problem on their side and try again.
So I've created this toy function and decorator, and I don't understand where I'm not following properly the example decorators I've been reading, and I'm having trouble reasoning my way through it:
from functools import wraps
def continual_retry(func):
def retry_decorated(*args, **kwargs):
@wraps(func)
def func_wrapper(*args, **kwargs):
while not stop:
try:
func(*args)
stop = True
except Exception as e:
print(f'Could not perform function {func.__name__}')
print(f' with args {repr(args)}')
print(f' due to error {e.class__}')
redo = input('Retry (y/n)? ')
if redo.lower() != 'y':
print('Exiting program due to error and user input')
sys.exit(0)
return func_wrapper
return retry_decorated
@continual_retry
def divide(a, b):
return a/b
When I run the function divide
, this is the result:
>>> divide(1, 2)
<function __main__.divide(a, b)>
Where I was expecting a result of
0.5
(Then I was going to test divide(1, 0)
)
Your decorator is a decorator factory that returns another decorator. You don't need a factory here, remove one layer:
def continual_retry(func):
@wraps(func)
def func_wrapper(*args, **kwargs):
while True:
try:
return func(*args, **kwargs)
except Exception as e:
print(f'Could not perform function {func.__name__}')
print(f' with args {repr(args)}')
print(f' due to error {e.class__}')
redo = input('Retry (y/n)? ')
if redo.lower() != 'y':
print('Exiting program due to error and user input')
sys.exit(0)
return func_wrapper
You also need to return the function result, and I changed the while
loop to an endless one with while True:
, as a successful return
will exit the loop. I also updated the call to func()
to pass along keyword arguments (return func(*args, **kwargs)
).
When Python encounters @continual_retry
, it passes in the function object to the continual_retry()
callable to replace the function with the result, as if you expected divide = continual_retry(divide)
, but in your version continual_retry(divide)
returns the retry_decorated()
function, which itself, when called, finally returns the func_wrapper
object. You want func_wrapper
to be used to be the replacement.
Your double-layer approach is great when you want to configure a decorator, where the outer decorator factory function accepts arguments other than the function. The goal is to then use that as @continual_retry(config_arg1, config_arg2, ...)
so that Python first calls that function to get a return value, and the decoration then happens by calling that return value.
You could, for example, add an option to limit the number of retries:
def continual_retry(limit=None):
def retry_decorator(func):
@wraps(func)
def func_wrapper(*args, **kwargs):
retries = 0
while True
try:
return func(*args, **kwargs)
except Exception as e:
print(f'Could not perform function {func.__name__}')
print(f' with args {repr(args)}')
print(f' due to error {e.class__}')
redo = input('Retry (y/n)? ')
if redo.lower() != 'y':
print('Exiting program due to error and user input')
sys.exit(0)
retries += 1
if limit is not None and retries > limit:
# reached the limit, re-raise the exception
raise
return func_wrapper
return retry_decorator
Now you must use @continual_retry()
or @continual_retry(<integer>)
when decorating, e.g.:
@continual_retry(3)
def divide(a, b):
return a / b
because it is continual_retry()
that produces the decorator, and continual_retry(3)(divide)
produces the wrapper that replaces the original function.