在程序运行过程中,总会遇到各种各样的错误。
有的错误是程序编写有问题造成的,比如本来应该输出整数结果输出了字符串,这种错误我们通常称之为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:按照规定去触发指定的异常,每种异常都具备被特殊的含义。
有一些任务,可能事先需要设置,事后做清理工作。对于这种场景,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)
运行
分析运行过程:
- 1. __enter__()方法先被执行
- 2. __enter__()方法返回的值 - 这个例子中是"Foo",赋值给变量'sample'
- 3. 执行with中的代码块,打印变量"sample",其值当前为 "Foo"
- 4. 最后__exit__()方法被调用