3

How to pass try function and exception handler function in decorator way

For example, this is the WCF connection.

def wcf():
    def send_request(result):
        # establish connection...
        result["response"] = True

    def handle_execption(e, result):
        # logger something
        result["error"] = str(e)
    result = {}
    try:
        send_request(result)
    except Exception as e:
        handle_execption(e, result)

Now, I want to add a retry mechanism to this connection. What is the best way to achieve that, I have multiple connection ways(like REST, SOAP WCF, etc)? In general, they share the same pattern and all have the send_request and handle_execption.

The hard-code one is like the following, but it's quite silly to add the same logic to every protocol.

for attempt in range(0, 3):
    sleep_seconds = attempt ** 2
    sleep(sleep_seconds)

    try:
        send_request(result)
        break
    except Exception as e:
        handle_execption(e, result)
4
  • 1
    You don't have to increase attempt in the for loop, as well as using if statement to sleep Commented May 17, 2020 at 21:01
  • Oh, you are right, thanks @AlexanderGolys Commented May 17, 2020 at 21:47
  • Shouldn't result be a dict instead of a list, i.e. result = {} instead of result = []? Commented May 17, 2020 at 22:37
  • @md2perpe, exactly.... thanks Commented May 17, 2020 at 22:39

2 Answers 2

2

If you always handle exceptions in the same way, you can write a decorator that hardcodes that behavior and handles the retrying logic:

def retry_decorator(base_function):
    def new_function(*args, **kwargs):  # This allows you to decorate functions without worrying about what arguments they take
        for attempt in range(3):
            sleep_seconds = attempt ** 2
            sleep(sleep_seconds)

            try:
                return base_function(*args, **kwargs)  # base_function is whatever function this decorator is applied to
            except Exception as e:
                print(e)  # Replace this with whatever you want, as long as it's the same for all possible base_functions

    return new_function


@retry_decorator  # Replaces send_request() with retry_decorator(send_request)()
def send_request(result):
    result["response"] = True

If you want to use different exception handling logic for the different connections, things get a bit more complicated:

def retry_decorator_2(exception_handle_function):
    def new_decorator(base_function):
        def new_function(*args, **kwargs):
            for attempt in range(3):
                sleep_seconds = attempt ** 2
                sleep(sleep_seconds)

                try:
                    return base_function(*args, **kwargs)
                except Exception as e:
                    exception_handle_function(e)

        return new_function

    return new_decorator


@retry_decorator_2(print)  # Replace 'print' with an exception handling function
def send_request(result):
    result["response"] = True

In this case, retry_decorator_2(print) creates a new decorator (which stores print as the exception_handle_function), then applies that decorator to the send_request function.

If you need the exception_handle_function to take arguments, you can pass them as part of the decorator:

def retry_decorator_3(exception_handle_function, *exception_function_args, **exception_function_kwargs):
    def new_decorator(base_function):
        def new_function(*args, **kwargs):
            for attempt in range(3):
                sleep_seconds = attempt
                sleep(sleep_seconds)

                try:
                    return base_function(*args, **kwargs)
                except Exception as e:
                    exception_handle_function(e, *exception_function_args, **exception_function_kwargs)

        return new_function

    return new_decorator


@retry_decorator_3(print, 1, 2, 3)  # 1, 2, 3 become arguments for the print function, or whatever you replace it with
def send_request(result):
    result["response"] = True
Sign up to request clarification or add additional context in comments.

1 Comment

This is awesome, one more question. What if we want to pass arguments into exception_handle_function? @water_ghosts
0

Why decorator? I think that it's just as clean with

def wcf():
    def send_request(result):
        # establish connection...
        result["response"] = True

    def handle_exception(e, result):
        # logger something
        result["error"] = str(e)

    return retry(send_request, handle_exception)


def retry(send_request, handle_exception):
    result = {}

    for attempt in range(0, 3):
        sleep(attempt ** 2)

        try:
            send_request(result)
            return result

        except Exception as e:
            return handle_execption(e, result)

1 Comment

That is a good point. If we are being stubborn and for learning perspective, do you know how to rewrite them as decorator?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.