【Python零基础快速入门系列 | 12】程序员为什么自嘲面向Bug编程?今天来聊一聊异常管理

这是机器未来的第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 
      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 
----> 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 
     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 
     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】函数、类、模块和包如何构建四级模块化体系

你可能感兴趣的:(Python零基础快速入门系列,python,bug,开发语言,Python基础,python学习笔记)