python基础-Bug与异常机制

“Bug”指程序错误,源于1947年飞蛾卡继电器事件。分类包括语法错误、运行时异常、逻辑错误和环境错误。Python用try-except处理异常,支持捕获特定或全部异常,else和finally块确保逻辑清晰和资源释放。自定义异常和traceback模块增强调试能力

Posted by Hilda on July 2, 2025

1、Bug的由来及分类

“Bug”是指程序中的错误或缺陷,导致程序行为不符合预期。它们通常分为语法错误、运行时错误(异常)、逻辑错误和环境错误。

(1)Bug的起源

“Bug”一词的起源可以追溯到计算机早期。最著名的故事是1947年,计算机科学家格蕾丝·霍珀(Grace Hopper)在Mark II计算机中发现一只飞蛾卡在继电器中,导致计算机故障。她将这只飞蛾从日志中取出,并写下了“First actual case of bug being found”(第一个发现的真正Bug)。自此,“Bug”便成为计算机程序错误的代称。

Bug是软件开发过程中不可避免的一部分,它们可能导致程序崩溃、产生错误结果、安全漏洞或性能问题。理解Bug的分类有助于我们更有效地识别和解决它们。

(2)Bug的分类

  • 语法错误 (Syntax Errors):

定义: 违反了Python语言的语法规则。这类错误在程序运行前(即解释器解析代码时)就会被检测到。

表现: 解释器会抛出 SyntaxError,并指出错误发生的行号和位置。

原理: Python解释器在执行代码之前会进行“解析”(parsing)阶段。在这个阶段,它会尝试将源代码转换为抽象语法树(AST)。如果代码不符合Python的语法规范(例如缺少冒号、括号不匹配、关键字拼写错误等),解析器就无法成功构建AST,从而报告 SyntaxError。这类错误是最低级的错误,通常最容易修复。

  • 运行时错误 / 异常 (Runtime Errors / Exceptions):

定义: 程序在执行过程中发生的错误,通常是因为某个操作无法完成或数据不符合预期。这些错误在语法上是合法的,但在特定运行时条件下才会显现。

表现: 解释器会抛出各种具体的异常类型(如 NameError, TypeError, ValueError, ZeroDivisionError, FileNotFoundError 等)。如果未被捕获和处理,程序将终止。

原理: Python的运行时系统在执行代码时会不断检查操作的合法性。例如,当尝试除以零时,系统会检测到这种非法操作,并根据预定义的规则创建一个 ZeroDivisionError 对象。这个异常对象会沿着函数调用栈向上“传播”(unwinding the stack),直到找到一个匹配的 except 块来处理它。如果整个调用栈都没有找到合适的处理者,异常就会导致程序崩溃。

  • 逻辑错误 (Logical Errors):

定义: 程序按照预期运行,没有抛出任何错误或异常,但输出结果不正确,或者程序的行为不符合设计意图。

表现: 程序正常结束,但结果是错误的。这类错误是最难发现和调试的,因为它们不会立即导致程序崩溃。

原理: 逻辑错误通常是程序员对问题理解有误、算法设计缺陷或实现细节不正确造成的。解释器无法自动检测这类错误,因为它认为代码的执行流程是合法的。发现逻辑错误需要通过严谨的测试、预期结果与实际结果的对比、以及细致的调试(例如使用调试器逐步执行代码,检查变量值)。

  • 环境错误 (Environmental Errors):

定义: 与程序运行环境相关的错误,而不是代码本身的错误。

表现: 例如,文件不存在(即使代码路径正确)、网络连接中断、权限不足、依赖库未安装或版本不兼容等。

原理: 这类错误发生在程序与外部系统(文件系统、网络、操作系统、第三方库)交互时。Python代码本身可能完全正确,但由于外部条件的限制或不满足,导致程序无法正常执行。这些错误通常需要检查系统配置、网络状态、文件权限或依赖项。

(3)错误 (Error) 与 异常 (Exception) 的区别

在Python中,所有的运行时错误都是异常(Exception 类或其子类的实例)。“错误”是一个更广泛的概念,包含了语法错误和逻辑错误。而“异常”特指程序运行时可以被捕获和处理的错误事件。Python的异常处理机制(try-except)就是专门用来优雅地处理这些运行时异常的。


下面看一些例子:

【1】粗心导致的语法错误:

image-20250702180216217

(1)是age在input时是str类型,而进入if比较时应该转成int类型。

(2)首先i没有初始化,第二,(i)用的是中文的括号,第三,i在执行体中没有增量

image-20250702180342986

(3)问题是=是赋值,==才是判断相等与否

以上错误可以大致归纳为:

1.漏了末尾的冒号,如if语句,循环语句,else子句等

2.缩进错误,该缩进的没缩进,不该缩进的瞎缩进

3.把英文符号写成中文符号,比如说:引号,冒号,括号

4.字符串拼接的时候,把字符串和数字拼在一起

5.没有定义变量,比如说while的循环条件的变量

6.“==”比较运算符和”=”赋值运算符的混用

【2】知识不熟练导致的错误

image-20250702202325198

(1)中list长度是4,合法的索引值是0-3,所以要得到list的最后一个值44,可以是lst[3]或者lst[-1]

(2)append每次只添加一个元素,如果想一下子添加多个元素,可以用extend方法:

1
2
3
lst = []
lst.extend(["A", "B", "C"])
print(lst)

知识点不熟悉导致错误时,需要多练习。

【3】思路不清导致的问题解决方案

现在看一个场景,需要完成以下功能:

题目要求:豆瓣电影Top250排行,使用列表存储电影信息,

要求输入名字在屏幕上显示xxx出演了哪部电影。

image-20250702202751679

已有代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
lst=[{'rating':[9.7,2062397],'id':'1292052','type':['犯罪','剧情'],'title':'肖申克的救赎','actors':['蒂姆·罗宾斯','摩根·弗里曼']},
    {'rating':[9.6,1528760],'id':'1291546','type':['剧情','爱情','同性'],'title':'霸王别姬','actors':['张国荣' ,'张丰毅' , '巩俐' ,'葛优']},
    {'rating':[9.5,1559181],'id':'1292720','type':['剧情','爱情'],'title':'阿甘正传','actors':['汤姆·汉克斯','罗宾·怀特 ']}
     ]

name=input('请输入你要查询的演员:')

for item in lst:  #遍历列表  -->{}  item是一个又一个的字典
    for movie in item:
        actors = movie["actors"]
        if name in actors:
            print(name,'出演了',item['title'])

查询出现报错:

image-20250702203138235

原因是得到的item是字典,上面的代码把内容搞复杂了,合理的写法如下:

1
2
3
4
5
6
7
8
9
10
11
lst=[{'rating':[9.7,2062397],'id':'1292052','type':['犯罪','剧情'],'title':'肖申克的救赎','actors':['蒂姆·罗宾斯','摩根·弗里曼']},
    {'rating':[9.6,1528760],'id':'1291546','type':['剧情','爱情','同性'],'title':'霸王别姬','actors':['张国荣' ,'张丰毅' , '巩俐' ,'葛优']},
    {'rating':[9.5,1559181],'id':'1292720','type':['剧情','爱情'],'title':'阿甘正传','actors':['汤姆·汉克斯','罗宾·怀特 ']}
     ]

name=input('请输入你要查询的演员:')

for item in lst:  #遍历列表  -->{}  item是一个又一个的字典
    actors = item["actors"]
    if name in actors:
        print(name,'出演了',item['title'])

image-20250702203651228

第一层for循环遍历列表可以得到每一部电影,而每一部电影又是一个字典,只需要根据key在字典中取值即可。根据演员的键actors取出学员的列表,使用判断name在列表中是否存在,最后根据电影名称的键title取出电影的名称,进行输出

【4】被动掉坑:程序代码逻辑没有错,只是因为用户错误操作或者一些“例外情况”而导致的程序崩溃

Python提供了异常处理机制,可以在异常出现时即时捕获,然后内部“消化”,让程序继续运行

例如:输入两个整数并进行除法运算

image-20250702204510348

image-20250702204519655

加入try-except进行异常的捕获:

1
2
3
4
5
6
7
8
9
10
try:
    a = int(input("请输入第一个数:"))
    b = int(input("请输入第二个数:"))
    res = a / b
    print(f"{a}/{b}的结果是{res}")
except ZeroDivisionError as e:
    print(f"程序报错:{e}, 不允许被除数是0!")
except ValueError as e:
    print(f"程序报错:{e}, 非法的输入!")
print("程序结束")

2、不同异常类型的处理方式

Python使用 try-except 语句来捕获和处理运行时异常。可以捕获特定类型的异常,也可以捕获多个异常类型,甚至捕获所有异常。

try-except 块是Python异常处理的核心。它允许你将可能引发异常的代码放在 try 块中,并在 except 块中定义如何处理这些异常。

try 块: 包含可能引发异常的代码。

except 块:

捕获特定异常: except ExceptionType:。当 try 块中发生 ExceptionType 类型的异常时,对应的 except 块会被执行。

捕获多个特定异常: except (ExceptionType1, ExceptionType2):。将多个异常类型放在一个元组中,可以为它们编写相同的处理逻辑

捕获所有异常: except Exception:。这将捕获所有继承自 Exception 类的异常。虽然方便,但通常不推荐无差别捕获所有异常,因为它可能掩盖你未预料到的错误,使调试变得困难。更好的做法是捕获你预期的特定异常,或者在捕获 Exception 后重新抛出(raise)不理解的异常。

获取异常对象: except ExceptionType as e:。e 是一个变量,它会绑定到捕获到的异常对象上,你可以通过它访问异常的详细信息(如错误消息)。


(1)异常层级结构

image-20250702221405074

Python的异常是类层次结构。所有的内置异常都继承自 BaseException。Exception 类是大多数用户自定义异常和非系统退出异常的基类。当你捕获一个父类异常时,它也会捕获其所有子类异常。因此,在 except 块的顺序上,应该先捕获更具体的异常,再捕获更一般的异常,否则更具体的异常可能永远不会被捕获到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
BaseException
├── SystemExit
├── KeyboardInterrupt
├── GeneratorExit
└── Exception
    ├── ArithmeticError
       ├── FloatingPointError
       ├── OverflowError
       └── ZeroDivisionError
    ├── AttributeError
    ├── EOFError
    ├── FileNotFoundError
    ├── IndexError
    ├── KeyError
    ├── NameError
    ├── TypeError
    ├── ValueError
    └── ... (还有很多其他异常类型)

当 try 块中的代码执行时,Python解释器会对其进行监控。如果发生异常:

  • 异常对象创建: 解释器会创建一个异常对象(例如 ZeroDivisionError 的实例)。

  • 栈展开 (Stack Unwinding): 解释器会暂停当前代码的执行,并沿着函数调用栈(从当前函数向上到调用它的函数,再到调用那个函数的函数,以此类推)寻找能够处理这个异常的 except 块。

  • 匹配与执行:

○ 在每一层栈帧中,解释器都会检查是否有 try 块及其对应的 except 块。

○ 它会从上到下(代码顺序)检查 except 块,看异常对象的类型是否与 except 后面指定的异常类型匹配(即异常对象是指定类型的实例,或者是其子类的实例)。

○ 找到第一个匹配的 except 块后,程序会跳转到该块的代码开始执行。

○ 一旦 except 块执行完毕,程序将继续执行 try-except 结构之后的代码(或者如果存在 else 或 finally 块,则执行它们)。

○ 如果没有找到任何匹配的 except 块,异常会继续向上层传播,直到到达程序的顶层。如果顶层也没有处理,程序就会终止并打印未捕获的异常信息(Traceback)。

(2)捕获单个特定异常

1
2
3
4
5
6
7
8
9
10
11
def divide_fun(a, b):
    try:
        res = a / b
        print(f"{a}/{b}结果是:{res}")
    except ZeroDivisionError:
        print("除数不可为0!!!")
    except TypeError:
        print("输入类型不正确,请确保是数字。")
divide_fun(10, 2)
divide_fun(10, 0)
divide_fun("a", 10)

image-20250702205449917

(3)捕获多个特定异常 (使用元组)

1
2
3
4
5
6
7
8
9
10
11
12
# 同时捕获 IndexError 和 TypeError
def get_list_index(lst, index):
    try:
        print(f"{lst}的第{index+1}个元素是{lst[index]}")
    except (IndexError, TypeError) as e:
        print(f"报错:{e}")
        print("请检查是否越界或者列表元素类型是否正确")

data_list = [1, 2, "Three", 4]
get_list_index(data_list, 1)
get_list_index(data_list, 5)
get_list_index(data_list, "a")

image-20250702205846887

(4)捕获所有异常 (不推荐-无差别使用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def risky_operation(x, y):
    try:
        # 人为创造一个异常
        if y == 0:
            raise ValueError("y 在这个函数中不可以为0")
        res = x / y
        print(f"操作结果:{res}")
    except Exception as e:
        print(f"发生了一个未知错误:{type(e).__name__}-{e}")
        # 在这里可以记录日志,然后根据需要重新抛出或采取其他恢复措施

risky_operation(10, 5)
risky_operation(10, 0)
risky_operation("a", 2)

image-20250702210413055

(5)异常捕获顺序的重要性 (从具体到一般)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def specific_general_order(n):
    try:
        if n == 0:
            raise ZeroDivisionError("除零了!!")
        elif n == 1:
            raise ValueError("值不正确,不可为1")
        else:
            print(f"值是:{n}")
    except ZeroDivisionError as e:
        print(f"捕获到ZeroDivisionError 异常:{e}")
    except ValueError as e:
        print(f"捕获到 ValueError:{e}")
    except Exception as e:
        print(f"捕获到通用:{e}")

specific_general_order(0)
specific_general_order(1)
specific_general_order(10)

image-20250702210759199

越是具体的异样,越先捕获。

3、异常处理机制

Python的异常处理机制由 try, except, else, finally 块组成,提供了一个结构化的方式来管理程序中的错误,确保资源的正确释放和代码的健壮性。

(1)完整的异常处理结构

image-20250702221340536

完整的异常处理结构包括 try, except, else, 和 finally 块。

  • try 块:

○ 放置你认为可能引发异常的代码。

○ 这是异常处理的起点。

  • except 块:

○ 捕获并处理 try 块中发生的特定类型或所有类型的异常。

○ 可以有多个 except 块来处理不同类型的异常。

  • else 块 (可选):

○ 如果 try 块中的代码没有引发任何异常,那么 else 块中的代码就会被执行。

用途: 适用于那些只有在 try 块成功执行后才应该运行的代码。将这些代码放在 else 块中,可以避免 except 块意外地捕获到 try 块成功后才执行的代码所引发的异常,从而提高代码的清晰度和逻辑分离。

  • finally 块 (可选):

○ 无论 try 块中是否发生异常,无论异常是否被 except 块捕获,也无论 try 或 except 块中是否有 return 或 break 语句,finally 块中的代码总是会被执行。

用途: 主要用于执行清理操作,例如关闭文件、释放锁、关闭数据库连接等,确保资源在任何情况下都能被正确释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def process_data(data, divisor):
    try:
        res = data / divisor
    except ZeroDivisionError:
        print("除数不可为0")
        return None
    except TypeError:
        print("数据或除数类型不正确。")
        return None
    else:
        print(f"计算成功,结果是:{res}")
        return res
    finally:
        print("--- 资源清理或最终操作完成 ---")

process_data(10, 2)
process_data(10, 0)
process_data(10, "a")

image-20250702213217498

(2)raise 语句

● 用于手动引发一个异常。可以引发Python内置的异常,也可以定义并引发自定义异常。

● raise 后面可以跟一个异常类的实例,也可以跟一个异常类(此时会自动创建该类的一个实例)。

● 单独使用 raise(不带任何参数)可以在 except 块中重新抛出当前正在处理的异常,这在需要将异常传递给上层处理时很有用。

(3)自定义异常

● 通过继承 Exception 类(或其子类)来创建自己的异常类。

● 自定义异常可以使你的代码更具表现力,更容易理解特定错误的原因。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class InvalidInputError(Exception):
    def __init__(self, message="输入值无效", value=None):
        self.message = message
        self.value = value
        super().__init__(self.message)

def validate_age(age):
    if not isinstance(age, int):
        raise TypeError("年龄必须是整数!")
    if not (0 <= age <= 150):
        raise InvalidInputError("年龄必须是0-150岁之间!!", value=age)
    print(f"年龄{age}有效")

try:
    validate_age(25)
    validate_age(-5)
    validate_age("abc")
except TypeError as e:
    print(f"捕获到类型错误:{e}")
except InvalidInputError as e:
    print(f"捕获到自定义错误:{e.message}, {e.value}")
except Exception as e:
    print(f"捕获到其他未知异常:{e}")

print("-"*30)
try:
    validate_age("abc")
except TypeError as e:
    print(f"捕获到类型错误:{e}")
except InvalidInputError as e:
    print(f"捕获到自定义错误:{e.message}, {e.value}")
except Exception as e:
    print(f"捕获到其他未知异常:{e}")

image-20250702215807150

(4)底层原理

try 块的监控: 当进入 try 块时,Python解释器会设置一个内部的“异常处理帧”或“保护区域”。

异常发生时的控制流:

  • 当 try 块中的代码引发异常时,当前执行的代码会被中断。

  • 解释器会查找与异常类型匹配的 except 块。

  • 如果找到匹配的 except 块,控制流会跳转到该块。

  • 如果 except 块执行完毕,并且没有重新抛出异常,控制流会跳过 else 块(如果异常发生),直接执行 try-except 结构之后的代码。

  • 如果 try 块没有发生异常,else 块会被执行。

finally 块的保证执行: finally 块的特殊之处在于,无论 try 块中发生什么(包括异常、return 语句、break 语句、甚至 sys.exit()),finally 块的代码都会在控制流离开 try-except-finally 结构之前被执行

栈展开与 finally: 当异常发生并沿着调用栈向上层传播时,finally 块会在栈帧被销毁之前执行其清理代码。

return 与 finally: 如果 try 块或 except 块中有 return 语句,finally 块会在 return 语句真正将值返回给调用方之前执行。如果 finally 块本身也有 return 语句,它会覆盖 try 或 except 块中的 return 值

raise 与 finally: 如果 try 或 except 块中引发了异常(或重新抛出),finally 块会先执行,然后异常会继续向上层传播。

这种机制确保了即使在程序出现意外错误时,关键的资源也能被正确管理和释放,从而提高程序的稳定性和可靠性。

(5)在 except 块中重新抛出异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def outer_function():
    try:
        inner_function()
    except ValueError as e:
        print(f"Outer function 出现 ValueError:{e}")
        raise
    finally:
        print("Outer function的finally部分")

def inner_function():
    try:
        x = int("abc")
    except ValueError as e:
        print(f"Inner function 出现 ValueError:{e}")
        raise
    finally:
        print("Inner function的finally部分")

try:
    outer_function()
except ValueError as e:
    print(f"程序最高层捕获异常:{e}")

image-20250702221201516

(6)traceback模块

【1】使用traceback.print_exc()打印当前的异常堆栈跟踪:

1
2
3
4
5
import  traceback
try:
    res = 10/0
except Exception:
    traceback.print_exc()

image-20250702222137607

【2】使用traceback.format_exc()获取异常信息的字符串:输出与print_exc()类似,但返回字符串,可用于日志记录。

1
2
3
4
5
6
import  traceback
try:
    res = 10/0
except Exception:
    error_message = traceback.format_exc()
    print(error_message)

image-20250702222356314

控制台不再是红色了,因为输出是字符串

【3】使用traceback.print_exception()控制显示的详细信息:

1
2
3
4
5
import  traceback
try:
    res = 10/0
except Exception:
    traceback.print_exception()

image-20250702222521659

4、PyCharm的调试模式

image-20250702232407461

断点:程序运行到此处,暂时挂起,停止执行。此时可以详细观察程序的运行情况,方便做出进一步的判断

进入调试视图的三种方式

  • (1)单击工具栏上的按钮
  • (2)右键单击编辑区:点击:debug’模块名’
  • (3)快捷键:shift+F9