该文章内容整理自《Python编程:从入门到实践》、《流畅的Python》、以及网上各大博客
和 C++、Java 这些编程语言一样,Python 也提供了处理异常的机制,让 Python 解释器在程序运行出现错误时执行事先准备好的除错程序,进而尝试恢复程序的执行。常见异常类型如下
Python 中,用try except语句块捕获并处理异常的基本语法结构如下
try:
# 可能产生异常的代码块
except [ (Error1, Error2, ... ) [as e] ]:
# 处理异常的代码块1
except [ (Error3, Error4, ... ) [as e] ]:
# 处理异常的代码块2
except [Exception]:
# 处理其它异常
else:
# 没有出现异常时进入
finally:
# 最后总会进入
其中
每种异常类型都提供了如下几个属性和方法,通过调用它们,就可以获取当前处理异常类型的相关信息:
try except 语句的执行流程如下。首先执行 try 中的代码块,如果执行过程中出现异常,系统会自动生成一个异常类型,并将该异常提交给 Python 解释器,此过程称为捕获异常。当 Python 解释器收到异常对象时,会寻找能处理该异常对象的 except 块,如果找到合适的 except 块,则把该异常对象交给该 except 块处理,这个过程被称为处理异常。如果 Python 解释器找不到处理异常的 except 块,则程序运行终止,Python 解释器也将退出。事实上,不管程序代码块是否处于 try 块中,甚至包括 except 块中的代码,只要执行该代码块时出现了异常,系统都会自动生成对应类型的异常。但是,如果此段程序没有用 try 包裹,又或者没有为该异常配置处理它的 except 块,则 Python 解释器将无法处理,程序就会停止运行;反之,如果程序发生的异常经 try 捕获并由 except 处理完成,则程序可以继续执行
Python 允许使用 raise 语句在程序中手动抛出异常,基本语法为
raise [exceptionName [(reason)]]
try:
a = input("输入一个数:")
if(not a.isdigit()):
raise ValueError("a 必须是数字")
except ValueError as e:
print("引发异常:",repr(e))
raise
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 提供了大量的异常类,这些异常类之间有严格的继承关系
可见 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)
模块 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,它们的含义分别是:
除了使用 sys.exc_info() 方法获取更多的异常信息之外,还可以使用 traceback 模块,该模块可以用来查看异常的传播轨迹,追踪异常触发的源头。当异常发生时,会异常从发生异常的函数或方法逐渐向外传播,首先传给该函数或方法的调用者,该函数或方法的调用者再传给其调用者,直至最后传到 Python 解释器,此时 Python 解释器会中止该程序,并打印异常的传播轨迹信息
使用 traceback 模块查看异常传播轨迹,首先需要将 traceback 模块引入,该模块提供了如下两个常用方法:
在开发过程中,如果出现了问题是很容易使用 Debug 工具来排查的。但程序开发完成,将它部署到生产环境中去之后,这时只能看到其运行的效果而不能直接看到代码运行过程中每一步的状态的。此时,检查运行情况就会变得非常麻烦。而通过日志记录,不论是正常运行还是出现报错都有相关的时间记录、状态记录、错误记录等,就可以方便地追踪到在当时的运行过程中出现了的状况,从而可以快速排查问题
虽然可以将 print 语句输出重定向到文件输出流保存到文件中,但这样做是非常不规范的。在 Python 中有一个标准的 logging 模块来进行标注的日志记录,同时还可以做更方便的级别区分以及一些额外日志信息的记录,如时间、运行模块信息等。总的来说 logging 模块相比 print 有这么几个优点:
整个日志记录的框架可以分为这么几个部分:
一个简单例子
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()等方法就可以输出各个级别的信息,参数为需要输出的内容
下面详细介绍basicConfig()函数的参数。这些参数也可以在创建Logger对象后调用对象的setLevel()、addHandler()等方法设置
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')
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')
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')