【Python】第八章 异常

该文章内容整理自《Python编程:从入门到实践》、《流畅的Python》、以及网上各大博客

文章目录

  • 异常
    • try
    • raise
    • assert
    • 自定义异常
    • 获取异常信息
      • exc_info()
      • traceback 模块
    • logging
      • Logging配置
      • 捕获Traceback
      • 配置共享
      • 文件配置

异常

和 C++、Java 这些编程语言一样,Python 也提供了处理异常的机制,让 Python 解释器在程序运行出现错误时执行事先准备好的除错程序,进而尝试恢复程序的执行。常见异常类型如下

  1. AssertionError:当 assert 关键字后的条件为假时,程序运行会停止并抛出 AssertionError 异常。如assert(False)
  2. AttributeError:试图访问的对象属性不存在
  3. FileNotFoundError:输入输出异常,无法打开文件
  4. ImportError:无法引入模块或包,路径文件错误
  5. IndentationError:语法错误,代码没有对齐
  6. IndexError:索引超出序列范围
  7. KeyboardInterrupt:按下Ctrl+C中断程序
  8. KeyError:字典中查找一个不存在的关键字
  9. NameError:尝试访问一个未声明的变量
  10. SyntaxError:代码非法,无法编译
  11. TypeError:不同类型数据之间的无效操作
  12. ZeroDivisionError:除法运算中除数为 0

try

Python 中,用try except语句块捕获并处理异常的基本语法结构如下

try:
    # 可能产生异常的代码块
except [ (Error1, Error2, ... ) [as e] ]:
    # 处理异常的代码块1
except [ (Error3, Error4, ... ) [as e] ]:
    # 处理异常的代码块2
except  [Exception]:
    # 处理其它异常
else: 
    # 没有出现异常时进入
finally:
    # 最后总会进入

其中

  • try 块有且仅有一个,但 except 代码块可以有多个,并且每个 except 块都可以同时处理多种异常,如(Error1, Error2,…) 、(Error3, Error4,…),但不推荐这种写法。
  • [as e]作为可选参数,表示给这些异常类型起一个别名 e,这样做的好处是方便在 except 块中调用异常类型。
  • [Exception]作为可选参数,可以代指程序可能发生的所有异常情况,其通常用在最后一个 except 块。一般来说,可处理全部异常的 except 块 Exception 要放到所有 except 块的后面,父类异常的 except 块要放到子类异常的 except 块的后面。
  • 在try except后也可以加上else代码块,当try块中出现错误时会被except块捕获,而当没有出现错误时则会进入else块。else块是在出现异常时才起作用,当没有出现异常时会进入到else块,然后try except块外的代码;而当出现异常时,则不执行else块代码,在处理完异常后直接执行try except外的代码
  • 最后还可以加上finally块。finally块只与try匹配,而与except或else块无关。无论 try 块是否发生异常,最终都要进入 finally 语句,并执行其中的代码块。如当 try 块打开了一些物理资源时,由于这些资源必须手动回收,则回收工作通常就放在 finally 块中。因为一旦 try 块中的某行代码发生异常,则其后续的代码将不会得到执行;其次 except 和 else 也不适合,它们都可能不会得到执行。而 finally 块中的代码,无论 try 块是否发生异常,该块中的代码都会被执行

每种异常类型都提供了如下几个属性和方法,通过调用它们,就可以获取当前处理异常类型的相关信息:

  • args:返回异常的错误编号和描述字符串
  • str(e):返回异常信息,但不包括异常信息的类型
  • repr(e):返回较全的异常信息,包括异常信息的类型

try except 语句的执行流程如下。首先执行 try 中的代码块,如果执行过程中出现异常,系统会自动生成一个异常类型,并将该异常提交给 Python 解释器,此过程称为捕获异常。当 Python 解释器收到异常对象时,会寻找能处理该异常对象的 except 块,如果找到合适的 except 块,则把该异常对象交给该 except 块处理,这个过程被称为处理异常。如果 Python 解释器找不到处理异常的 except 块,则程序运行终止,Python 解释器也将退出。事实上,不管程序代码块是否处于 try 块中,甚至包括 except 块中的代码,只要执行该代码块时出现了异常,系统都会自动生成对应类型的异常。但是,如果此段程序没有用 try 包裹,又或者没有为该异常配置处理它的 except 块,则 Python 解释器将无法处理,程序就会停止运行;反之,如果程序发生的异常经 try 捕获并由 except 处理完成,则程序可以继续执行

raise

Python 允许使用 raise 语句在程序中手动抛出异常,基本语法为
raise [exceptionName [(reason)]]

try:
    a = input("输入一个数:")
    if(not a.isdigit()):
        raise ValueError("a 必须是数字")
except ValueError as e:
    print("引发异常:",repr(e))
    raise
  1. raise:单独一个 raise。该语句引发当前上下文中捕获的异常(比如在 except 块中),如上面程序中已经手动引发了 ValueError 异常,因此这里当再使用 raise 语句时,它会再次引发一次;或默认引发 RuntimeError 异常。
  2. raise exceptionName:raise 后带一个异常类名称,表示引发执行类型的异常
  3. raise exceptionName(reason):在引发指定类型的异常的同时,附带异常的描述信息

assert

Python 提供了 assert 语句用来调试程序。assert 语句的完整语法格式为
assert 条件表达式 [,描述信息]
当条件表达式的值为真时,该语句什么也不做,程序正常运行;反之,若条件表达式的值为假,则 assert 会抛出 AssertionError 异常。其中,[,描述信息] 作为可选参数,用于对条件表达式可能产生的异常进行描述

try:
    s_age = input("请输入您的年龄:")
    age = int(s_age)
    assert 20 < age < 80 , "年龄不在 20-80 之间"
    print("您输入的年龄在20和80之间")
except AssertionError as e:
    print("输入年龄不正确", e)

另外,当在命令行模式运行 Python 程序时传入 -O(大写)参数,可以禁用程序中包含的 assert 语句

自定义异常

Python 提供了大量的异常类,这些异常类之间有严格的继承关系
【Python】第八章 异常_第1张图片

可见 BaseException 是 Python 中所有异常类的基类,但一般来说程序中可能出现的各种异常都继承自 Exception,因而 Exception 是万能错误拦截,可以拦下所有错误。同时,自定义异常也应该继承 Exception 类而非 BaseException 类

下面是自定义异常类的简单例子

class MyException(Exception):
	def __init__(self, msg):
		self.message = msg
	def __str__(self):
		return self.message

try:
	raise MyException("New Exception")
except MyException as e:
	print(e)

获取异常信息

exc_info()

模块 sys 中,有两个方法可以返回异常的全部信息,分别是 exc_info() 和 last_traceback(),这两个函数有相同的功能和用法。这里只介绍exc_info()函数

import sys
import traceback
try:
    # ...
except:
    print(sys.exc_info())
    traceback.print_tb(sys.exc_info()[2])

exc_info() 方法会将当前的异常信息以元组的形式返回,该元组中包含 3 个元素,分别为 type、value 和 traceback,它们的含义分别是:

  • type:异常类型的名称,它是 BaseException 的子类。如
  • value:捕获到的异常实例。如ZeroDivisionError(‘division by zero’,)
  • traceback:是一个 traceback 对象。要查看 traceback 对象包含的内容,需要先引进 traceback 模块,然后调用 traceback 模块中的 print_tb 方法,并将 sys.exc_info() 输出的 traceback 对象作为参数参入。输出信息中包含了更多的异常信息,包括文件名、抛出异常的代码所在的行数、抛出异常的具体代码

traceback 模块

除了使用 sys.exc_info() 方法获取更多的异常信息之外,还可以使用 traceback 模块,该模块可以用来查看异常的传播轨迹,追踪异常触发的源头。当异常发生时,会异常从发生异常的函数或方法逐渐向外传播,首先传给该函数或方法的调用者,该函数或方法的调用者再传给其调用者,直至最后传到 Python 解释器,此时 Python 解释器会中止该程序,并打印异常的传播轨迹信息

使用 traceback 模块查看异常传播轨迹,首先需要将 traceback 模块引入,该模块提供了如下两个常用方法:

  • print_exc([limit[, file]]):将异常传播轨迹信息输出到控制台或指定文件中。其中limit用于限制显示异常传播的层数。比如函数 A 调用函数 B,函数 B 发生了异常,如果指定 limit=1,则只显示函数 A 里面发生的异常。如果不设置 limit 参数,则默认全部显示。file指定将异常传播轨迹信息输出到指定文件中。如果不指定该参数,则默认输出到控制台
  • format_exc([limit[, chain]]):将异常传播轨迹信息转换成字符串。limit限制显示异常传播的层数。chain默认为True,也就是一并显示__cause__、__context__等串连起来的异常

logging

在开发过程中,如果出现了问题是很容易使用 Debug 工具来排查的。但程序开发完成,将它部署到生产环境中去之后,这时只能看到其运行的效果而不能直接看到代码运行过程中每一步的状态的。此时,检查运行情况就会变得非常麻烦。而通过日志记录,不论是正常运行还是出现报错都有相关的时间记录、状态记录、错误记录等,就可以方便地追踪到在当时的运行过程中出现了的状况,从而可以快速排查问题

虽然可以将 print 语句输出重定向到文件输出流保存到文件中,但这样做是非常不规范的。在 Python 中有一个标准的 logging 模块来进行标注的日志记录,同时还可以做更方便的级别区分以及一些额外日志信息的记录,如时间、运行模块信息等。总的来说 logging 模块相比 print 有这么几个优点:

  • 可以在 logging 模块中设置日志等级,在不同的版本(如开发环境、生产环境)上通过设置不同的输出等级来记录对应的日志,非常灵活
  • print 的输出信息都会输出到标准输出流中,而 logging 模块就更加灵活,可以设置输出到任意位置,如写入文件、写入远程服务器等
  • logging 模块具有灵活的配置和格式化功能,如配置输出当前模块信息、运行时间等,相比 print 的字符串格式化更加方便易用

整个日志记录的框架可以分为这么几个部分:

  • Logger:Logger Main Class,是日志记录时创建的对象。可以调用它的方法传入日志模板和信息,来生成一条条日志记录,称作 Log Record
  • Log Record:生成的一条条日志记录
  • Handler:用来处理日志记录的类,可以将 Log Record 输出到指定的日志位置和存储形式等。如可以指定将日志通过 FTP 协议记录到远程的服务器上
  • Formatter:实际上生成的 Log Record 也是一个个对象,如果想要将其保存成一条条日志文本,则需要有一个格式化的过程,而这个过程就由 Formatter 来完成,返回日志字符串,然后传回给 Handler 来处理
  • Filter:保存日志时可能不需要全部保存,如只保存某个级别的日志,或只保存包含某个关键字的日志等,所以保存前还需要进行过滤,而这个过滤过程就交给 Filter 来完成
  • Parent Handler:Handler 之间可以存在分层关系,以使得不同 Handler 之间共享相同功能的代码

一个简单例子

import logging
 
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
 
logger.info('This is a log info')
logger.debug('Debugging')
logger.warning('Warning exists')
logger.info('Finish')

其中,basicConfig()函数用来进行日志的全局配置。getLogger()方法用来声明一个Logger对象,初始化时需要传入了模块的名称,这里直接使用 __name__ ,即模块的名称来代替,若不传入则为 __main__,若为 import 的模块的话就是被引入模块的名称,这个变量在不同的模块中的名字是不同的,所以一般使用 __name__ 来表示。调用对象里的info()、debug()、warning()等方法就可以输出各个级别的信息,参数为需要输出的内容

Logging配置

下面详细介绍basicConfig()函数的参数。这些参数也可以在创建Logger对象后调用对象的setLevel()、addHandler()等方法设置

  • filename:日志输出的文件名,如果指定了这个信息之后,实际上会启用 FileHandler,而不再是 StreamHandler,这样日志信息便会输出到文件中了
  • filemode:这个是指定日志文件的写入方式,有两种形式,一种是 w,一种是 a,分别代表清除后写入和追加写入
  • format:指定日志信息的输出格式,即上文示例所示的参数,部分常用参数如下所示:
    • %(levelno)s:打印日志级别的数值
    • %(levelname)s:打印日志级别的名称
    • %(pathname)s:打印当前执行程序的路径,其实就是sys.argv[0]
    • %(filename)s:打印当前执行程序名
    • %(funcName)s:打印日志的当前函数
    • %(lineno)d:打印日志的当前行号
    • %(asctime)s:打印日志的时间
    • %(thread)d:打印线程ID
    • %(threadName)s:打印线程名称
    • %(process)d:打印进程ID
    • %(processName)s:打印线程名称
    • %(module)s:打印模块名称
    • %(message)s:打印日志信息
import logging
 
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
 
# bad
logging.debug('Hello {0}, {1}!'.format('World', 'Congratulations'))
# good
logging.debug('Hello %s, %s!', 'World', 'Congratulations')
  • datefmt:指定时间的输出格式,如 ‘%Y/%m/%d %H:%M:%S’
  • style:如果 format 参数指定了,这个参数就可以指定格式化时的占位符风格,如 %、{、$ 等
  • level:指定日志输出的类别,程序会输出大于等于此级别的信息。个等级及数值如下:CRITICAL:50、FATAL:50、ERROR:40、WARNING:30、WARN:30、INFO:20、DEBUG:10、NOTSET:0
  • stream:在没有指定 filename 的时候会默认使用 StreamHandler,这时 stream 可以指定初始化的文件流
  • handlers:可以指定日志处理时所使用的 Handlers,必须是可迭代的。logging 模块提供了多种 Handler,每个 Handler 还可以各自的配置信息
    • StreamHandler:logging.StreamHandler;日志输出到流,可以是 sys.stderr,sys.stdout 或者文件
    • FileHandler:logging.FileHandler;日志输出到文件
    • BaseRotatingHandler:logging.handlers.BaseRotatingHandler;基本的日志回滚方式
    • RotatingHandler:logging.handlers.RotatingHandler;日志回滚方式,支持日志文件最大数量和日志文件回滚
    • TimeRotatingHandler:logging.handlers.TimeRotatingHandler;日志回滚方式,在一定时间区域内回滚日志文件
    • SocketHandler:logging.handlers.SocketHandler;远程输出日志到TCP/IP sockets
    • DatagramHandler:logging.handlers.DatagramHandler;远程输出日志到UDP sockets
    • SMTPHandler:logging.handlers.SMTPHandler;远程输出日志到邮件地址
    • SysLogHandler:logging.handlers.SysLogHandler;日志输出到syslog
    • NTEventLogHandler:logging.handlers.NTEventLogHandler;远程输出日志到Windows NT/2000/XP的事件日志
    • MemoryHandler:logging.handlers.MemoryHandler;日志输出到内存中的指定buffer
    • HTTPHandler:logging.handlers.HTTPHandler;通过”GET”或者”POST”远程输出到HTTP服务器
import logging
from logging.handlers import HTTPHandler
import sys
 
logger = logging.getLogger(__name__)
logger.setLevel(level=logging.DEBUG)
 
# StreamHandler
stream_handler = logging.StreamHandler(sys.stdout)
stream_handler.setLevel(level=logging.DEBUG)
logger.addHandler(stream_handler)
 
# FileHandler
file_handler = logging.FileHandler('output.log')
file_handler.setLevel(level=logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
 
# HTTPHandler
http_handler = HTTPHandler(host='localhost:8001', url='log', method='POST')
logger.addHandler(http_handler)
 
# Log
logger.info('This is a log info')
logger.debug('Debugging')
logger.warning('Warning exists')
logger.info('Finish')

捕获Traceback

Logging也可以捕获Traceback,在 error() 方法中将 exc_info 设置为 True,这样就可以输出执行过程中的信息了

import logging
 
logger = logging.getLogger(__name__)
logger.setLevel(level=logging.DEBUG)

formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler = logging.FileHandler('result.log')
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)

logger.info('Start')
logger.warning('Something maybe fail.')
try:
    result = 10 / 0
except Exception:
    logger.error('Faild to get result', exc_info=True)
logger.info('Finished')

配置共享

在写项目的时候,如果每个文件都来配置 logging 配置那就太繁琐了,logging 模块提供了父子模块共享配置的机制,会根据 Logger 的名称来自动加载父模块的配置
如在main.py文件将 Logger 的名称定义为 main

import logging
import core
 
logger = logging.getLogger('main')
logger.setLevel(level=logging.DEBUG)
 
# Handler
handler = logging.FileHandler('result.log')
handler.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
 
logger.info('Main Info')
logger.debug('Main Debug')
logger.error('Main Error')
core.run()

则在core.py文件中可将 Logger 的名称定义为 main.core,这样 core.py 里面的 Logger 就会复用 main.py 里面的 Logger 配置,而不用再去配置一次了

import logging
 
logger = logging.getLogger('main.core')
 
def run():
    logger.info('Core Info')
    logger.debug('Core Debug')
    logger.error('Core Error')

如此一来,只要在入口文件里面定义好 logging 模块的输出配置,子模块只需要在定义 Logger 对象时名称使用父模块的名称开头即可共享配置,非常方便

文件配置

在开发过程中,将配置在代码里面写死并不是一个好的习惯,更好的做法是将配置写在配置文件里面,然后运行时读取配置文件里面的配置,这样更方便管理和维护。如定义一个 yaml 配置文件,其中 root 指定了 handlers 是 console,即只输出到控制台。另外在 loggers 一项配置里面,我们定义了 main.core 模块,handlers 是 console、file、error 三项,即输出到控制台、输出到普通文件和回滚文件

version: 1
formatters:
  brief:
    format: "%(asctime)s - %(message)s"
  simple:
    format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
handlers:
  console:
    class : logging.StreamHandler
    formatter: brief
    level   : INFO
    stream  : ext://sys.stdout
  file:
    class : logging.FileHandler
    formatter: simple
    level: DEBUG
    filename: debug.log
  error:
    class: logging.handlers.RotatingFileHandler
    level: ERROR
    formatter: simple
    filename: error.log
    maxBytes: 10485760
    backupCount: 20
    encoding: utf8
loggers:
  main.core:
    level: DEBUG
    handlers: [console, file, error]
root:
  level: DEBUG
  handlers: [console]

再在main.py文件中调用

import logging
import core
import yaml
import logging.config
import os
 
 
def setup_logging(default_path='config.yaml', default_level=logging.INFO):
    path = default_path
    if os.path.exists(path):
        with open(path, 'r', encoding='utf-8') as f:
            config = yaml.load(f)
            logging.config.dictConfig(config)
    else:
        logging.basicConfig(level=default_level)
 
 
def log():
    logging.debug('Start')
    logging.info('Exec')
    logging.info('Finished')
 
 
if __name__ == '__main__':
    yaml_path = 'config.yaml'
    setup_logging(yaml_path)
    log()

core.py文件

import logging
 
logger = logging.getLogger('main.core')
 
def run():
    logger.info('Core Info')
    logger.debug('Core Debug')
    logger.error('Core Error')

你可能感兴趣的:(编程语言,python)