【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 <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】函数、类、模块和包如何构建四级模块化体系
  • 来源:i机器未来

    物联沃分享整理
    物联沃-IOTWORD物联网 » 【Python零基础快速入门系列 | 12】程序员为什么自嘲面向Bug编程?今天来聊一聊异常管理

    发表评论