python AI技术教程之错误和异常
在程序运行过程中,总会遇到各种各样的错误。
有的错误是程序编写有问题造成的,比如本来应该输出整数结果输出了字符串,这种错误我们通常称之为bug,bug是必须修复的。
有的错误是用户输入造成的,比如让用户输入email地址,结果得到一个空字符串,这种错误可以通过检查用户输入来做相应的处理。
还有一类错误是完全无法在程序运行过程中预测的,比如写入文件的时候,磁盘满了,写不进去了,或者从网络抓取数据,网络突然断掉了。这类错误也称为异常,在程序中通常是必须处理的,否则,程序会因为各种问题终止并退出。
Python内置了一套异常处理机制,来帮助我们进行错误处理。
此外,我们也需要跟踪程序的执行,查看变量的值是否正确,这个过程称为调试。Python的pdb可以让我们以单步方式执行代码。
最后,编写测试也很重要。有了良好的测试,就可以在程序修改后反复运行,确保程序输出符合我们编写的测试。
python内置了一套try...except...finally...的错误处理机制。
try块结构
单一异常
让我们用一个例子来看看try的机制:
try包含的是程序代码。
try:
r = 10 / 0
print('结果:', r)
except ZeroDivisionError as e:
print('异常:', e)
finally:
print('最终执行完毕...')
当我们认为某些代码可能会出错时,就可以用try来运行这段代码,如果执行出错,则后续代码不会继续执行,而是直接跳转至错误处理代码,即except语句块,执行完except后,如果有finally语句块,则执行finally语句块,至此,执行完毕。
上面的代码在计算10 / 0时会产生一个除法运算错误:
异常: division by zero
最终执行完毕...
从输出可以看到,当错误发生时,后续语句print('result:', r)不会被执行,except由于捕获到ZeroDivisionError,因此被执行。最后,finally语句被执行。然后,程序继续按照流程往下走。
如果把除数0改成2,则执行结果如下:
结果: 5
最终执行完毕...
由于没有错误发生,所以except语句块不会被执行,但是finally如果有,则一定会被执行(可以没有finally语句)。
多个异常
错误应该有很多种类,如果发生了不同类型的错误,应该由不同的except语句块处理。没错,可以有多个except来捕获不同类型的错误:
try:
r = 10 / int('a')
print('结果:', r)
except ValueError as e:
print('ValueError:', e)
except ZeroDivisionError as e:
print('ZeroDivisionError:', e)
finally:
print('最终执行完毕...')
int()函数可能会抛出ValueError,所以我们用一个except捕获ValueError,用另一个except捕获ZeroDivisionError。
else结构
此外,如果没有错误发生,可以在except语句块后面加一个else,当没有错误发生时,会自动执行else语句:
try:
r = 10 / int('2')
print('结果:', r)
except ValueError as e:
print('ValueError:', e)
except ZeroDivisionError as e:
print('ZeroDivisionError:', e)
else:
print('程序运行良好,没有异常!')
finally:
print('最终执行完毕...')
Python的错误其实也是class,所有的错误类型都继承自BaseException,所以在使用except时需要注意的是,它不但捕获该类型的错误,还把其子类也“一网打尽”。
try:
# r=10/int('a')
r=10/0
# r=10/3
print("结果:",r)
except ValueError as e: #BaseException可以提到上面做测试
print('value异常信息:',e)
except ZeroDivisionError as e:
print('zero异常信息:',e)
except BaseException as e:
print('老总异常:',e)
else:
print('程序正常运行,无bug,太牛了...')
finally:
print('程序结束了...')
常见的错误类型和继承关系
BaseException
├── BaseExceptionGroup
├── GeneratorExit
├── KeyboardInterrupt
├── SystemExit
└── Exception
├── ArithmeticError
│ ├── FloatingPointError
│ ├── OverflowError
│ └── ZeroDivisionError
├── AssertionError
├── AttributeError
├── BufferError
├── EOFError
├── ExceptionGroup [BaseExceptionGroup]
├── 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
│ ├── PythonFinalizationError
│ └── RecursionError
├── StopAsyncIteration
├── StopIteration
├── SyntaxError
│ └── IndentationError
│ └── TabError
├── SystemError
├── TypeError
├── ValueError
│ └── UnicodeError
│ ├── UnicodeDecodeError
│ ├── UnicodeEncodeError
│ └── UnicodeTranslateError
└── Warning
├── BytesWarning
├── DeprecationWarning
├── EncodingWarning
├── FutureWarning
├── ImportWarning
├── PendingDeprecationWarning
├── ResourceWarning
├── RuntimeWarning
├── SyntaxWarning
├── UnicodeWarning
└── UserWarning
使用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...')
也就是说,不需要在每个可能出错的地方去捕获错误,只要在合适的层次去捕获错误就可以了。这样一来,就大大减少了写try...except...finally的麻烦。
调试技巧
程序能一次写完并正常运行的概率很小,基本不超过1%。总会有各种各样的bug需要修正。有的bug很简单,看看错误信息就知道,有的bug很复杂,我们需要知道出错时,哪些变量的值是正确的,哪些变量的值是错误的,因此,需要一整套调试程序的手段来修复bug。
第一种方法简单直接粗暴有效,就是用print()把可能有问题的变量打印出来看看:
def foo(s):
n = int(s)
print('>>> n = %d' % n)
return 10 / n
def main():
foo('0')
main()
执行后在输出中查找打印的变量值:
$ python err.py
>>> n = 0
Traceback (most recent call last):
...
ZeroDivisionError: integer division or modulo by zero
用print()最大的坏处是将来还得删掉它,想想程序里到处都是print(),运行结果也会包含很多垃圾信息。所以,我们又有第二种方法。
断言
凡是用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 条件表达式, 错误提示 的逻辑是:
- 如果 “条件表达式的结果是 True” → 断言通过,程序继续往下走;
- 如果 “条件表达式的结果是 False” → 断言失败,抛出
AssertionError+ 错误提示。
程序中如果到处充斥着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的好处,它允许你指定记录信息的级别,有debug,info,warning,error等几个级别,当我们指定level=INFO时,logging.debug就不起作用了。同理,指定level=WARNING后,debug和info就不起作用了。这样一来,你可以放心地输出不同级别的信息,也不用删除,最后统一控制输出哪个级别的信息。
logging的另一个好处是通过简单的配置,一条语句可以同时输出到不同的地方,比如console和文件。
应用案例:
import logging
# 进阶配置日志:不仅输出到控制台,还能记录到文件,格式更清晰
logging.basicConfig(
level=logging.DEBUG, # 日志级别:DEBUG < INFO < WARNING < ERROR < CRITICAL
format='%(asctime)s - %(levelname)s - %(message)s', # 日志格式(时间+级别+信息)
handlers=[
logging.FileHandler('app.log'), # 日志保存到 app.log 文件
logging.StreamHandler() # 同时在控制台输出
]
)
def safe_divide(a, b_str):
"""安全除法函数:处理字符串转整数、除零问题"""
try:
# 步骤1:字符串转整数
b = int(b_str)
logging.debug(f'输入的字符串 "{b_str}" 已转为整数 {b}') # DEBUG 级日志(调试用)
# 步骤2:判断是否为 0
if b == 0:
logging.error('除数不能为 0!') # ERROR 级日志(记录错误)
return None
# 步骤3:执行除法
result = a / b
logging.info(f'{a} ÷ {b} = {result}') # INFO 级日志(记录正常结果)
return result
except ValueError:
# 处理字符串无法转整数的情况(比如 s='abc')
logging.error(f'字符串 "{b_str}" 不能转为整数!')
return None
# 实际调用(测试 3 种情况)
safe_divide(10, '2') # 正常情况:10 ÷ 2 = 5
safe_divide(10, '0') # 错误情况:除数为 0
safe_divide(10, 'abc') # 异常情况:字符串无法转整数
- 点赞
- 收藏
- 关注作者
评论(0)