目录
1、前言
2、常见的异常
3、异常处理try...except...finally
4、异常信息解读
5、raise
6、自定义异常
7、小结
1、前言
在编程中,异常(Exception)是程序在运行期间检测到的错误或异常状况。当程序执行过程中发生了一些无法继续执行的错误时,会引发异常,这可能是由于错误的输入、文件不存在、网络连接问题等多种原因引起的。而程序中对于异常的处理,是为了保持良好的程序健壮性,不会因为异常而导致程序终止甚至退出。
2、常见的异常
在Python中,异常是一个类的实例,通常是内置的异常类的子类。当某个异常条件触发时,Python会抛出(raise)一个异常对象,然后程序的控制流将被转移到处理该异常的代码块。异常处理的机制允许程序员在程序中检测并处理错误,以避免程序崩溃。
一般来说,异常包含了3大部分:异常类型、异常信息、异常堆栈。
1)异常类型:异常类型是指异常的分类,它指定了异常的种类。指示了引发异常的具体情况。
try:
result = 10 / 0 # 引发 ZeroDivisionError
except ZeroDivisionError as e:
# 异常类型是 ZeroDivisionError
print(f"Error Type: {type(e).__name__}")
2)异常信息:是一条包含有关异常原因的人类可读的描述。异常消息通常包含错误的详细信息,有助于开发者理解异常的具体原因。
try:
result = int("abc") # 引发 ValueError
except ValueError as e:
# 异常消息是 "invalid literal for int() with base 10: 'abc'"
print(f"Error Message: {str(e)}")
3)异常堆栈:异常堆栈信息包含了异常发生时程序调用栈的状态,它记录了异常的触发点以及导致异常的函数调用链。异常堆栈信息对于调试和定位问题非常有用。
def func1():
result = int("abc") # 引发 ValueError
def func2():
func1()
try:
func2()
except ValueError as e:
# 异常堆栈信息包含了函数调用链
print(f"Exception Traceback: {e.with_traceback(e.__traceback__)}")
而在Python中常见的异常类有:
- ZeroDivisionError:除以零错误
- ValueError:传入一个调用者不期望的值,即使值的类型是正确的
- TypeError:操作或函数的参数类型错误
- FileNotFoundError:文件不存在错误
- IndexError:索引超出序列范围
- KeyError:字典中的键不存在
- NameError:尝试访问未声明的变量
3、异常处理try...except...finally
传统来讲,如果程序在运行过程中发生了异常,可以实现约定好一些错误码,利用错误码来区分各种异常事件,典型的诸如Http状态码。这样根据不同的错误码就可以很清楚的知道是什么错误类型。但是如果错误码很多,那么维护起来就很不方便,而且错误码通常需要和业务代码结合在一起:
def method1():
code = do_something()
if code == 200:
return "ok"
elif code == 404:
return "resource not found"
elif code == 500:
return "server internal error"
......
else:
return "other error"
def do_something():
return 100
因此,Python内置了一套异常处理机制。在 Python 3 中,异常处理是通过使用 try, except, else, 和 finally 等关键字来实现的。异常处理的目的是在程序执行期间检测到错误,并提供一种机制来处理这些错误,防止程序中断或崩溃。以下是异常处理的基本语法:
try:
# 可能引发异常的代码块
result = 10 / 0 # 这里故意引发一个除零错误
except ZeroDivisionError as e:
# 异常处理块
print(f"Error: {e}")
else:
# 如果没有发生异常时执行的代码块
print("No exception occurred.")
finally:
# 无论是否发生异常,都会执行的代码块
print("Finally block executed.")
try 语句包裹了可能引发异常的代码块。如果在 try 语句中发生异常,程序将跳转到匹配的 except 语句块,执行相应的异常处理逻辑。如果没有异常发生,那么会执行 else 语句块中的代码。最后,无论是否发生异常,都会执行 finally 语句块中的代码。
上述代码执行后的结果:
当然这里的except捕获的异常可以有不同类型,如:
def test():
try:
# 可能引发异常的代码块
result = 10 / 0 # 这里故意引发一个除零错误,会抛出ZeroDivisionError
result = 10 / int('a') # 这里故意引发一个字符串转换类型错误,会抛出ValueError
"hello" + b # 这里故意引用一个未被声明的变量,会抛出NameError
except ValueError as ve: # 当发生ValueError时候,被这里的异常捕获
# 异常处理块
print(f"raise a exception : ValueError: {ve}")
except ZeroDivisionError as ze: # 当发生ZeroDivisionError时候,被这里的异常捕获
# 异常处理块
print(f"raise a exception : ZeroDivisionError: {ze}")
except Exception as e: # 当发生的异常上面都没有捕获时,最终会被这层捕获
# 异常处理块
print(f"raise a exception : Exception: {e}")
else:
# 如果没有发生异常时执行的代码块
print("No exception occurred.")
finally:
# 无论是否发生异常,都会执行的代码块
print("Finally block executed.")
test()
需要注意的是,这里的异常是逐层捕获的,越靠经try的except优先级越高。如果第一层except就捕获了Exception,那么接下来的ValueError都是捕获不到的。
def test():
try:
# 可能引发异常的代码块
result = 10 / 0 # 这里故意引发一个除零错误,会抛出ZeroDivisionError
# result = 10 / int('a') # 这里故意引发一个字符串转换类型错误,会抛出ValueError
"hello" + b # 这里故意引用一个未被声明的变量,会抛出NameError
except Exception as ve: # 调换一下顺序,把Exception放在第一层
# 异常处理块
print(f"raise a exception : Exception: {ve}")
except ZeroDivisionError as ze: # 当发生ZeroDivisionError时候,被这里的异常捕获
# 异常处理块
print(f"raise a exception : ZeroDivisionError: {ze}")
except ValueError as e: # 当发生的异常上面都没有捕获时,最终会被这层捕获
# 异常处理块
print(f"raise a exception : ValueError: {e}")
else:
# 如果没有发生异常时执行的代码块
print("No exception occurred.")
finally:
# 无论是否发生异常,都会执行的代码块
print("Finally block executed.")
test()
照理说,10/0会抛出ZeroDivisionError异常类型,但是由于Exception类型比ZeroDivisionError更靠近try,所以优先被Exception捕获。
因为Exception是所有异常类的基类。ValueError或NameError等异常都继承于Exception,因此Exception可以捕获所有属于它自己的子类异常类型。如果不存在继承关系,那么优先级属于平级,就会按照异常类型各自捕获。因此项目中,我们往往会把Exception最为保底的异常捕获类型来处理。
我们点开ValueError源码可以看到继承关系:
此外,使用try...except还有一个好处是,它可以跨层调用。如:
def test():
try:
result = test1() # 这里调用test1方法,test1方法会抛出异常,由上层捕获
except Exception as ve:
# 异常处理块
print(f"raise a exception : Exception: {ve}")
finally:
# 无论是否发生异常,都会执行的代码块
print("Finally block executed.")
def test1():
return 1 / 0
test()
这样,我们就不需要在每个调用方法的地方都进行异常捕获,只要在合适的层(如在统一入口进行捕获)就可以捕获到各个层次间的异常信息。而如果异常没有被捕获,则会一直网上抛,直到被Python解释器捕获,然后程序退出。
4、异常信息解读
上面我们介绍了基本的异常处理的语法。既然出现了异常,那么我们肯定是要进行修复的。那么读懂异常信息就很关键。前面介绍到异常一般分为3个部分,异常类型和异常信息就不说了,通常都很容易看懂。主要我们来看下异常堆栈,这里包含了异常的整个方法调用链,从中我们可以很容易看到具体哪个方法出现了异常。我们先来编写一段代码,模拟下异常:
def do_something():
return 1 / 0
def test():
try:
result = do_something() # 这里调用do_something方法,do_something方法会抛出异常,由上层捕获
except Exception as e:
# 异常处理块,使用with_traceback()打印出异常堆栈信息
print(f"raise a exception : Exception: {e.with_traceback()}")
finally:
# 无论是否发生异常,都会执行的代码块
print("Finally block executed.")
test()
执行结果:
所以可以看到,通过跟踪异常的堆栈信息,可以很容易定位到具体的错误代码。
注:使用e.with_traceback()打印的错误信息,只能在控制终端打印信息,并不能持久化。一般项目中需要把错误信息记录的日志文件中,方便排查。可以引入logging模块,使用logging记录到日志中
5、raise
除了try...except被动的捕获程序异常以外,我们还可以手动的进行抛出已识别的异常信息。这时就要用到raise关键字。通过 raise 关键字,你可以显式地引发异常,并指定异常类型、异常消息等信息。这对于在特定条件下主动引发异常、或在异常发生时进行额外的信息记录非常有用。
基础语法很简单:
raise 异常类("异常信息")
如:
def example_function(value):
if value < 0:
raise ValueError("这里引发一个异常,value值不能<0")
return value
try:
result = example_function(-5)
except ValueError as e:
print(f"捕获到异常: {e}")
example_function 函数中使用 raise 关键字在 value 小于 0 时引发了 ValueError 异常,并提供了异常消息。在异常处理块中,程序捕获了这个异常并进行了处理。raise 语句可以包含一个异常类、一个异常类的实例,或者是一个异常类和一个异常消息:
# 引发指定类型的异常
raise ValueError("This is a custom error message")
# 引发异常实例
custom_exception = ValueError("This is another custom error message")
raise custom_exception
使用 raise 关键字时需要注意,在没有捕获异常的情况下,异常会传递到调用栈的上层,直到被捕获或导致程序终止。因此,要慎重使用 raise,确保异常能够得到适当处理。
6、自定义异常
通常,结合raise使用的需要我们自定义异常类。根据不同的业务场景,定义符合业务场景类型的异常类。编写自定义异常时,需要继承异常的基类(Exception)或其子类,并在构造函数中设置一些自定义属性。如:
def example_function(value):
if value < 0:
raise CustomError(-500, "这里引发一个异常,value值不能<0")
return value
# 自定义异常CustomError,继承Exception
class CustomError(Exception):
# 构造函数,需要提供异常代码,异常信息属性
def __init__(self, code, message):
self.code = code
self.message = message
super().__init__(message)
try:
result = example_function(-5)
except ValueError as e:
print(f"捕获到异常: {e}")
CustomError 类继承自 Exception,并在其构造函数中定义了两个属性 code 和 message。在 example_function 中,当输入值小于 0 时,引发了自定义的异常,并在异常处理块中捕获并输出了异常的属性信息。
自定义异常的主要目的是提供更多的上下文信息,以便在异常发生时更好地理解问题的原因。在实际的应用中,可以根据具体的需求定义不同的自定义异常类,以便更好地组织和处理异常情况。
7、小结
总体来说,异常处理是一种良好的编程实践,有助于确保程序在面对各种异常情况时能够保持可控和可维护。通过适当的异常处理,开发者能够更好地应对意外情况,提高程序的质量和稳定性。