-5

When I handle some code exception code, I wander why python didn't include what exception will the function raise in its introspect system.For example,when I have to use a function refer to many other function that will raise different exceptions,I have to consider all that happen in my business logic. Like this:

def a():
    raise Exception('exception1')

def b():
    a()
    raise Exception('exception2')

def c():
    b()
    raise Exception('exception3')

def business():
    try:
        c()
    except Exception as e:
        pass

I have to keep digging in the function calls between them that I can know what maybe raise in this code block.And introspect system does not have information of exception.

And as I know, Java will explicitly annotated 'Throw' in function definition,and IDE and programmer can easily know what kinds of exception should I handle.

It will be better if I can know all the exception with object itself,for example:

all_exception = obj.__exceptions__()

So,my question is,why python not include exception introspect in function object.
Who can explain python's design?

dogewang
  • 648
  • 1
  • 7
  • 15
  • 1
    possibly related: https://stackoverflow.com/questions/32560116/how-to-list-all-exceptions-a-function-could-raise-in-python-3 – hiro protagonist Sep 24 '18 at 08:13
  • 2
    Guido van Rossum, probably? – ForceBru Sep 24 '18 at 08:13
  • 2
    What do you mean you don't have information about the exception? You've stored the exception in the variable `e`, and you can do whatever you want with it. – Aran-Fey Sep 24 '18 at 08:13
  • To understand pythonic design simply enter `import this` in a python shell.(and repeat it every time you ask yourself something about python.) When you do this it should give you a sense of why you wouldn't want to write exceptions like this in python(or any other languange). – meissner_ Sep 24 '18 at 08:15
  • Explicit is better than implicit.@ meissner_ – dogewang Sep 24 '18 at 08:19
  • Why are you trying to handle exceptions on the highest level of your code? – roganjosh Sep 24 '18 at 08:19
  • @roganjosh for example,I want to log what exception raise in production environment. – dogewang Sep 24 '18 at 08:21
  • 1
    a) In the generic sense, Python can't know, which imported routines raise which exceptions, since there is no declaration of it as e.g. in Java. b) Your code raising generic `Exception` instances will not win any beauty prices. – guidot Sep 24 '18 at 08:23

1 Answers1

4

Python is a dynamic language, and you can't know, up front, what exceptions a function could throw.

Take this example:

def throw(exception):
    raise exception

What exception will that function raise? I can use throw(ValueError) or throw(TypeError('foobar')), and both would work and are valid Python:

>>> throw(ValueError)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in throw
ValueError
>>> throw(TypeError('foobar'))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in throw
TypeError: foobar

Exceptions are just classes and instances. Current versions of Python require that the exception class must derive from BaseException, but in old Python versions you could even use strings for exceptions (raise "Your mother was a hamster").

And because they are looked up as globals and are not reserved names, you can assign different exceptions to names. The following is legal Python syntax too:

>>> def oops():
...     raise ValueError('Oops')
...
>>> ValueError = TypeError
>>> oops()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in oops
TypeError: Oops

That's why Python functions can't expose what exceptions they raise.

Note that there is never a good reason to use plain Exception. Use one of the standard exceptions where they make sense (ValueError, TypeError, IndexError, KeyError, etc.) or create your own API-specific exceptions by subclassing from Exception or a more specific exception subclass.

Then document your API properly. State what exceptions a developer should expect, where needed. The standard exceptions don't need to be spelled out; it is reasonably obvious that a function that only works on strings will throw TypeError if you pass in a file object instead.

You can use a exception class hierarchy in your business application if you need to catch multiple types:

class BusinessException(Exception):
    """The base exception for all of Business APIs"""

class SpecificBusinessException(BusinessException):
    """Exception that indicates a specific problem occurred"""

class DifferenBusinessException(BusinessException):
    """Exception that indicates a different specific problem occurred"""

then raise the subclassed exceptions and catch BusinessException to handle all, or catch only specific subclasses to customise handling.

If you must figure out what exceptions code raise and accept the risks involved with a dynamic language being able to change the names, then you could use abstract syntax tree (AST) analysis to at least find some information on exceptions. For straight raise Name and raise Name(args..) statements, extracting those names or calls by walking the AST is at least relatively straightforward:

import builtins
import inspect
import ast

class ExceptionExtractor(ast.NodeVisitor):
    def __init__(self):
        self.exceptions = []
    def visit_Raise(self, node):
        if node.exc is None:
            # plain re-raise
            return
        exc_name = node.exc
        if isinstance(exc_name, ast.Call):
            exc_name = exc_name.func
        if not (isinstance(exc_name, ast.Name) and
                isinstance(exc_name.ctx, ast.Load)):
            # not a raise Name or raise Name(...)
            return
        self.exceptions.append(exc_name.id)

def global_exceptions_raised(func):
    """Extract the expressions used in raise statements

    Only supports raise Name and raise Name(...) forms, and
    only if the source can be accessed. No checks are made for the
    scope of the name.

    returns references to those exception names that can be loaded from
    the function globals.

    """
    source = inspect.getsource(func)
    tree = ast.parse(source)
    extractor = ExceptionExtractor()
    extractor.visit(tree)
    fglobals = {**func.__globals__, **vars(builtins)}
    return [fglobals[name] for name in extractor.exceptions if name in fglobals]
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343