Python3 - 异常处理

1 异常捕捉与处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
def main(val):
try:
print('try...')
r = 10 / int(val)
print('result:', r)

# 若在执行try里的语句时,解释器捕捉到异常,则会根据相应的异常类型,选择一个except语句执行。若没有这种类型异常的except语句,则会执行else里的语句。
except ValueError as e:
print('ValueError:', e)

except ZeroDivisionError as e:
print('ZeroDivisionError:', e)

else:
print('no error!')

# 无论上边有无异常,都会执行finally里的语句。
finally:
print('finally...')

# 执行完上边所有和异常处理相关的语句后,程序继续执行,不会中断。
print('END')

# 抛出异常。然后执行后面的语句。
"""
try...
ZeroDivisionError: division by zero
finally...
END
"""
main(0)

# 无异常。
"""
try...
result: 10.0
no error!
finally...
END
"""
main(1)
  • Python的异常其实也是class,所有的异常类型都继承自BaseException(详见 Built-in Exceptions),所以在使用except时需要注意的是,它不但捕获该类型的异常,还把其子类也“一网打尽”。也就是说,当try语句里产生一个异常时,就从所有的except语句里从前往后寻找。当前边某个except语句对应的异常类型就是产生的这个异常的类型,或者它的一个父类时,就会选择这个except语句执行,或者说这个except语句捕获到了这个产生的异常。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    def foo():
    r = 10 / 0
    print(r)

    try:
    foo()

    except ArithmeticError as e:
    print('ArithmetricError')

    except ZeroDivisionError as e:
    print('ZeroDivisionError')

ArithmeticError是ZeroDivisionError的父类。try里的语句会抛出ZeroDivisionError这种异常,但是它是由第一个except语句捕获到的。

  • else语句可有可无,但最好是有。因为,假设没有else语句,如果前面的except语句都没有捕获到,则程序会结束,不会执行后面的语句(如果有finally语句,在执行其中的语句后结束),解释器打印出默认已定义好的异常。

  • finally语句可有可无。无论前边try语句有无异常,以及如果有异常,前边是否捕捉到,都会执行它里面的所有语句。

2 运用logging.exception()记录异常

在上面用try: ...except:... 进行异常处理的过程中,还可以运用logging.exception()打印捕获到的异常到终端。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import logging

def foo(s):
return 10 / int(s)

def bar(s):
return foo(s) * 2

def main():
try:
bar('0')
except Exception as e:
logging.exception(e)

main()
print('END')

程序运行后,打印到终端的信息为:

1
2
3
4
5
6
7
8
9
10
ERROR:root:division by zero
Traceback (most recent call last):
File "err_logging.py", line 13, in main
bar('0')
File "err_logging.py", line 9, in bar
return foo(s) * 2
File "err_logging.py", line 6, in foo
return 10 / int(s)
ZeroDivisionError: division by zero
END

打印出的第一句,就是记录的信息。

在使用logging.except()时,一定要和try: ... except: ... 连用,即logging.except()必须是作为except: 的处理函数。 另外,在运用logging.except()时,无需通过logging.basicConfig()对logging进行配置。 运用logging模块还可以记录日志到文件中,在后边会说到。

3 用raise语句显示抛出异常

上面的异常处理,都是隐式地自动抛出异常。也可以用raise语句,显示抛出一个异常,并打印出这个异常。 一般情况下,raise后边跟的是一个异常类型对应的实例。如下所示:

1
2
3
# 括号里的内容是实例初始化时赋予给其属性的值,一般为描述异常的信息。
# StopIteration: iteration stop
raise StopIteration("iteration stop")

4 自定义异常

异常是一个类型或者实例,也可以自定义一个类或其实例表示一个异常,但是其类必须继承自Exception类或其子类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 自定义的异常类型继承自ValueError这个类。
class FooError(ValueError):
pass

def foo(s):
n = int(s)
if n==0:
# 用自定义的异常类。
raise FooError('invalid value: %s' % s)
return 10 / n

foo('0')

def func(s):
n = int(s)
if n==0:
# 用内置的异常类。
raise ValueError('invalid value: %s' % s)
return 10 / n

func('0')

5 函数调用时的异常处理

需要用到except语句来捕捉异常,以及空的raise语句来将捕捉到的异常原样抛出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def foo(s):
n = int(s)
if n==0:
raise ValueError('invalid value: %s' % s)
return 10 / n

def bar():
try:
# 调用foo函数,且抛出异常。
foo('0')
except ValueError as e:
print('ValueError!')
# 由于bar函数不知道如何处理这个异常,因此把异常通过空的raise语句原样抛出。
raise

bar()

在bar()函数中的except语句捕获到了异常,但是,打印一个ValueError!后,由于当前函数不知道应该怎么处理该异常,所以把这个异常通过空的raise语句抛出去了。

6 调试

常见的是通过print()方法进行调试,这种方法简单容易,但它的最大坏处就是将来还得删掉它,想想程序里到处都是print(),运行结果也会包含很多垃圾信息。这里介绍其他三种调试方式。

6.1 assert语句

用法为: assert 表达式1 表达式2

  • 表达式1必须有,表达式2可有可无。

  • 表达式1是一个判定语句,表示断言表达式1为True。否则,抛出异常。

  • 若assert语句中定义了表达式2,则当抛出异常时,打印这个提示。

  • 在程序执行时,如果要关闭所有的assert语句,在终端执行命令时,用以下形式: python3 -O xxx.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def foo(s):
n = int(s)
assert n != 0, 'n is zero!'
return 10 / n

def main():
foo('0')

"""
Traceback (most recent call last):
...
AssertionError: n is zero!
"""
main()

6.2 logging

6.2.1 logging的简单使用

assert有一个问题,就是在取消所有assert语句时,要么手动删除源文件的所有assert语句,要么在终端执行时,手动加上一个 -O 参数。 logging的使用,在直观上类似于print()。只不过print()是打印到终端,而logging除了可以打印到终端外,还可以打印到文件里。 要使用logging记录日志信息,还需要通过logging.basicConfig()进行一些必要的配置。

1
2
3
4
5
6
7
8
9
10
11
12
import logging

# logging.INFO是日志记录的等级。
logging.basicConfig(level = logging.INFO)

s = '0'
n = int(s)
# 在这个地方通过logging.info()函数记录日志。
logging.info('n = %d' % n)
print(10 / n)

print("END")
1
2
3
4
5
6
INFO:root:n = 0
Traceback (most recent call last):
File "err.py", line 8, in <module>
print 10 / n
ZeroDivisionError: division by zero
# 注意到,由于没有异常处理,产生异常的语句之后的所有语句都不会执行。

6.2.2 日志记录的等级

logging记录日志共有5个等级,由低到高分别为:

  • DEBUG

  • INFO

  • WARNING

  • ERROR

  • CRITICAL

当我们指定level为logging.INFO时,logging.debug()就不起作用了。同理,指定level=logging.WARNING后,logging.debug()和logging.info()就不起作用了。这样一来,就可以放心地输出不同级别的信息,也不用删除,最后统一控制输出哪个级别的信息。

1
2
3
4
5
6
7
8
9
import logging

logging.basicConfig(level = WARNING)

logging.debug("debug...")
logging.info("info...")
logging.warning("warning...")
logging.error("error...")
logging.critical("critical...")
1
2
3
4
# logging.debug()与logging.info()中的信息没有记录下来。
WARNING:root:warning...
ERROR:root:error...
CRITICAL:root:critical...

6.2.3 日志记录的属性(条目)

属性名 格式 说明
asctime %(asctime)s 返回创建这条日志记录的日期及时间,比如:2015-08-07 16:49:45,896(逗号后面的是微秒)
filename %(filename)s 返回文件名(路径的最后那部分)
funcName %(funcName)s 返回调用这个logging的函数的名字
levelname %(levelname)s 返回所定义的日志记录的level
module %(module)s 返回模块名,也就是文件名的一部分
message %(message)s 返回日志记录的具体信息

6.2.4 logging.basicConfig()的参数

上面在运用logging.basicConfig()进行配置时,仅仅指定了level参数,事实上,这个函数的所有参数为:

  • filename. 指定一个文件路径,同时创建一个FileHandler,并将日志写入到指定文件中。

  • filemode. 日志文件的打开模式,默认为a(追加)。

  • format. 用于格式化输出的日志对应的字符串。

  • datefmt. 用于格式化输出的日志对应的字符串中日期和时间的字符串。

  • level. 指定日志记录的等级。

  • stream. 指定一个流(sys.stderr以及sys.stdout),同时创建一个StreamHandler,并将日志输出到这个流中。 > 注意,stream和filename不能同时指定,否则会发生异常。

  • handlers. 指定一个自定义的handler。 > 注意,handlers不能和stream或filename同时指定,否则会发生异常。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import logging, os

    logging.basicConfig(filename=os.path.join(os.getcwd(), 'log.txt'), level=logging.INFO, filemode='w', format='%(asctime)s - %(levelname)s: %(message)s - filename: %(filename)s', datefmt = '%Y-%m-%d, %H:%M:%S')

    logging.debug("debug...")
    logging.info("info...")
    logging.warning("warning...")
    logging.error("error...")
    logging.critical("critical...")

    输出到日志文件中的信息为:

    1
    2
    3
    4
    2015-08-07, 21:39:36 - INFO: info... - filename: test.py
    2015-08-07, 21:39:36 - WARNING: warning... - filename: test.py
    2015-08-07, 21:39:36 - ERROR: error... - filename: test.py
    2015-08-07, 21:39:36 - CRITICAL: critical... - filename: test.py

6.2.5 logging模块中的基类-Logger

logging模块中,除了提供上面的函数外,还提供了四个基类,分别为Logger, Handler, Filter, Formatter. 事实上,之所以要提供这四个基类,是因为这样适用于更复杂更多样的情况。它们可以形成一个工作流,可以初始化一个Logger实例,然后通过Logger的实例方法将Handler和Filter实例添加进来,而Hanlder实例又可以通过它的实例方法将Formatter实例添加进来,也还可以将Filter实例添加进来。

  • 创建一个Logger实例. logger = logging.getLogger(_name_)。其中参数可以为带有父子层次结构的字符串,但一般为_name_

  • 实例属性propagate. 默认设置为True,表示这个Logger实例对应的日志将向它的所有的祖先Logger对象的Handler传播。

  • 实例方法setLevel(). 设置这个Logger对应日志的等级。

  • 实例方法debug(). 设置DEBUG等级对应的具体的日志信息。除此之外,还有info(), warning(), error(), critical().

  • 实例方法log(). 可以在它所设定的具体的日志信息的同时,设定日志等级。它的好处在于综合了各个等级对应的日志的方法,只需要设定第一个参数对应的等级,就可以具体地转换为相应等级的日志的方法。

  • 实例方法exception(). 只能作为try: ... except: ... 中except的一个处理函数。

  • 实例方法addHandler(). 将一个Handler实例绑定到一个Logger实例中。

  • 实例方法removeHanler(). 从一个Logger实例中去掉指定的Hanlder实例。

  • 实例方法addFilter(). 将一个Filter实例绑定到一个Logger实例中。

  • 实例方法removeFilter(). 从一个Logger实例中去掉指定的Handler实例。

6.2.6 logging模块中的基类-Handler

建立一个Handler:

  • 建立一个FileHandler. fh = logging.FileHandler(filename, mode='a', encoding=None, delay=False)。

  • 建立一个StreamHandler. sh = logging.StreamHandler(),参数可以为sys.stderr, sys.stdout,默认为sys.stderr。

Handler常用的实例方法有:

  • setLevel(). 设置日志级别,低于该级别的被忽略。

  • setFormatter(). 为Handler实例绑定一个Formatter实例。

  • setFilter(). 给Handler实例绑定一个Filter实例。

  • removeFilter(). 移除Handler实例中指定的Filter实例。

6.2.7 logging模块中的基类-Formatter

建立一个Formatter: fmt = logging.Formatter(fmt=None, datefmt=None, style='%'),其中fmt参数即为basicConfig()中的format参数。datefmt参数和style也是basicConfig中的相应参数。

6.2.8 编写一个同时向终端和文件中输出日志的日志系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import logging

# 创建一个Logger实例。
logger = logging.getLogger(__name__)

# 设置日志记录级别。
logger.setLevel(logging.ERROR)

# 创建一个FileHandler实例,用于将日志存入文件。
fh = logging.FileHandler('log.txt')
# 设置日志记录级别。
fh.setLevel(logging.INFO)

# 创建一个StreamHandler实例,用于将日志输出到终端。
sh = logging.StreamHandler()
# 设置日志记录级别。
sh.setLevel(logging.WARNING)

# 创建一个Formatter实例。
fmt = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s', datefmt = '%Y-%m-%d, %H:%M:%S')

# 将fmt绑定到fh这个FileHandler实例上以及sh这个StreamHandler实例上。
fh.setFormatter(fmt)
sh.setFormatter(fmt)

# 将fh这个FileHandler实例和sh这个StreamHanlder实例绑定到logger这个Logger实例上。
logger.addHandler(fh)
logger.addHandler(sh)

# 调用Logger关于记录日志的实例方法。
logger.debug("debug...")
logger.info("info...")
logger.warning("warning...")
logger.error("error...")
logger.critical("critical...")

注意:从这个例子中可以明显看到,在Logger实例和在Handler实例中均上用setLevel方法设置了日志等级,在输出日志到终端以及文件的时候,首先日志信息先通过Logger实例的中Level验证,低于该Level的被忽略,然后在分别进入不同的Handler对象中设置的Level进行二次验证。

6.3 pdb

类似于linux或者mac下的c++调试器gdb,用得不多,所以没看。


Reference