这是机器未来的第22篇文章
原文首发地址:https://blog.csdn.net/RobotFutures/article/details/125454677
1. 概述
程序员经常自嘲,“面向BUG编程”,它不是玩笑,是真的!非常贴切!!!程序员基本上天天与BUG打交道,写BUG,改BUG, 写BUG,改BUG…无限循环。那么怎么驾驭BUG呢,今天来认识一下BUG!
2. 错误与异常
我们根据程序编译时和运行时两种场景,将BUG区分为错误和异常。
2.1 错误
错误又分为语法错误和逻辑错误。
- 语法错误是编译时,编译器直接报错的类型,这种类型是比较简单的,编译器会直接报错,直接解决就行,例如
# 语法错误
def add(a, b):
return a+b
File "C:\Users\ZHOUSH~1\AppData\Local\Temp/ipykernel_19160/942780934.py", line 2
def add(a, b)
^
SyntaxError: invalid syntax
如上提示语法错误,在def add(a, b)后面标识了个向上的三角符号,表示少了冒号。
- 逻辑错误
逻辑错误编译器不会报错,程序可以正常运行,但是拿不到预期的结果。例如计算奇偶数,本来设计的是整除2为偶数,其余为奇数,但是判断条件设置错误,导致不能获得正确的值。
x = 12
if x % 2 == 0:
print("x 是奇数")
else:
print("x 是偶数")
x 是奇数
这里都是展示的都是很简单的例子,一个真实的项目往往很复杂,业务逻辑复杂后,出现逻辑错误会相对难以定位问题点。但是足够细心也是可以快速定位问题的,将代码分块,给定预期输出,判断结果是否满足预期即可定位。项目开发完成后,往往会进行单元测试,覆盖所有可能的场景。
2.2 异常
排除语法错误后,出现在执行时出现的问题,被称为异常。举个简单的例子:
a = 10
b = 0
a / b
print("程序结束")
---------------------------------------------------------------------------
ZeroDivisionError Traceback (most recent call last)
C:\Users\ZHOUSH~1\AppData\Local\Temp/ipykernel_19160/565678119.py in <module>
1 a = 10
2 b = 0
----> 3 a / b
4 print("程序结束")
ZeroDivisionError: division by zero
当被除数为0时,程序抛出异常,并且停止运行,可以看到“程序结束”未打印输出,在处理之前就结束了。那么Python内部支持哪些异常呢?Python的异常是Exception类管理的,我们用Exception??看一下
Exception??
Init signature: Exception(self, /, *args, **kwargs)
Docstring: Common base class for all non-exit exceptions.
Type: type
Subclasses: TypeError, StopAsyncIteration, StopIteration, ImportError, OSError, EOFError, RuntimeError, NameError, AttributeError, SyntaxError, ...
可以看到它有很多子类:TypeError, StopAsyncIteration, StopIteration, ImportError, OSError, EOFError, RuntimeError, NameError, AttributeError, SyntaxError, …,每个子类代表一种错误或异常,我们刚才看的语法错误、除零错误都可以在它的子类中看到。
3. 异常处理
刚才我们提到,程序抛出异常时,程序会退出,在有一些场景,我希望即使数据异常,我仍然希望它继续运行,对于不符合要求的数据填充默认值进行处理,在爬虫数据抓取的过程中经常用到。
比如我想分析一下市场股票的走势,需要拉取很多股票的数据,但是因为各种原因,可能爬取的数据不全,有缺失的情况,加载到程序中处理,就会报数值错误、不能除零等等,对于这种情况我们会对数据进行预处理,不能满足输入条件的数据给予默认值,让他满足程序输入的要求,并保持程序继续运行,不直接退出。
这里要用到异常捕获语句:
try:
pass # 正常代码执行块
except ExceptionA:
pass # 异常类型A处理代码块
except ExceptionB:
pass # 异常类型B处理代码块
except:
pass # 捕获除了异常类型A和异常类型B之外所有异常代码块
else:
pass # 没有异常时代码块(可选)
finally:
pass # 不论是否有异常,都会执行的代码块
上面的异常捕获语句是最完整的结构,实际使用时按需选择,不一定需要全部支持,例如在程序设计时,对于外部输入一般要进行合法性校验,假如现在我们要输入年龄,该如何设计呢?
age = int(input('请输入一个你的年龄:'))
print(f"你的年龄为{
age}")
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
C:\Users\ZHOUSH~1\AppData\Local\Temp/ipykernel_19160/1653428413.py in <module>
----> 1 age = int(input('请输入一个你的年龄:'))
2 print(f"你的年龄为{age}")
ValueError: invalid literal for int() with base 10: 'qw'
从上面的代码中可以看到,程序设计预期想输入的数据为整数类型,但是用户各种各样,它输入字母时就抛出异常了,那么怎么将程序设计的更加友好,更加人性化呢?
try:
age = int(input('请输入一个你的年龄:'))
print(f"你的年龄为{
age}")
except:
print("您输入的数据类型不对,请输入整数!")
您输入的数据类型不对,请输入整数!
可以看到,程序不再生硬的抛出一堆代码异常给用户,而是友好的提示信息。下面演示程序中捕获多个异常:
try:
a = int(input("请输入除数a:"))
b = int(input("请输入被除数b:"))
c = a / b
except ValueError:
print("除数、被除数必须为整数!")
except ZeroDivisionError:
print("被除数不能为0!")
except Exception as e: # 未知异常的捕获
print(f"未识别的异常, 输入值为{
a}, {
b}, 异常信息:{
e}")
else:
print(f"计算结果:{
c}") # 未发生错误时输出计算结果
finally:
print("程序运行完毕")
计算结果:16.0
程序运行完毕
上面的例程捕获了多个异常ValueError和ZeroDivisionError,并且对于未知的异常也进行了管理,通过Exception捕获异常信息,并且同时记录了异常的输入信息,便于快速定位问题, 类似这样:
# 此输出在注释 ValueError和ZeroDivisionError 两个异常分支后输入32和0获得的。
未识别的异常, 输入值为32, 0, 异常信息:division by zero
程序运行完毕
上面的例程在没有异常发生时,通过else语句输出计算结果;
上面的例程不论是否发生异常,最终的finally语句都会被执行。
对于多个异常,也可以组合成一个处理逻辑,类似这样:
try:
a = int(input("请输入除数a:"))
b = int(input("请输入被除数b:"))
c = a / b
except (ValueError, ZeroDivisionError):
print("除数、被除数必须为整数!且被除数不能为0!")
except Exception as e: # 未知异常的捕获
print(f"未识别的异常, 输入值为{
a}, {
b}, 异常信息:{
e}")
else:
print(f"计算结果:{
c}") # 未发生错误时输出计算结果
finally:
print("程序运行完毕")
除数、被除数必须为整数!且被除数不能为0!
程序运行完毕
4. 抛出异常
在捕获异常章节提到,在一些场景发生异常,在进行相关的处理后仍然可以继续运行,但有些场景,异常是致命的,它不能满足程序输入需求,需要直接退出程序,那么怎么处理呢?
这里用raise主动抛出异常,终止程序。
例如一个深度学习模型的标签数据的矩阵结构为(3, 1),但是输入形状为(3),程序将无法处理,应该立即抛出异常,并且告知原因以待修改。
import numpy as np
y = [1, 2, 3]
y_pred = [[1], [2], [3]]
y1 = np.array(y)
y2 = np.array(y_pred)
print(y1.shape, y2.shape)
if y1.shape[-1] == 1: # 检查最后一维是否为1
print("data shape is ok")
else:
raise ValueError(f"数据形状不一致,输入数据形状为{
y1.shape}, 期望形状为{
y2.shape}")
(3,) (3, 1)
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
C:\Users\ZHOUSH~1\AppData\Local\Temp/ipykernel_19160/4000627889.py in <module>
12 print("data shape is ok")
13 else:
---> 14 raise ValueError(f"数据形状不一致,输入数据形状为{y1.shape}, 期望形状为{y2.shape}")
ValueError: 数据形状不一致,输入数据形状为(3,), 期望形状为(3, 1)
5. 自定义异常
class MyError(Exception):
"""
自定义异常类
"""
def __init__(self, msg):
super(MyError, self).__init__
self.msg = msg
import numpy as np
y = [1, 2, 3]
y_pred = [[1], [2], [3]]
y1 = np.array(y)
y2 = np.array(y_pred)
print(y1.shape, y2.shape)
if y1.shape[-1] == 1: # 检查最后一维是否为1
print("data shape is ok")
else:
raise MyError(f"数据形状不一致,输入数据形状为{
y1.shape}, 期望形状为{
y2.shape}")
(3,) (3, 1)
---------------------------------------------------------------------------
MyError Traceback (most recent call last)
C:\Users\ZHOUSH~1\AppData\Local\Temp/ipykernel_19160/3613492880.py in <module>
17 print("data shape is ok")
18 else:
---> 19 raise MyError(f"数据形状不一致,输入数据形状为{y1.shape}, 期望形状为{y2.shape}")
MyError: 数据形状不一致,输入数据形状为(3,), 期望形状为(3, 1)
捕获自定义异常类,如下的代码可以看到,自定义异常类是可以被捕获到的,而且增加捕获程序后,输出信息更加友好。
class MyError(Exception):
"""
自定义异常类
"""
def __init__(self, msg):
super(MyError, self).__init__
self.msg = msg
import numpy as np
y = [1, 2, 3]
y_pred = [[1], [2], [3]]
y1 = np.array(y)
y2 = np.array(y_pred)
print(y1.shape, y2.shape)
try:
if y1.shape[-1] == 1: # 检查最后一维是否为1
print("data shape is ok")
else:
raise MyError(f"数据形状不一致,输入数据形状为{
y1.shape}, 期望形状为{
y2.shape}")
except Exception as e:
print(e)
(3,) (3, 1)
数据形状不一致,输入数据形状为(3,), 期望形状为(3, 1)
6.异常处理注意事项与建议
6.1 注意事项
- 只执行最先匹配的一个except
- 如果父类异常在最前面,会吞噬所有子类异常
- 多except注意:
- 只会匹配一个except
- 要先写子类异常再写父类异常
- 如果except捕获的错误与触发的错误不一致,程序会捕获不到
6.2 使用建议
- 不建议使用异常来代替常规的检查,如if…else判断
- 避免过多依赖于异常处理机制
- 在必要的时候,可以手动引发异常(raise)=> 函数或方法
- 在函数中,需要注意在try/except/finally使用return
- 在finally中使用return,异常无法回溯
- 在函数中的try/except语句使用return后,仍然会执行finally中的内容
以上就是Python异常处理的基础知识了。后面列一些常见的异常目录。
7. 附录 常见异常
-
BaseException 所有异常的基类
-
SystemExit 解释器请求退出
-
KeyboardInterrupt 用户中断执行(通常是输入^C)
-
Exception 常规错误的基类
-
StopIteration 迭代器没有更多的值
-
GeneratorExit 生成器(generator)发生异常来通知退出
-
StandardError 所有的内建标准异常的基类
-
ArithmeticError 所有数值计算错误的基类
-
FloatingPointError 浮点计算错误
-
OverflowError 数值运算超出最大限制
-
ZeroDivisionError 除(或取模)零 (所有数据类型)
-
AssertionError 断言语句失败
-
AttributeError 对象没有这个属性
-
EOFError 没有内建输入,到达EOF 标记
-
EnvironmentError 操作系统错误的基类
-
IOError 输入/输出操作失败
-
OSError 操作系统错误
-
WindowsError 系统调用失败
-
ImportError 导入模块/对象失败
-
LookupError 无效数据查询的基类
-
IndexError 序列中没有此索引(index)
-
KeyError 映射中没有这个键
-
MemoryError 内存溢出错误(对于Python 解释器不是致命的)
-
NameError 未声明/初始化对象 (没有属性)
-
UnboundLocalError 访问未初始化的本地变量
-
ReferenceError 弱引用(Weak reference)试图访问已经垃圾回收了的对象
-
RuntimeError 一般的运行时错误
-
NotImplementedError 尚未实现的方法
-
SyntaxError Python 语法错误
-
IndentationError 缩进错误
-
TabError Tab 和空格混用
-
SystemError 一般的解释器系统错误
-
TypeError 对类型无效的操作
-
ValueError 传入无效的参数
-
UnicodeError Unicode 相关的错误
-
UnicodeDecodeError Unicode 解码时的错误
-
UnicodeEncodeError Unicode 编码时错误
-
UnicodeTranslateError Unicode 转换时错误
-
Warning 警告的基类
-
DeprecationWarning 关于被弃用的特征的警告
-
FutureWarning 关于构造将来语义会有改变的警告
-
OverflowWarning 旧的关于自动提升为长整型(long)的警告
-
PendingDeprecationWarning 关于特性将会被废弃的警告
-
RuntimeWarning 可疑的运行时行为(runtime behavior)的警告
-
SyntaxWarning 可疑的语法的警告
-
UserWarning 用户代码生成的警告
《Python零基础快速入门系列》快速导航:
- 【Python零基础入门笔记 | 01】 人工智能序章:开发环境搭建Anaconda+VsCode+JupyterNotebook(零基础启动)
- 【Python零基础入门笔记 | 02】一文快速掌握Python基础语法
- 【Python零基础入门笔记 | 03】AI数据容器底层核心之Python列表
- 【Python零基础入门笔记 | 04】为什么内存中最多只有一个“Love“?一文读懂Python内存存储机制
- 【Python零基础入门笔记 | 05】Python只读数据容器:列表List的兄弟,元组tuple
- 【Python零基础入门笔记 | 06】字符串、列表、元组原来是一伙的?快看序列Sequence
- 【Python零基础入门笔记 | 07】成双成对之Python数据容器字典
- 【Python零基础入门笔记 | 08】无序、不重复、元素只读,Python数据容器之集合
- 【Python零基础入门笔记 | 09】高级程序员绝世心法——模块化之函数封装
- 【Python零基础入门笔记 | 10】类的设计哲学:自然法则的具现
- 【Python零基础入门笔记 | 11】函数、类、模块和包如何构建四级模块化体系