Python learning notes - exception handling (try/except, except...as, handling _exit _exceptionsof context manager, custom exceptions)

Exception type

Python built-in exception

The interpreter uses a hierarchy to organize exceptions
All built-in exceptions (including custom exceptions) inherit a class named Exception

BaseException
 +-- SystemExit
 +-- KeyboardInterrupt
 +-- GeneratorExit
 +-- Exception
      +-- StopIteration
      +-- StopAsyncIteration
      +-- ArithmeticError
      |    +-- FloatingPointError
      |    +-- OverflowError
      |    +-- ZeroDivisionError
      
      ...
      
      +-- NameError
      |    +-- UnboundLocalError
      +-- OSError
      |    +-- BlockingIOError
      |    +-- ChildProcessError
      |    +-- ConnectionError
      |    |    +-- BrokenPipeError
      |    |    +-- ConnectionAbortedError
      |    |    +-- ConnectionRefusedError
      |    |    +-- ConnectionResetError
      |    +-- FileExistsError
      |    +-- FileNotFoundError
      |    +-- InterruptedError
      |    +-- IsADirectoryError
      |    +-- NotADirectoryError
      |    +-- PermissionError
      |    +-- ProcessLookupError
      |    +-- TimeoutError
      ...

For more exception types, see the official documentation https://docs.python.org/3/library/exceptions.html , a complete list of built-in exception types is listed at the bottom of the page

Handling exceptions with try/except

The following code threw an exception because the file does not exist

with open('myfile.txt') as f:
    file_data = f.read()
print(file_data)
=============== RESTART: C:/Users/13272/Desktop/try_example.py ===============
Traceback (most recent call last):
  File "C:/Users/13272/Desktop/try_example.py", line 1, in <module>
    with open('myfile.txt') as f:
FileNotFoundError: [Errno 2] No such file or directory: 'myfile.txt'
  • When a runtime error occurs, Python displays a "traceback" message that details what went wrong and where.

However, such backtracking messages are often ugly, puzzling, making the user experience worse, and so on
Thus, a try/except statement for handling exceptions appears. The advantages are: replacing the code that may crash with other code, recording error information before ending the application, displaying more friendly and easy to read exception information, handling exceptions in the background without showing these exceptions to the user, and so on

  • Python's Exception is a controlled program problem triggered by the interpreter
  • When a runtime error occurs, an exception is generated
  • If the generated exception is ignored, it is said that the exception is not caught, and the interpreter terminates the code and displays a runtime error message (traceback)
  • You can catch exceptions with try statements; The exception statement is used to handle this exception

How to use try/except

  • The try / exception code group protects code that may cause exceptions
  • Put the code that may throw an exception in the code group of try
  • When an exception is generated, the code in the try code group terminates and then runs the code in the except code group
  • Define what you want to do next in the exception code group (Note: don't try to ignore exceptions with pass, so that the error message can't appear on the screen: don't mistakenly think that if you ignore an exception, it will disappear. Be sure to write corresponding code to handle exceptions)
  • After exception, you can specify either a specific exception or an exception class (see the hierarchy mentioned at the beginning of the article)
    For example, except OSError can catch FileNotFoundError and PermissionError exceptions at the same time
    except Exception can catch all exceptions
  • To understand the code using try/except, first read the try code group to understand what the code needs to do, and then read the except code group to understand what to do in case of problems
try:
    with open('myfile.txt') as fh:
        file_data = fh.read()
    print(file_data)
    
except FileNotFoundError:#Exception generated when the file does not exist
    print('The data file is missing.')
    
except PermissionError:#Exception generated when the file is not accessible
    print('This is not allowed.')
    
except:#All other unforeseen exceptions
    print('Some other error occurred.')

=============== RESTART: C:/Users/13272/Desktop/try_example.py ===============
The data file is missing.

Now you can get concise output (there is no ugly traceback), but you also lose some important information: now you no longer know the specific problems encountered in the code.
When handling an exception, you need to get the data related to the exception. There are two ways to do this: you can use the functionality of the sys module, or you can use the extended try/ except statement.

Get exception information from sys

  • Through the sys module of the standard library, you can access the internal information of the interpreter (a set of variables and functions available at runtime)
  • Exc of sys module_ The info function provides information about the currently processed exception
    The function returns a tuple: the first value is the exception type, the second value is the detailed description of the exception, and the third value is the backtrace object, which allows access to the backtrace message.
    If there are no exceptions, exc_info return tuple (None,None,None)
>>> import sys
>>> try:
	1/0
except:
	err = sys.exc_info()
	for e in err:
		print(e)

		
<class 'ZeroDivisionError'>
division by zero
<traceback object at 0x00000252A69F8AC8>

Get exception information with exception... As

  • You can extend the except statement with an as keyword
  • This makes it easy to get sys.exc_info function: there is no need to import sys module or process exc_ Tuple returned by info function
  • This will assign the current exception object to a variable (the common variable name is err) to create a more informative error message
  • The value of err here is actually exc_info returns the second value of the tuple (that is, the detailed description of the exception)

Let's take a look at another version of the code, where exception as err is used:

try:
    with open('myfile.txt') as fh:
        file_data = fh.read()
    print(file_data)
    
except FileNotFoundError:
    print('The data file is missing.')
    
except PermissionError:
    print('This is not allowed.')
    
except Exception as err:
    print('Some other error occurred:', str(err))

Exceptions in context manager (and their handling)

We once defined a context manager UseDatabase in DBcm.py (DataBaseContextManager) and used it to connect to the database / clean up the connection (with statement) in vsearch4web.py

Avoid tightly coupled code

  • The context manager provides a convenient abstraction to decouple the working code in the with code group from the back-end database: if MySQL is replaced with PostgreSQL in the future, only the DBcm module needs to be modified without any modification to all the codes using UseDatabase
  • However, handling exceptions in the following way can lead to tight coupling problems:
    To handle the custom exception InterfaceError of MySQL, your code must import the mysql.connector module (which defines this specific exception)
    The new except statement also references mysql.connector.errors.InterfaceError, which tightly binds (couples) the code of vsearch4web.py with the MySQL back-end database
import mysql.connector
...
try:
    with UseDatabase(dbconfig) as cursor:
        ...

except mysql.connector.errors.InterfaceError as err
    print('Is your database switched on? Error:',str(err))
except Exception as err:
    print('Some other error occurred:', str(err))

This is not good: we should maintain the commonality of vsearch4web.py code (which is very meaningful for managing complex projects):

  • It is hoped that vsearch4web.py code can be applied to different database interfaces (not only for MySQL database)
    If you want to change the back-end database from Mysql to PostgreSQL, you don't need to modify the code of vsearch4web.py, just modify the context manager DBcm.py, because all the codes specific to MySQL database are in DBcm.py

Therefore, for the current need to write code tightly coupled with the back-end database (code for handling MySQL custom exceptions), you should consider handling such custom exceptions in the DBcm (context manager) module

Possible exceptions in context manager

Now, we write the exception handling code in DBcm.py

First, import the MySQL module (which contains the custom exception InterfaceError of MySQL)

import mysql.connector

Exception during with statement execution (build phase)

  • Possible exceptions: for example, the database to which you want to connect may not be available, and you may not be able to log in to the (available) database
  • This type of exception occurs (and is handled) in _enter
class UseDatabase:
	def __init__(self, config: dict):
	...
    def __enter__(self) -> 'cursor':
        try:
            self.conn = mysql.connector.connect(**self.configuration)
            self.cursor = self.conn.cursor()
            return self.cursor
        except mysql.connector.errors.InterfaceError as err:
            print('Is your database switched on?Error:',str(err))
        except mysql.connector.errors.ProgrammingError as err:
            print('User/Password issues.Error:',str(err))
            
    def __exit__(self, exc_type, exc_value, exc_traceback):
    ...    

An exception inside the code group (processing phase) of the with statement

  • Possible exceptions: for example, database query may fail, and other situations that may not be encountered may occur
  • This type of exception occurs in the with code group and triggers _exit _ (and is handled in it)
  • When an exception is generated (but not caught) in the with code group, the interpreter terminates the code of the with code group, jumps to the _exit _method, and then executes this method. Three parameters exc_type, exc_value and exc_traceback are passed in to indicate the exception information (when there is no exception, their values are all None)
class UseDatabase:
	def __init__(self, config: dict):
	...
    def __enter__(self) -> 'cursor':
    ...
    def __exit__(self, exc_type, exc_value, exc_traceback):
        self.conn.commit()
        self.cursor.close()
        self.conn.close()
        if exc_type is mysql.connector.errors.ProgrammingError:#Query failed
            print('Is your query correct?Error:',str(exc_value))
        elif exc_type:#Other unknown exceptions
            raise exc_type(exc_value)
  • The exception handling here is special: the try/except statement is no longer used because the exception information in the with code group has passed the three parameters exc_type, exc_value, exc_traceback is passed in to determine whether there is an exception by whether their value is None
  • For__ exit__ The exception handling code added in the method should be placed in__ exit__ After the existing code, make sure to complete its original work before handling the incoming exception

Extended knowledge: custom exceptions

In addition to built-in exceptions, you can also customize exceptions

For example, in the MySQL Connector module, you can customize the exception InterfaceError

Create your custom exception

Just define an empty class that inherits Python's built-in Exception class (meaning that custom exceptions can use all the properties and behaviors of Exception)

>>> class ConnectError(Exception):
	pass

A custom exception was generated

You can generate a custom exception with the raise keyword
The parameters here fill in the error details you want to display

>>> raise ConnectError('Cannot connect...')
Traceback (most recent call last):
  File "<pyshell#11>", line 1, in <module>
    raise ConnectError('Cannot connect...')
ConnectError: Cannot connect...

Capture and handle custom exceptions

After user-defined exceptions are generated, they can be caught and handled with try/except

>>> try:
	raise ConnectError('Whoops!')
except ConnectError as err:
	print('Get:',str(err))

	
Get: Whoops!

When should I use custom exceptions:
Sometimes, the error messages of exceptions generated in different locations are the same:

  • The credentials for accessing the database are wrong, resulting in mysql.connector.errors.InterfaceError exception;
  • When querying the database, the command is wrong, and the mysql.connector.errors.InterfaceError exception is also generated

At this time, you can detect the exception in different locations (the exception causes are different), and raise different custom exceptions to distinguish the two different exceptions
In this way, the generic exception becomes a custom exception with a specific meaning for a specific code

class CredentialsError(Exception):#Custom credential error exception
    """Raised if the database is up, but there's a login issue."""
    pass
class QueryError(Exception):#Custom database query error exception
    """Raised if the query caused problems."""
    pass
    
class UseDatabase:
	def __init__(self, config: dict):
	...
    def __enter__(self) -> 'cursor':
        try:
            self.conn = mysql.connector.connect(**self.configuration)
            self.cursor = self.conn.cursor()
            return self.cursor
        except mysql.connector.errors.ProgrammingError as err:#The credentials for accessing the database are incorrect
            raise CredentialsError(err) from err#A credential error exception was thrown
            #from err is an exception chain: it can track what raises a custom exception
        except ...:
        ...
            
    def __exit__(self, exc_type, exc_value, exc_traceback):
        self.conn.commit()
        self.cursor.close()
        self.conn.close()
        if exc_type is mysql.connector.errors.ProgrammingError:#Instruction error in database query
            raise SQLError(exc_value)#A database query error exception was thrown
        elif exc_type:
            raise exc_type(exc_value)   

Tags: Python Back-end

Posted on Sat, 23 Oct 2021 07:34:40 -0400 by akx