Книга: Practical Programming, Fourth Edition
Назад: Common Built-in Exceptions
Дальше: Defining Custom Exceptions

Raising and Chaining Exceptions

Raising an exception in Python is as simple as using the raise keyword. The raise statement takes an exception object, either built-in or user-defined (and, in exceptional situations, objects of other types), and immediately stops normal execution at that point.

 >>>​​ ​​raise​​ ​​ZeroDivisionError
 Traceback (most recent call last):
  File "<python-input-0>", line 1, in <module>
  raise ZeroDivisionError
 ZeroDivisionError
 >>>​​ ​​raise​​ ​​Exception
 Traceback (most recent call last):
  File "<python-input-1>", line 1, in <module>
  raise Exception
 Exception
 >>>​​ ​​raise​​ ​​Exception()
 Traceback (most recent call last):
  File "<python-input-2>", line 1, in <module>
  raise Exception()
 Exception

You should raise exceptions in a function as soon as an error becomes detectable. For example, the function below multiplies a list by a given number of copies. Before doing so, it checks whether the number is positive. If it isn’t, the function raises a ValueError.

Without this check, the function would silently return an empty list, which is likely not what the caller intended:

 >>>​​ ​​def​​ ​​multiply_list(l:​​ ​​list,​​ ​​n:​​ ​​int)​​ ​​->​​ ​​list:
 ...​​ ​​"""Return list `l` replicated `n` times."""
 ...​​ ​​if​​ ​​n​​ ​​<=​​ ​​0:
 ...​​ ​​raise​​ ​​ValueError
 ...​​ ​​return​​ ​​n​​ ​​*​​ ​​l
 ...
 >>>​​ ​​multiply_list([1,​​ ​​2,​​ ​​3],​​ ​​0)
 Traceback (most recent call last):
  File "<python-input-12>", line 1, in <module>
  multiply_list([1, 2, 3], 0)
  ~~~~~~~~~~~~~^^^^^^^^^^^
  File "<python-input-11>", line 3, in multiply_list
  raise ValueError
 ValueError

Here, ValueError is the appropriate exception to raise, not TypeError. We assume that n has the correct type (int), but its value is invalid. If the type itself is incorrect, Python’s multiplication operator will raise a TypeError on its own.

You can also pass arguments to an exception when you raise it. These arguments are stored in the exception object’s args attribute (see ) and provide additional context during debugging:

 >>>​​ ​​def​​ ​​multiply_list(l:​​ ​​list,​​ ​​n:​​ ​​int)​​ ​​->​​ ​​list:
 ...​​ ​​"""Return list `l` replicated `n` times."""
 ...​​ ​​if​​ ​​n​​ ​​<=​​ ​​0:
 ...​​ ​​raise​​ ​​ValueError(n)
 ...​​ ​​return​​ ​​n​​ ​​*​​ ​​l
 ...
 >>>​​ ​​multiply_list([1,​​ ​​2,​​ ​​3],​​ ​​-10)
 Traceback (most recent call last):
  File "<python-input-26>", line 1, in <module>
  multiply_list([1, 2, 3], -10)
  ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^
  File "<python-input-25>", line 3, in multiply_list
  raise ValueError(n)
 ValueError: -10
Exception chaining preserves the original cause of an error, giving you better insight for debugging

Beyond simple error reporting, raise can also be used for exception chaining: a mechanism that lets you preserve the original cause of an error while raising a new, more abstract, higher-level one. This is especially useful when translating low-level exceptions into higher-level errors without losing valuable debugging information.

Consider the following example. The function load_data tries to open a file and anticipates a FileNotFoundError. Instead of handling that exception directly, it raises a new exception, IOError, with an additional message argument, chaining the new exception to the original:

 >>>​​ ​​def​​ ​​load_data(filename:​​ ​​str)​​ ​​->​​ ​​str:
 ...​​ ​​"""Return the contents of the file `filename`."""
 ...​​ ​​try:
 ...​​ ​​with​​ ​​open(filename)​​ ​​as​​ ​​infile:
 ...​​ ​​return​​ ​​infile.read()
 ...​​ ​​except​​ ​​FileNotFoundError​​ ​​as​​ ​​eo:
 ...​​ ​​raise​​ ​​IOError(f​​'Could not load data from {filename}'​​)​​ ​​from​​ ​​eo
 ...
 >>>​​ ​​import​​ ​​traceback
 ...​​ ​​try:
 ...​​ ​​load_data(​​'missing.txt'​​)
 ...​​ ​​except​​ ​​IOError​​ ​​as​​ ​​eo:
 ...​​ ​​print(eo)
 ...​​ ​​print(f​​'Original cause: {eo.__cause__}'​​)
 ...​​ ​​traceback.print_exception(eo)
 ...
 Could not load data from missing.txt
 Original cause: [Errno 2] No such file or directory: 'missing.txt'
 Traceback (most recent call last):
  File "<python-input-2>", line 3, in load_data
  with open(filename) as infile:
  ~~~~^^^^^^^^^^
 FileNotFoundError: [Errno 2] No such file or directory: 'missing.txt'
 
 The above exception was the direct cause of the following exception:
 
 Traceback (most recent call last):
  File "<python-input-5>", line 3, in <module>
  load_data('missing.txt')
  ~~~~~~~~~^^^^^^^^^^^^^^^
  File "<python-input-2>", line 6, in load_data
  raise IOError(f'Could not load data from {filename}') from eo
 OSError: Could not load data from missing.txt

This design accomplishes three important goals:

The from eo syntax in the raise statement copies details from the original exception object (eo) into the new one, where you can access them through the __cause__ attribute. The function traceback.print_exception shows the full call stack for post-mortem analysis.

Use exception chaining when you want to abstract away implementation details from the caller but still retain them for debugging, logging, or troubleshooting.

Назад: Common Built-in Exceptions
Дальше: Defining Custom Exceptions