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】粗心导致的语法错误:
(1)是age在input时是str类型,而进入if比较时应该转成int类型。
(2)首先i没有初始化,第二,(i)
用的是中文的括号,第三,i在执行体中没有增量
(3)问题是=
是赋值,==
才是判断相等与否
以上错误可以大致归纳为:
1.漏了末尾的冒号,如if语句,循环语句,else子句等
2.缩进错误,该缩进的没缩进,不该缩进的瞎缩进
3.把英文符号写成中文符号,比如说:引号,冒号,括号
4.字符串拼接的时候,把字符串和数字拼在一起
5.没有定义变量,比如说while的循环条件的变量
6.“==
”比较运算符和”=
”赋值运算符的混用
【2】知识不熟练导致的错误
(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出演了哪部电影。
已有代码如下:
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'])
查询出现报错:
原因是得到的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'])
第一层for循环遍历列表可以得到每一部电影,而每一部电影又是一个字典,只需要根据key在字典中取值即可。根据演员的键actors取出学员的列表,使用判断name在列表中是否存在,最后根据电影名称的键title取出电影的名称,进行输出
【4】被动掉坑:程序代码逻辑没有错,只是因为用户错误操作或者一些“例外情况”而导致的程序崩溃
Python提供了异常处理机制,可以在异常出现时即时捕获,然后内部“消化”,让程序继续运行
例如:输入两个整数并进行除法运算
加入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)异常层级结构
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)
(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")
(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)
(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)
越是具体的异样,越先捕获。
3、异常处理机制
Python的异常处理机制由 try, except, else, finally 块组成,提供了一个结构化的方式来管理程序中的错误,确保资源的正确释放和代码的健壮性。
(1)完整的异常处理结构
完整的异常处理结构包括 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")
(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}")
(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}")
(6)traceback模块
【1】使用traceback.print_exc()
打印当前的异常堆栈跟踪:
1
2
3
4
5
import traceback
try:
res = 10/0
except Exception:
traceback.print_exc()
【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)
控制台不再是红色了,因为输出是字符串
【3】使用traceback.print_exception()
控制显示的详细信息:
1
2
3
4
5
import traceback
try:
res = 10/0
except Exception:
traceback.print_exception()
4、PyCharm的调试模式
断点:程序运行到此处,暂时挂起,停止执行。此时可以详细观察程序的运行情况,方便做出进一步的判断
进入调试视图的三种方式
- (1)单击工具栏上的按钮
- (2)右键单击编辑区:点击:
debug’模块名’
- (3)快捷键:
shift+F9