异常处理,调试,测试


python标准异常(exception)总结

AssertionError 断言语句(assert)失败 (不满足条件是抛出异常)
AttributeError 尝试访问未知的对象属性
EOFError 用户输入文件末尾标志EOF(Ctrl+d)
FloatingPointError 浮点计算错误
GeneratorExit generator.close()方法被调用的时候
ImportError 导入模块失败的时候
IndexError 索引超出序列的范围
KeyError 字典中查找一个不存在的关键字
KeyboardInterrupt 用户输入中断键(Ctrl+c)
MemoryError 内存溢出(可通过删除对象释放内存)
NameError 尝试访问一个不存在的变量
NotImplementedError 尚未实现的方法
OSError 操作系统产生的异常(例如打开一个不存在的文件)
OverflowError 数值运算超出最大限制
ReferenceError 弱引用(weak reference)试图访问一个已经被垃圾回收机制回收了的对象
RuntimeError 一般的运行时错误
StopIteration 迭代器没有更多的值
SyntaxError Python的语法错误
IndentationError 缩进错误
TabError Tab和空格混合使用
SystemError Python编译器系统错误
SystemExit Python编译器进程被关闭
TypeError 不同类型间的无效操作
UnboundLocalError 访问一个未初始化的本地变量(NameError的子类)
UnicodeError Unicode相关的错误(ValueError的子类)
UnicodeEncodeError Unicode编码时的错误(UnicodeError的子类)
UnicodeDecodeError Unicode解码时的错误(UnicodeError的子类)
UnicodeTranslateError Unicode转换时的错误(UnicodeError的子类)
ValueError 传入无效的参数
ZeroDivisionError 除数为零

以下是 Python 内置异常类的层次结构:

  • 所有错误继承自BaseException类, Built-in Exceptions — Python 3.10.4 documentation

    BaseException
     +-- SystemExit
     +-- KeyboardInterrupt
     +-- GeneratorExit
     +-- Exception
          +-- StopIteration
          +-- StopAsyncIteration
          +-- ArithmeticError
          |    +-- FloatingPointError
          |    +-- OverflowError
          |    +-- ZeroDivisionError
          +-- AssertionError
          +-- AttributeError
          +-- BufferError
          +-- EOFError
          +-- ImportError
          |    +-- ModuleNotFoundError
          +-- LookupError
          |    +-- IndexError
          |    +-- KeyError
          +-- MemoryError
          +-- NameError
          |    +-- UnboundLocalError
          +-- OSError
          |    +-- BlockingIOError
          |    +-- ChildProcessError
          |    +-- ConnectionError
          |    |    +-- BrokenPipeError
          |    |    +-- ConnectionAbortedError
          |    |    +-- ConnectionRefusedError
          |    |    +-- ConnectionResetError
          |    +-- FileExistsError
          |    +-- FileNotFoundError
          |    +-- InterruptedError
          |    +-- IsADirectoryError
          |    +-- NotADirectoryError
          |    +-- PermissionError
          |    +-- ProcessLookupError
          |    +-- TimeoutError
          +-- ReferenceError
          +-- RuntimeError
          |    +-- NotImplementedError
          |    +-- RecursionError
          +-- SyntaxError
          |    +-- IndentationError
          |         +-- TabError
          +-- SystemError
          +-- TypeError
          +-- ValueError
          |    +-- UnicodeError
          |         +-- UnicodeDecodeError
          |         +-- UnicodeEncodeError
          |         +-- UnicodeTranslateError
          +-- Warning
               +-- DeprecationWarning
               +-- PendingDeprecationWarning
               +-- RuntimeWarning
               +-- SyntaxWarning
               +-- UserWarning
               +-- FutureWarning
               +-- ImportWarning
               +-- UnicodeWarning
               +-- BytesWarning
               +-- EncodingWarning
               +-- ResourceWarning
    

异常处理语法

  1. try - except - finally语句
try:	
	A语句
	B语句
	C语句
	…
	检测范围

except Exception1 as e1:        # 可以多个except捕获异常
	出现异常后的处理代码 + str(e1)
except Exception2 as e2:
	出现异常后的处理代码 + str(e2)
    ...
else:     	 # 没有错误发生时执行                                                
    print('No error')

finally:
	无论如何都会被执行的代码

注意:

1) 异常的检测是从上到下依次检测,遇到异常马上跳转到except的块,如果未找到相应的except代码则会正常显示相应的红字traceback

2) as e 可以打印出具体的内容

3)finally 里面的语句不管前面什么结果都会运行,收尾命令需要放在这里(f.close()之类的)

4)使用try...except捕获错误还有一个巨大的好处,就是可以跨越多层调用,比如函数main()调用bar()bar()调用foo(),结果foo()出错了,这时,只要main()捕获到了,就可以处理:

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

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

def main():
    try:
        bar('0')
    except Exception as e:
        print('Error:', e)
    finally:
        print('finally...')
	eg: 
		try:
		    f = open("Text file.txt", 'w')
		    f.write("testtest!")
		    sum = 1 + '1'
		except OSError as reason:
		    print("Errors on this file!\nPlease check the reason: " + str(reason))
		except TypeError as err:
		    print("Type Errors found on this file!\nPlease check the reason: " + str(err))
		finally:
		    f.close()
		 
		another format: (同时抓取多个exception)
		try:    
		    f = open("Text file.txt", 'w')
		    f.write("testtest!")
		    sum = 1 + '1'
		except (OSError, TypeError):
		    print("Errors on this file!)
		finally:
		    f.close()
		
		
		'''' 输入整数正常返回,否则出错提示重新输入'''
		def int_input(prompt = ''):
		        while True:
		            try:
		                accept = int(input('Please input a int number: '))
		                break
		            except ValueError as reason:
		                print("Wrong, you're not inputing a int number!")
		            
		int_input('Please input a int number: ')  

记录错误

正常情况下如果不捕获错误,Python解释器来打印出错误堆栈,但程序也被结束了。

Python内置的logging模块可以非常容易地记录错误信息,然后分析错误原因,同时,让程序继续执行下去。:

# err_logging.py

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')

同样是出错,但程序打印完错误信息后会继续执行,并正常退出:

$ python3 err_logging.py
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还可以把错误记录到日志文件里,方便事后排查。

抛出错误 Raise

格式:raise Exception('...explanation...'),主动引出异常

  • 尽量选择Python已有的内置的错误类型抛出,只有在必要的时候再自己定义。
try:
    for i in range(3):
        for j in range(3):
            if i == 2:
                raise KeyboardInterrupt('故意设置')
            print(i, j)
except KeyboardInterrupt as reason:
    print('exit!' + str(reason))

  • 另一种常见错误处理方式

    # err_reraise.py
    
    def foo(s):
        n = int(s)
        if n==0:
            raise ValueError('invalid value: %s' % s)
        return 10 / n
    
    def bar():
        try:
            foo('0')
        except ValueError as e:
            print('ValueError!')
            raise
    
    bar()
    

bar()函数中,我们明明已经捕获了错误打印一个ValueError!后,又把错误通过raise语句抛出去了?

==> 相当常见。捕获错误目的只是记录一下,便于后续追踪。但是,由于当前函数不知道应该怎么处理该错误,所以,最恰当的方式是继续往上抛,让顶层调用者去处理。

  • raise语句如果不带参数,就会把当前错误原样抛出。此外,在exceptraise一个Error,还可以把一种类型的错误转化成另一种类型:
try:
    10 / 0
except ZeroDivisionError:
    raise ValueError('input error!')

只要是合理的转换逻辑就可以,但是,决不应该把一个IOError转换成毫不相干的ValueError

丰富的else语句

  • 搭配if, 要么怎样,要么不怎样

  • 搭配while/for, 干完能怎样,干不完就别想怎样
    while循环执行完后才会执行else

	def max_divisor(num):
	    count = num // 2
	    while count > 1:
	        if num % count == 0:
	            print('The max common divisor is %d' % count)
	            break
	        count -= 1
	    else:
	        print('%d is prime number!' % num)

  • 搭配异常检测,没有问题那就干吧
	try:
	    print(int('111'))
	except ValueError as reason:
	    print('Something wrong!' + str(reason))
	else:
	    print('All good!')

调试

print()

简单粗暴的找出有问题的变量, 最大的坏处是将来还得删掉它,运行结果很多垃圾信息。

断言assert

凡是用print()来辅助查看的地方,都可以用断言(assert)来替代:

def foo(s):
    n = int(s)
    assert n != 0, 'n is zero!'
    return 10 / n

def main():
    foo('0')

assert的意思是,表达式n != 0应该是True,否则,根据程序运行的逻辑,后面的代码肯定会出错。

如果断言失败,assert语句本身就会抛出AssertionError

$ python err.py
Traceback (most recent call last):
  ...
AssertionError: n is zero!

程序中如果到处充斥着assert,和print()相比也好不到哪去。不过,启动Python解释器时可以用-O参数来关闭assert

$ python -O err.py
Traceback (most recent call last):
  ...
ZeroDivisionError: division by zero

注意:断言的开关“-O”是英文大写字母O,不是数字0。

关闭后,你可以把所有的assert语句当成pass来看。

logging

print()替换为logging是第3种方式,和assert比,logging不会抛出错误,而且可以输出到文件:

import logging

s = '0'
n = int(s)
logging.info('n = %d' % n)
print(10 / n)

logging.info()就可以输出一段文本。运行,发现除了ZeroDivisionError,没有任何信息。怎么回事?

别急,在import logging之后添加一行配置再试试:

import logging
logging.basicConfig(level=logging.INFO)

看到输出了:

$ python err.py
INFO:root:n = 0
Traceback (most recent call last):
  File "err.py", line 8, in <module>
    print(10 / n)
ZeroDivisionError: division by zero

这就是logging的好处,它允许你指定记录信息的级别,有debuginfowarningerror等几个级别,当我们指定level=INFO时,logging.debug就不起作用了。同理,指定level=WARNING后,debuginfo就不起作用了,打印级别: DEBUG < INFO < WARNING < ERROR < CRITICAL。这样一来,你可以放心地输出不同级别的信息,也不用删除,最后统一控制输出哪个级别的信息。

logging的另一个好处是通过简单的配置,一条语句可以同时输出到不同的地方,比如console和文件。

import logging
logging.basicConfig(
    level=logging.DEBUG,
    filename="test.log",
    datefmt="%Y-%m-%d %H:%M:%S",
    format="【%(asctime)s %(levelname)s】 %(lineno)d: %(message)s"
)
logging.debug("debug")
logging.info("info")
logging.warning("warning")
logging.error("error")

>>> test.log
[2022-04-26 22:15:49 DEBUG] 10: debug level
[2022-04-26 22:15:49 INFO] 11: info level
[2022-04-26 22:15:49 WARNING] 12: warning level
[2022-04-26 22:15:49 ERROR] 13: error level
PDB

启动Python的调试器pdb,让程序以单步方式运行,可以随时查看运行状态。

# err.py
s = '0'
n = int(s)
print(10 / n)

然后启动:

$ python -m pdb err.py
> /Users/michael/Github/learn-python3/samples/debug/err.py(2)<module>()
-> s = '0'

以参数-m pdb启动后,pdb定位到下一步要执行的代码-> s = '0'。输入命令l来查看代码:

(Pdb) l
  1     # err.py
  2  -> s = '0'
  3     n = int(s)
  4     print(10 / n)

输入命令n可以单步执行代码:

(Pdb) n
> /Users/michael/Github/learn-python3/samples/debug/err.py(3)<module>()
-> n = int(s)
(Pdb) n
> /Users/michael/Github/learn-python3/samples/debug/err.py(4)<module>()
-> print(10 / n)

任何时候都可以输入命令p 变量名来查看变量:

(Pdb) p s
'0'
(Pdb) p n
0

输入命令q结束调试,退出程序:

(Pdb) q

这种通过pdb在命令行调试的方法理论上是万能的,但实在是太麻烦了,如果有一千行代码,要运行到第999行得敲多少命令啊。还好,我们还有另一种调试方法。

pdb.set_trace()

这个方法也是用pdb,但是不需要单步执行,我们只需要import pdb,然后,在可能出错的地方放一个pdb.set_trace(),就可以设置一个断点:

# err.py
import pdb

s = '0'
n = int(s)
pdb.set_trace() # 运行到这里会自动暂停
print(10 / n)

运行代码,程序会自动在pdb.set_trace()暂停并进入pdb调试环境,可以用命令p查看变量,或者用命令c继续运行:

$ python err.py 
> /Users/michael/Github/learn-python3/samples/debug/err.py(7)<module>()
-> print(10 / n)
(Pdb) p n
0
(Pdb) c
Traceback (most recent call last):
  File "err.py", line 7, in <module>
    print(10 / n)
ZeroDivisionError: division by zero

这个方式比直接启动pdb单步调试效率要高很多,但也高不到哪去。

IDE

如果要比较爽地设置断点、单步执行,就需要一个支持调试功能的IDE。目前比较好的Python IDE有:

Visual Studio Code:https://code.visualstudio.com/,需要安装Python插件。

PyCharm:http://www.jetbrains.com/pycharm/

另外,Eclipse加上pydev插件也可以调试Python程序。

但是最后你会发现,logging才是终极武器。

单元测试

单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。

  • 测试通过–> 可以正常工作(可能有隐藏bug)

  • 测试不通过–> 肯定有bug or 测试条件设置有问题

  • 意义:修改后的代码如果能再次通过单元测试,可以保证该代码行为仍然正确

编写一个My_Dict类,这个类的行为和dict一致,但是可以通过属性来访问,用起来就像下面这样:

>>> d = My_Dict(a=1, b=2)
>>> d['a']
1
>>> d.a
1
# -*- coding: UTF-8 -*-
class My_Dict(dict):
  def __init__(self, **kw):
    super().__init__(**kw)

  def __getattr__(self, key):
    try:
      return self[key]
    except KeyError:
      raise AttributeError('The %s not exist in this dict!' % key)
  
  def __setattr__(self, key, value):
    self[key] = value

单元测试代码

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from t3 import My_Dict
import unittest

class Test_dict(unittest.TestCase):

    def test_init(self):
        d = My_Dict(a=1, b='test')
        self.assertEqual(d.a, 1)
        self.assertEqual(d.b, 'test')
        self.assertTrue(isinstance(d, dict))

    def test_key(self):
        d = My_Dict()
        d['key'] = 'value'
        self.assertEqual(d.key, 'value')

    def test_attr(self):
        d = My_Dict()
        d.key = 'value'
        self.assertTrue('key' in d)
        self.assertEqual(d['key'], 'value')

    def test_keyerror(self):
        d = My_Dict()
        with self.assertRaises(KeyError):
            value = d['empty']

    def test_attrerror(self):
        d = My_Dict()
        with self.assertRaises(AttributeError):
            value = d.empty

    def setUp(self):
        print('starting setup...')


    def tearDown(self):
        print('end teardown...')

编写单元测试时,我们需要编写一个测试类,从unittest.TestCase继承。

test开头的方法就是测试方法,不以test开头的方法不被认为是测试方法,测试的时候不会被执行。

对每一类测试都需要编写一个test_xxx()方法。由于unittest.TestCase提供了很多内置的条件判断,我们只需要调用这些方法就可以断言输出是否是我们所期望的。最常用的断言就是assertEqual()

self.assertEqual(abs(-1), 1) # 断言函数返回的结果与1相等

另一种重要的断言就是期待抛出指定类型的Error,比如通过d['empty']访问不存在的key时,断言会抛出KeyError

with self.assertRaises(KeyError):
    value = d['empty']

而通过d.empty访问不存在的key时,我们期待抛出AttributeError

with self.assertRaises(AttributeError):
    value = d.empty

可以在单元测试中编写两个特殊的setUp()tearDown()方法。这两个方法会分别在每调用一个测试方法的前后分别被执行。

setUp()tearDown()方法有什么用呢?设想你的测试需要启动一个数据库,这时,就可以在setUp()方法中连接数据库,在tearDown()方法中关闭数据库,这样,不必在每个测试方法中重复相同的代码:

  • 测试输出
(py3.9) C:\Users\meij1\Videos\OdoCSV>python -m unittest t2.py
starting setup...
end teardown...
.starting setup...
end teardown...
.starting setup...
end teardown...
.starting setup...
end teardown...
.starting setup...
end teardown...
.
----------------------------------------------------------------------
Ran 5 tests in 0.003s

OK

运行方法1:最简单的,在mydict_test.py的最后加上两行代码变成普通python文件:

if __name__ == '__main__':
    unittest.main()
    
$ python mydict_test.py

方法2:推荐,命令行通过参数-m unittest直接运行单元测试,这样可以一次批量运行很多单元测试,并且,有很多工具可以自动来运行这些单元测试。

文档测试

写在注释中的测试代码,可以通过Python内置的“文档测试”(doctest)模块可以直接提取注释中的代码并执行测试。

doctest严格按照Python交互式命令行的输入和输出来判断测试结果是否正确。只有测试异常的时候,可以用...表示中间一大段烦人的输出。

让我们用doctest来测试上次编写的Dict类:

# mydict2.py
class Dict(dict):
    '''
    Simple dict but also support access as x.y style.

    >>> d1 = Dict()
    >>> d1['x'] = 100
    >>> d1.x
    100
    >>> d1.y = 200
    >>> d1['y']
    200
    >>> d2 = Dict(a=1, b=2, c='3')
    >>> d2.c
    '3'
    >>> d2['empty']
    Traceback (most recent call last):
        ...
    KeyError: 'empty'
    >>> d2.empty
    Traceback (most recent call last):
        ...
    AttributeError: 'Dict' object has no attribute 'empty'
    '''
    def __init__(self, **kw):
        super(Dict, self).__init__(**kw)

    def __getattr__(self, key):
        try:
            return self[key]
        except KeyError:
            raise AttributeError(r"'Dict' object has no attribute '%s'" % key)

    def __setattr__(self, key, value):
        self[key] = value

if __name__=='__main__':
    import doctest
    doctest.testmod()

运行python mydict2.py

$ python mydict2.py

什么输出也没有。这说明我们编写的doctest运行都是正确的。如果程序有问题,比如把__getattr__()方法注释掉,再运行就会报错:

$ python mydict2.py
**********************************************************************
File "/Users/michael/Github/learn-python3/samples/debug/mydict2.py", line 10, in __main__.Dict
Failed example:
    d1.x
Exception raised:
    Traceback (most recent call last):
      ...
    AttributeError: 'Dict' object has no attribute 'x'
**********************************************************************
File "/Users/michael/Github/learn-python3/samples/debug/mydict2.py", line 16, in __main__.Dict
Failed example:
    d2.c
Exception raised:
    Traceback (most recent call last):
      ...
    AttributeError: 'Dict' object has no attribute 'c'
**********************************************************************
1 items had failures:
   2 of   9 in __main__.Dict
***Test Failed*** 2 failures.

注意到最后3行代码。当模块正常导入时,doctest不会被执行。只有在命令行直接运行时,才执行doctest。所以,不必担心doctest会在非测试环境下执行。


喜欢的三连哦 😃

Logo

GitCode 天启AI是一款由 GitCode 团队打造的智能助手,基于先进的LLM(大语言模型)与多智能体 Agent 技术构建,致力于为用户提供高效、智能、多模态的创作与开发支持。它不仅支持自然语言对话,还具备处理文件、生成 PPT、撰写分析报告、开发 Web 应用等多项能力,真正做到“一句话,让 Al帮你完成复杂任务”。

更多推荐