一、什么是程序异常
在程序运行过程中,总会遇到各种各样的错误。
有的错误是程序编写有问题造成的,比如本来应该输出整数结果输出了字符串,这种错误我们通常称之为bug,bug是必须修复的。
有的错误是用户输入造成的,比如让用户输入email地址,结果得到一个空字符串,这种错误可以通过检查用户输入来做相应的处理。
还有一类错误是完全无法在程序运行过程中预测的,比如写入文件的时候,磁盘满了,写不进去了,或者从网络抓取数据,网络突然断掉了。这类错误也称为异常,在程序中通常是必须处理的,否则,程序会因为各种问题终止并退出。
比如我们执行下面的程序
if __name__ == '__main__': a = 10/0 print(a)
执行结果
可以看到执行没有成功而是发生了问题,这就是程序异常
二、异常处理机制
高级语言通常都内置了一套try...except...finally...
的错误处理机制,Python也不例外。
比如下面的程序
try: print('try...') r = 10 / 0 print('result:', r) except ZeroDivisionError as e: print('except:', e) finally: print('finally...') print('END')
当我们认为某些代码可能会出错时,就可以用try
来运行这段代码,如果执行出错,则后续代码不会继续执行,而是直接跳转至错误处理代码,即except
语句块,执行完except
后,如果有finally
语句块,则执行finally
语句块,至此,执行完毕。
三、异常格式
异常处理格式1:
try: # 逻辑代码 except Exception as e: # try中的代码如果有异常,则此代码块中的代码会执行。
异常处理格式2:
try: # 逻辑代码 except Exception as e: # try中的代码如果有异常,则此代码块中的代码会执行。 finally: # try中的代码无论是否报错,finally中的代码都会执行,一般用于释放资源。 print("end")
特殊的finally
当在函数或方法中定义异常处理的代码时,要特别注意finally和return。
def func(): try: return 123 except Exception as e: pass finally: print(666) func()
在try或except中即使定义了return,也会执行最后的finally块中的代码。
四、异常细分
如果错误有很多种类,发生了不同类型的错误,之前只是简单的捕获了异常,出现异常则统一提示信息即可。如果想要对异常进行更加细致的异常处理,应该由不同的except
语句块处理:
try: print('try...') r = 10 / int('a') print('result:', r) except ValueError as e: print('ValueError:', e) except ZeroDivisionError as e: print('ZeroDivisionError:', e) finally: print('finally...') print('END')
int()
函数可能会抛出ValueError
,所以我们用一个except
捕获ValueError
,用另一个except
捕获ZeroDivisionError
。
此外,如果没有错误发生,可以在except
语句块后面加一个else
,当没有错误发生时,会自动执行else
语句:
try: print('try...') r = 10 / int('2') print('result:', r) except ValueError as e: print('ValueError:', e) except ZeroDivisionError as e: print('ZeroDivisionError:', e) else: print('no error!') finally: print('finally...') print('END')
如果想要对错误进行细分的处理,例如:发生Key错误和发生Value错误分开处理。
基本格式:
try: # 逻辑代码 pass except KeyError as e: # 小兵,只捕获try代码中发现了键不存在的异常,例如:去字典 info_dict["n1"] 中获取数据时,键不存在。 print("KeyError") except ValueError as e: # 小兵,只捕获try代码中发现了值相关错误,例如:把字符串转整型 int("无诶器") print("ValueError") except Exception as e: # 王者,处理上面except捕获不了的错误(可以捕获所有的错误)。 print("Exception")
Python中内置了很多细分的错误,供你选择。
常见异常: """ AttributeError 试图访问一个对象没有的树形,比如foo.x,但是foo没有属性x IOError 输入/输出异常;基本上是无法打开文件 ImportError 无法引入模块或包;基本上是路径问题或名称错误 IndentationError 语法错误(的子类) ;代码没有正确对齐 IndexError 下标索引超出序列边界,比如当x只有三个元素,却试图访问n x[5] KeyError 试图访问字典里不存在的键 inf['xx'] KeyboardInterrupt Ctrl+C被按下 NameError 使用一个还未被赋予对象的变量 SyntaxError Python代码非法,代码不能编译(个人认为这是语法错误,写错了) TypeError 传入对象类型与要求的不符合 UnboundLocalError 试图访问一个还未被设置的局部变量,基本上是由于另有一个同名的全局变量, 导致你以为正在访问它 ValueError 传入一个调用者不期望的值,即使值的类型是正确的 """ 更多异常: """ ArithmeticError AssertionError AttributeError BaseException BufferError BytesWarning DeprecationWarning EnvironmentError EOFError Exception FloatingPointError FutureWarning GeneratorExit ImportError ImportWarning IndentationError IndexError IOError KeyboardInterrupt KeyError LookupError MemoryError NameError NotImplementedError OSError OverflowError PendingDeprecationWarning ReferenceError RuntimeError RuntimeWarning StandardError StopIteration SyntaxError SyntaxWarning SystemError SystemExit TabError TypeError UnboundLocalError UnicodeDecodeError UnicodeEncodeError UnicodeError UnicodeTranslateError UnicodeWarning UserWarning ValueError Warning ZeroDivisionError """
Python的错误其实也是class,所有的错误类型都继承自BaseException
,所以在使用except
时需要注意的是,它不但捕获该类型的错误,还把其子类也“一网打尽”。比如:
try: foo() except ValueError as e: print('ValueError') except UnicodeError as e: print('UnicodeError')
第二个except
永远也捕获不到UnicodeError
,因为UnicodeError
是ValueError
的子类,如果有,也被第一个except
给捕获了。
Python所有的错误都是从BaseException
类派生的,常见的错误类型和继承关系
BaseException ├── BaseExceptionGroup ├── GeneratorExit ├── KeyboardInterrupt ├── SystemExit └── Exception ├── ArithmeticError │ ├── FloatingPointError │ ├── OverflowError │ └── ZeroDivisionError ├── AssertionError ├── AttributeError ├── BufferError ├── EOFError ├── ExceptionGroup [BaseExceptionGroup] ├── ImportError │ └── ModuleNotFoundError ├── LookupError │ ├── IndexError │ └── KeyError ├── MemoryError ├── NameError │ └── UnboundLocalError ├── OSError │ ├── BlockingIOError │ ├── ChildProcessError │ ├── ConnectionError │ │ ├── BrokenPipeError │ │ ├── ConnectionAbortedError │ │ ├── ConnectionRefusedError │ │ └── ConnectionResetError │ ├── FileExistsError │ ├── FileNotFoundError │ ├── InterruptedError │ ├── IsADirectoryError │ ├── NotADirectoryError │ ├── PermissionError │ ├── ProcessLookupError │ └── TimeoutError ├── ReferenceError ├── RuntimeError │ ├── NotImplementedError │ └── RecursionError ├── StopAsyncIteration ├── StopIteration ├── SyntaxError │ └── IndentationError │ └── TabError ├── SystemError ├── TypeError ├── ValueError │ └── UnicodeError │ ├── UnicodeDecodeError │ ├── UnicodeEncodeError │ └── UnicodeTranslateError └── Warning ├── BytesWarning ├── DeprecationWarning ├── EncodingWarning ├── FutureWarning ├── ImportWarning ├── PendingDeprecationWarning ├── ResourceWarning ├── RuntimeWarning ├── SyntaxWarning ├── UnicodeWarning └── UserWarning
使用try...except
捕获错误还有一个巨大的好处,就是可以跨越多层调用,比如函数main()
调用bar()
,bar()
调用foo()
,结果foo()
出错了,这时,只要main()
捕获到了,就可以处理:
def foo(s): return 10 / int(s) def bar(s): return foo(s) * 2 def main(): try: bar('0') except Exception as e: print('Error:', e) finally: print('finally...')
也就是说,不需要在每个可能出错的地方去捕获错误,只要在合适的层次去捕获错误就可以了。这样一来,就大大减少了写try...except...finally
的麻烦。
五、调用栈
如果错误没有被捕获,它就会一直往上抛,最后被Python解释器捕获,打印一个错误信息,然后程序退出。来看看err.py
:
# err.py: def foo(s): return 10 / int(s) def bar(s): return foo(s) * 2 def main(): bar('0') main()
执行,结果如下:
$ python3 err.py Traceback (most recent call last): File "err.py", line 11, in <module> main() File "err.py", line 9, in main bar('0') File "err.py", line 6, in bar return foo(s) * 2 File "err.py", line 3, in foo return 10 / int(s) ZeroDivisionError: division by zero
出错并不可怕,可怕的是不知道哪里出错了。解读错误信息是定位错误的关键。我们从上往下可以看到整个错误的调用函数链:
错误信息第1行:
Traceback (most recent call last):
告诉我们这是错误的跟踪信息。
第2~3行:
File "err.py", line 11, in <module> main()
调用main()
出错了,在代码文件err.py
的第11行代码,但原因是第9行:
File "err.py", line 9, in main bar('0')
调用bar('0')
出错了,在代码文件err.py
的第9行代码,但原因是第6行:
File "err.py", line 6, in bar return foo(s) * 2
原因是return foo(s) * 2
这个语句出错了,但这还不是最终原因,继续往下看:
File "err.py", line 3, in foo return 10 / int(s)
原因是return 10 / int(s)
这个语句出错了,这是错误产生的源头,因为下面打印了:
ZeroDivisionError: integer division or modulo by zero
根据错误类型ZeroDivisionError
,我们判断,int(s)
本身并没有出错,但是int(s)
返回0
,在计算10 / 0
时出错,至此,找到错误源头。
六、记录异常
如果不捕获错误,自然可以让Python解释器来打印出错误堆栈,但程序也被结束了。既然我们能捕获错误,就可以把错误堆栈打印出来,然后分析错误原因,同时,让程序继续执行下去。
Python内置的logging
模块可以非常容易地记录错误信息:
# err_logging.py import logging def foo(s): return 10 / int(s) def bar(s): return foo(s) * 2 def main(): try: bar('0') except Exception as e: logging.exception(e) main() print('END')
同样是出错,但程序打印完错误信息后会继续执行,并正常退出:
$ python3 err_logging.py ERROR:root:division by zero Traceback (most recent call last): File "err_logging.py", line 13, in main bar('0') File "err_logging.py", line 9, in bar return foo(s) * 2 File "err_logging.py", line 6, in foo return 10 / int(s) ZeroDivisionError: division by zero END
通过配置,logging
还可以把错误记录到日志文件里,方便事后排查。
七、自定义异常&抛出异常
因为错误是class,捕获一个错误就是捕获到该class的一个实例。因此,错误并不是凭空产生的,而是有意创建并抛出的。Python的内置函数会抛出很多类型的错误,我们自己编写的函数也可以抛出错误。
class MyException(Exception): pass try: pass except MyException as e: print("MyException异常被触发了", e) except Exception as e: print("Exception", e)
上述代码在except中定义了捕获MyException异常,但他永远不会被触发。因为默认的那些异常都有特定的触发条件,例如:索引不存在、键不存在会触发IndexError和KeyError异常。
对于我们自定义的异常,如果想要触发,则需要使用:raise MyException()
类实现。
class MyException(Exception): pass try: # 。。。 raise MyException() # 。。。 except MyException as e: print("MyException异常被触发了", e) except Exception as e: print("Exception", e)
自定义异常类的时候可以加上异常信息属性
class MyException(Exception): def __init__(self, msg, *args, **kwargs): super().__init__(*args, **kwargs) self.msg = msg try: raise MyException("xxx失败了") except MyException as e: print("MyException异常被触发了", e.msg) except Exception as e: print("Exception", e)
或者
class MyException(Exception): title = "请求错误" try: raise MyException() except MyException as e: print("MyException异常被触发了", e.title) except Exception as e: print("Exception", e)
使用案例1:你我合作协同开发,你调用我写的方法。
我定义了一个函数
class EmailValidError(Exception): title = "邮箱格式错误" class ContentRequiredError(Exception): title = "文本不能为空错误" def send_email(email,content): if not re.match("\w+@live.com",email): raise EmailValidError() if len(content) == 0 : raise ContentRequiredError() # 发送邮件代码... # ...
你调用我写的函数
def execute(): # 其他代码 # ... try: send_email(...) except EmailValidError as e: pass except ContentRequiredError as e: pass except Exception as e: print("发送失败") execute() # 提示:如果想要写的简单一点,其实只写一个Exception捕获错误就可以了。
使用案例2:在框架内部已经定义好,遇到什么样的错误都会触发不同的异常。
import requests from requests import exceptions while True: url = input("请输入要下载网页地址:") try: res = requests.get(url=url) print(res) except exceptions.MissingSchema as e: print("URL架构不存在") except exceptions.InvalidSchema as e: print("URL架构错误") except exceptions.InvalidURL as e: print("URL地址格式错误") except exceptions.ConnectionError as e: print("网络连接错误") except Exception as e: print("代码出现错误", e) # 提示:如果想要写的简单一点,其实只写一个Exception捕获错误就可以了。
案例3:按照规定去触发指定的异常,每种异常都具备被特殊的含义。
八、with的用法
With语句是什么?
有一些任务,可能事先需要设置,事后做清理工作。对于这种场景,Python的with语句提供了一种非常方便的处理方式。其中一个很好的例子是文件处理,你需要获取一个文件句柄,从文件中读取数据,然后关闭文件句柄。
如果不用with语句,代码如下:
file = open("/tmp/foo.txt") data = file.read() file.close()
这里有两个问题。一是可能忘记关闭文件句柄;二是文件读取数据发生异常,没有进行任何处理。下面是处理异常的加强版本:
file = open("/tmp/foo.txt") try: data = file.read() finally: file.close()
这段代码运行良好,但是太冗长。这时候with便体现出了优势。 除了有更优雅的语法,with还可以很好的处理上下文环境产生的异常。下面是with版本的代码:
with open("/tmp/foo.txt") as file: data = file.read()
是不是很简单?
但是如果对with工作原理不熟悉的通许可能会和刚才的我一样,不懂其中原理
那么下面我们简单看一下with的工作原理
with是如何工作的?
基本思想是:with所求值的对象必须有一个enter()方法,一个exit()方法。
紧跟with**后面的语句被求值后,返回对象的**__enter__()方法被调用,这个方法的返回值将被赋值给as后面的变量。当with后面的代码块全部被执行完之后,将调用前面返回对象的exit()方法。
下面是一个例子
###################### ########with()########## ###################### class Sample: def __enter__(self): print("in __enter__") return "Foo" def __exit__(self, exc_type, exc_val, exc_tb): #exc_type: 错误的类型 #exc_val: 错误类型对应的值 #exc_tb: 代码中错误发生的位置 print("in __exit__") def get_sample(): return Sample() with get_sample() as sample: print("Sample: " ,sample)
运行
分析运行过程:
- 进入这段程序,首先创建Sample类,完成它的两个成员函数enter ()、exit()的定义,然后顺序向下定义get_sample()函数.
- 进入with语句,调用get_sample()函数,返回一个Sample()类的对象,此时就需要进入Sample()类中,可以看到
1. __enter__()方法先被执行 2. __enter__()方法返回的值 - 这个例子中是"Foo",赋值给变量'sample' 3. 执行with中的代码块,打印变量"sample",其值当前为 "Foo" 4. 最后__exit__()方法被调用