3
\$\begingroup\$

This is a script that utilizes lazy exception throwing. What I do is decorate a function that might throw an exception to throw an exception with a formatted string.

from functools import wraps

def on_fail(message):
    def on_fail_dec(func):
        @wraps(func)
        def on_fail_wrapper(*args, **kw):
            ret = None
            success = True
            message_addon = ""
            try:
                ret = func(*args, **kw)
            except Exception as e:
                success = False
                message_addon = str(e)
            # raise Exception if failed
            if not success:
                raise Exception("{0}\nBecause: {1}".format(message, message_addon))

            return ret
        return on_fail_wrapper
    return on_fail_dec

# Usage :

@on_fail("Division Failed")
def divide(a, b):
    return a / b

test_cases = [(1, 2), (20, 5), (3, 0)]

for a,b in test_cases:
    print("Trying to divide {0} from {1}".format(a, b))
    print(divide(a, b))

Output:

Trying to divide 1 from 2
0.5
Trying to divide 20 from 5
4.0
Trying to divide 3 from 0
Traceback (most recent call last):
  File "<string>", line 30, in <module>
  File "<string>", line 16, in on_fail_wrapper
Exception: Division Failed
Because: division by zero
\$\endgroup\$
2
  • \$\begingroup\$ I don't understand, what do mean by lazy exception throwing here? \$\endgroup\$ Feb 17, 2015 at 6:53
  • \$\begingroup\$ @AshwiniChaudhary: I want to fail fast raising an Exception with a custom message. \$\endgroup\$
    – JaDogg
    Feb 17, 2015 at 8:19

2 Answers 2

5
\$\begingroup\$
  • Instead of handling just "Division Failed" you should extend your function to handle any possible error, plus it's better if you pass the exception object along with the message to the decorator as well, by doing this you'll know exactly which error you're going to handle because a except Exception will handle any Exception not just what we actually intended it to.

  • Second there's no need to use flag variables here, we can simply return the return value from the try-block and in case of exception we re-raise the exception after it is caught.

Code:

from functools import wraps


def on_fail(exception, message):
    def on_fail_dec(func):
        @wraps(func)
        def on_fail_wrapper(*args, **kw):
            message_addon = ""
            try:
                # do something before
                ret = func(*args, **kw)
                # do something after
                return ret
            except exception as e:
               message_addon = str(e)
            raise exception("{0}\nBecause: {1}".format(message, message_addon))
        return on_fail_wrapper
    return on_fail_dec


@on_fail(ZeroDivisionError, "Division Failed")
def divide(a, b):
    return a / b

test_cases = [(1, 2), (20, 5), (3, 0)]

for a, b in test_cases:
    print("Trying to divide {0} from {1}".format(a, b))
    print(divide(a, b))

Note that in above code I didn't use something simple like:

except exception as e:
    raise exception("{0}\nBecause: {1}".format(message, e))

That's because Python 3 introduced something called exception chaining, which means if an exception occurs while handling another exception then the previous exception still remains available to us(Read PEP 3134 - PEP 3134 -- Exception Chaining and Embedded Tracebacks), but this is not useful here as it will end up raising something like:

Trying to divide 3 from 0
Traceback (most recent call last):
  File "/home/ashwini/py/so.py", line 10, in on_fail_wrapper
    ret = func(*args, **kw)
  File "/home/ashwini/py/so.py", line 20, in divide
    return a / b
ZeroDivisionError: division by zero

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/ashwini/py/so.py", line 26, in <module>
    print(divide(a, b))
  File "/home/ashwini/py/so.py", line 13, in on_fail_wrapper
    raise exception("{0}\nBecause: {1}".format(message, e))
ZeroDivisionError: Division Failed
Because: division by zero
\$\endgroup\$
1
  • \$\begingroup\$ How about assigning exception = type(e) inside the except block, to wrap all exceptions? That would make sense to me if the intent is to describe what failed, while the original exception describes why? \$\endgroup\$ Feb 17, 2015 at 7:57
3
\$\begingroup\$

You don't need to initialize the local variables, and some of them you don't need at all:

  • ret : instead of saving the return value, you can return directly. If an exception happens you will re-raise, so there will be no other return value except the normal one
  • success : if nothing is raised, that by itself is success. If something was raise, you'll be out of the try block, that's how you'll know it was not a success
  • message_addon : this you do need to save in the except block, but you don't need to initialize it beforehand

Considering the above, the implementation can be simplified:

def on_fail(message):
    def on_fail_dec(func):
        @wraps(func)
        def on_fail_wrapper(*args, **kw):
            try:
                return func(*args, **kw)
            except Exception as e:
                message_addon = str(e)
            raise Exception("{0}\nBecause: {1}".format(message, message_addon))
        return on_fail_wrapper
    return on_fail_dec
\$\endgroup\$

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.