版权声明:本文为CSDN博主「一笑照夜」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/erwugumo/article/details/96146119
1、为什么需要异常处理
先看一下我们在第十章之后写完的代码:
from flask import Flask, render_template,request,redirect,escape,session
from vsearch import search4letters
from DBcm import UseDatabase
from checker import check_logged_in
app=Flask(__name__)
app.secret_key='YouWillNeverGuess'
#无法连接到数据库怎么办?
app.config['dbconfig']={'host':'127.0.0.1',
'user':'vsearch',
'password':'vsearchpasswd',
'database':'vsearchlogDB',}
def log_request(req:'flask_request',res:str)->None:
with UseDatabase(app.config['dbconfig']) as cursor:
#这里能够防范SQL攻击吗?
_INSERT="""insert into log
(phrase,letters,ip,browser_string,results)
values
(%s,%s,%s,%s,%s)"""
#在执行SQL代码的时候卡死怎么办?
cursor.execute(_INSERT,(req.form['phrase'],
req.form['letters'],
req.remote_addr,
req.user_agent.browser,
res,))
@app.route('/search4',methods=['POST'])
def do_search() -> 'html':
phrase=request.form['phrase']
letters=request.form['letters']
results=str(search4letters(phrase,letters))
#函数调用失败怎么办?
log_request(request,results)
return render_template('results.html',
the_title='Here are your results',
the_phrase=phrase,
the_letters=letters,
the_results=results)
@app.route('/')
@app.route('/entry')
def entry_page() -> 'html':
return render_template('entry.html',
the_title='Welcome to search4letters on the web!')
@app.route('/viewlog')
@check_logged_in
def view_the_log()->str:
with UseDatabase(app.config['dbconfig']) as cursor:
#这里能够防范SQL攻击吗?
_SELECT="""select phrase,letters,ip,browser_string,results from log"""
cursor.execute(_SELECT)
#在执行SQL代码的时候卡死怎么办?
contents=cursor.fetchall()
titles=('Phrase','Letters','Remote_addr','User_agent','Results')
return render_template('viewlog.html',
the_title='View Log',
the_row_titles=titles,
the_data=contents,)
@app.route('/login')
def do_login()->str:
session['logged_in']=True
return 'You are now logged in'
@app.route('/logout')
def do_logout()->str:
session.pop('logged_in')
return 'You are now logged out.'
@app.route('/status')
def check_status()->str:
if 'logged_in' in session:
return 'You are currently logged in.'
return 'You are NOT logged in.'
if __name__=='__main__':
from werkzeug.contrib.fixers import ProxyFix
app.wsgi_app=ProxyFix(app.wsgi_app)
app.run()
在注释中我提出了以下几个问题:
无法连接到SQL数据库怎么办?遭受SQL注入攻击怎么办?处理时间过长怎么办?函数调用出错怎么办?
这些都属于异常,我们应对这些异常的出现事先做好准备。如何做好准备呢?一般是给出通知,记录下错误的类型,出现时间等,便于定位错误甚至复现。
下面分别分析这四个异常:
①数据库连接失败:
我们在后台关闭SQL服务,就会出现如下的InterfaceError错误。
很常见,只要你的代码依赖的外部资源不可用,就会出现错误。出现这种情况时,解释器会报错“InterfaceError”,可以使用python的内置异常处理机制发现这个错误并做出反应。
②数据库受到攻击
暂时不用考虑这点,python的DB-API已经有了对常见攻击的防范。他们做的比我们好得多。
③代码运行时间过长
这其实不是异常,只是代码优化问题或者单纯因为服务器太差了。但是用户可能认为是网站已经崩溃,为了让用户知道网站不是崩溃而是在很努力的处理用户的请求,我们需要一些措施。
④函数调用出错
这是我自己的问题了,太菜导致代码本身有问题,解释器会给出错误提示,我们只需要记录这个错误即可,本质上与第一个问题是相同的。
比如说常见的RuntimeError错误等。
2、开始异常处理——保护log_request函数
由于问题①和问题④的特点类似,我们就先从这俩入手。
python实际上是有一组非常丰富的内置异常类型的,它涵盖了许多我们使用python时可能出现的错误。我们看到的这些所有异常都属于一个名为exception的类,这些异常按层次结构组织。
如果一个错误不存在于内置异常该怎么办?这就需要我们定制异常。第一种错误报的错“InterfaceError”就是mysql.connector的一个定制异常。
怎么发现一个异常呢?需要使用Python的try语句,学过java应该会好理解一些,Java中也有类似的try-catch语句。运行时如果出现问题,try可以帮助你处理异常。来看下面几行有问题的代码:
with open('myfile.txt') as fh:
file_data=fh.read()
print(file_data)
看上去好像没什么问题,然而,如果你所在的用户组没有读取权限,或者myfile这个文件现在还不存在,那么就会产生错误。运行一下试试看。
嗯,报错FileNotFoundError。python很懂啊,看来这是个常见的异常,总会有人蠢蠢的在建文件之前就去读取文件,以至于python的内置异常中有了这么一条。
出现运行时错误时,就会产生一个异常,如果我们忽略这个异常,就称为这个异常未捕获,解释器就会强行终止我们的代码,然后显示一个运行时错误消息,就是上面红的四行。当然我们可以选择用try来捕获这个异常,但是只捕获还不够,还得去进一步说明该养啊该杀啊炖了吃肉还是烧烤什么的。因此在用try捕获之后还要写代码来描述之后干嘛,不然捕获和未捕获没什么两样。
在捕获之后,可以选择:忽略异常(那你捕获它干啥),运行另外一些代码来代替出错的代码,记录出现的异常等,无论选择哪种处理方式,都要使用try。
为了用try保护代码,就要把代码放在try的代码组中。如果产生了一个异常,try代码组中的代码会终止,然后运行except中的代码,在这个except的代码组中定义如何处理。如下:
try:
with open('myfile.txt') as fh:
file_data=fh.read()
print(file_data)
except FileNotFoundError:
print('The data file is missing.')
这时再运行上面代码,发现错误信息发生了改变:
说明try的确捕获到了这个异常,并返回了通知。
那我们新建myfile文件,并设置为只读。对其执行写操作,如下:
try:
with open('myfile.txt','w') as fh:
file_data=fh.read()
print(file_data)
except FileNotFoundError:
print('The data file is missing.')
会报错PermisssionError,如下:
这次try没能捕获这个异常,因为这个异常在except中没有对应的处理方式,既然知道了原因,增加这种异常的处理即可:
try:
with open('myfile.txt','w') as fh:
file_data=fh.read()
print(file_data)
except FileNotFoundError:
print('The data file is missing.')
except PermissionError:
print('This is not allowed.')
再次运行如下:
现在问题来了,我不可能预见到所有的异常,一旦出现未预见到的异常,就会导致未捕获,这对于用户的使用体验影响很大。因此我们还是需要一个能够捕获所有异常的异常处理器,但是只对那些常见的异常有对应的通知,对于不常见的异常,我们均返回相同的通知即可。只需要在最后加两行代码即可:
try:
with open('myfile.txt','w') as fh:
file_data=fh.read()
print(file_data)
except FileNotFoundError:
print('The data file is missing.')
except PermissionError:
print('This is not allowed.')
except:
print('Some other error occured.')
这就类似C中的switch-case一样,不过switch的参数是我们输入的,然后去case找对应;而try-except则是解释器给参数,也在except中找对应。
但是捕获所有异常这种方法有一个缺点:除了FileNotFoundError和PermisssionError以外,我们不知道出了什么错误,因为这两种错误是特殊的,我们可以立刻反应过来,其他的通知都是一样的,所以没法知道。那该怎么办呢?try可以知道发生了什么错误,然后在except中对应,能不能让try先记录下来,然后再对应呢?
可以的。有两种方法:使用sys模块的功能,使用扩展的try/except技术。
sys模块可以用于访问解释器的内部信息,其中有一个函数exc_info,它会提供当前处理的异常的有关信息。调用该函数时,它会返回一个包括三个值的元组,第一个值是异常的类型,第二个字详细描述异常的值,第三个值包含一个回溯跟踪对象,通过该对象可以访问回溯跟踪消息。如果当前没有异常,则会返回三个None。
举例如下:
首先必须import sys模块,不然会报错。
然后在try中写下一个会报异常的代码,这里是除零异常。
最后在except中调用exc_info函数,并打印该元组。
元组元素第一个是异常的类型,可以看出是ZeroDivisionError,即除零错误类;第二个是异常的值;第三个是对象。
虽然我们通过查询回溯跟踪对象可以更深入的了解,但是现在只需要知道异常类型就足够用了,也即是说,现在只需要元组的第一个元素。
也就是说我们只需要储存err[0]就可以咯。实际上更简单,由于这种方式十分常用,python扩展了try-except的功能,让它直接支持这种方法查看异常,也不用import sys模块,也不用自己看,而只需要按如下方式修改代码:
try:
with open('myfile.txt','w') as fh:
file_data=fh.read()
print(file_data)
except FileNotFoundError:
print('The data file is missing.')
except PermissionError:
print('This is not allowed.')
except Exception as err:
print('Some other error occured:',str(err))
也就是说,在遇到其他异常,会把这个异常对象赋给一个变量,一般称为err,然后就可以输出这个变量了。
接下来进入正题:如果我们应用中的log_request函数调用失败怎么办?
当然是把这个函数写进try的代码组啊。如下:
@app.route('/search4',methods=['POST'])
def do_search() -> 'html':
phrase=request.form['phrase']
letters=request.form['letters']
results=str(search4letters(phrase,letters))
try:
log_request(request,results)
except Exception as err:
print('******Logging failed with this error:',str(err))
return render_template('results.html',
the_title='Here are your results',
the_phrase=phrase,
the_letters=letters,
the_results=results)
注意不要把return部分写进代码组。
在修改之后,即使log_request函数调用失败,也不会阻碍网页上显示结果,而只会在日志记录上失败。极大提高了用户的使用体验,用户根本不会知道你的网页有过问题,而这个报错信息也会被隐藏在后台。因为print输出在后台,而不是在网页:
因此,即使代码出错也不会导致整个应用的崩溃,提高了应用的鲁棒性。
3、进阶——保护view_the_log函数
上文我们保护了log_request函数,很简单,只需要把该函数的调用部分卸载try的代码组里就可以。接下来看view_the_log函数。
@app.route('/viewlog')
@check_logged_in
def view_the_log()->str:
#contents=[]
with UseDatabase(app.config['dbconfig']) as cursor:
_SELECT="""select phrase,letters,ip,browser_string,results from log"""
cursor.execute(_SELECT)
contents=cursor.fetchall()
titles=('Phrase','Letters','Remote_addr','User_agent','Results')
return render_template('viewlog.html',
the_title='View Log',
the_row_titles=titles,
the_data=contents,)
这个函数用于查看日志,它并不是我们自己调用的,也就是说我们没法写一个try把它放在里面。因为它与一个url直接相连,真正调用它的地方在flask内部。那该怎么保护它呢?
如果没法保护它的调用,至少要保护它的代码。就是这样。
代码会出哪些问题?比如说后端数据库不可用,比如说可能无法登陆,比如说查询失败等等等等。
我们当然可以把函数的代码全放在try的代码组中,在return下面再写一个except,但是这样做不太好。比如说如果我想针对数据库不可用这一异常做出特定的反映,这种捕获所有异常的方法显然无法实现这个功能。
那好办,为这个异常定制一个返回不就可以了。
当然,像下面这样:
@app.route('/viewlog')
@check_logged_in
def view_the_log()->str:
#contents=[]
try:
with UseDatabase(app.config['dbconfig']) as cursor:
_SELECT="""select phrase,letters,ip,browser_string,results from log"""
cursor.execute(_SELECT)
contents=cursor.fetchall()
titles=('Phrase','Letters','Remote_addr','User_agent','Results')
return render_template('viewlog.html',
the_title='View Log',
the_row_titles=titles,
the_data=contents,)
except mysql.connector.errors.InterfaceError as err:
print('Is your database switched on? Error:',str(err))
except Exception as err:
print('Something went srong:',str(err))
注意,这里定制异常的时候,不能直接写InterfaceError,因为这个异常的定义在connector中,而不像之前文件权限异常等是默认异常。因此需要import mysql.connector模块来识别出这个异常。
现在就能够给出特定的异常通知了。
但是这并不好。为什么?我们更改的代码和mysql这个数据库耦合的太紧了。这可能有点难理解。通俗一点来说就是我们现在的代码和mysql纠缠太深,如果我们想更换别的数据库,需要改的地方太多了,不能很快的改过去。这对于主程序来说是一个很大的缺点。
如何改进呢?
在DBcm接口中使用紧耦合的代码,并提供一个接口,主程序通过这个接口就能实现和import mysql.connector一样的功能,若是想更改数据库,根本不用改主程序,因为它使用的是DBcm的接口,只需要改DBcm的代码就可以了。这样就实现了主程序和数据库的解耦。
之前我们在写DBcm的代码时,目的是写一个上下文管理器,它的exit函数有四个参数,后三个参数就是用来做这个的。现在终于可以用上了:exc_type、exc_value、exc_trace,正好对应元组中的三个元素。我们来看原来的DBcm代码:
import mysql.connector
class UseDatabase:
def __init__(self,dbconfig:dict)->None:
self.dbconfig=dbconfig
def __enter__(self)->'cursor':
self.conn=mysql.connector.connect(**self.dbconfig)
self.cursor=self.conn.cursor()
return self.cursor
def __exit__(self,exc_type,exc_value,exc_trace)->None:
self.conn.commit()
self.cursor.close()
self.conn.close()
如果出问题,会有什么后果呢?
如果enter出问题,那with会直接终止,后续的exit处理也会取消。因为enter都出问题了,上下文正确配置好的概率微乎其微,连接可能还没建立,你断开个毛线。
那enter会出什么问题呢?最大的问题应该是后端数据库不可用,连接建立失败,要针对这个生成一个定制异常。
如何创建一个定制异常?
首先重申一下定制异常是什么。定制异常是python的Exception类中没有的异常,因为没有这种异常,因此需要我们自己写,也就是定制。一般是针对一种情况,给他起个别名,这就是定制异常。InterfaceError就是一个定制异常。然而为了脱耦,我们要把这个定制异常写成我们自己的定制异常,从而让主程序捕获我们的异常,当数据库改变时,就直接改我们的定制异常就可以,主程序捕获的不变,这就是脱耦的原理。
定制异常也是异常,因此它要继承Exception这个类。下面做一个简单的实验:
class ConnectionError(Exception):
pass
我们定义了一个名为ConnectionError的异常,它继承了Exception的类,继承某个类A只需要定义的时候在类名后加上(A)即可。
这是一个空类,但并不代表它什么都做不了,至少它具有Exception类的所有功能,因此看上去就好像是在Exception类中新加了一个成员一样。
如何引发这个异常呢?他是个空类,也没告诉我什么时候可能会出现这个异常啊。
使用raise产生这个异常。如下:
会产生一个回溯跟踪消息,表明产生了一个异常。
也可以使用try-except来捕获这个异常,如下:
可以看到,try成功捕获了这个异常。
还可以看到这一点:即用raise产生一个异常时,异常名后面括号里的字符串实际上就是异常的类型,可以调整这里更改err输出的内容。
接下来我们要修改DBcm的代码,定制一个自己的异常,用于反映数据库连接失败。如下:
import mysql.connector
class ConnectionError(Exception):
pass
class UseDatabase:
def __init__(self,dbconfig:dict)->None:
self.dbconfig=dbconfig
def __enter__(self)->'cursor':
try:
self.conn=mysql.connector.connect(**self.dbconfig)
self.cursor=self.conn.cursor()
return self.cursor
except mysql.connector.errors.InterfaceError as err:
raise ConnectionError(err)
def __exit__(self,exc_type,exc_value,exc_trace)->None:
self.conn.commit()
self.cursor.close()
self.conn.close()
套路是一样的,首先定义一个新类,然后当enter运行时,把内部代码放在try的代码组内,注意三句都要放进去,而不是只放建立连接那句。因为只放那一句的话,如果连接建立失败,虽然会捕获异常,但是剩下两句还是会继续运行,还是会报错导致应用崩溃。如果三句都放进去,第一句出错的话,后面两句直接不会运行。
然后用except捕获因无法连接数据库而产生的异常mysql.connector.errors.InterfaceError,将它的类型储存在err中,然后产生我们自己的异常ConnectionError,我们的异常的类型就是err。
简而言之,这是一个接力:连接数据库出错→mysql.connector产生异常InterfaceError→捕获该异常→产生异常ConnectionError。
接下来修改view_the_log函数如下:
from DBcm import UseDatabase,ConnectionError
@app.route('/viewlog')
@check_logged_in
def view_the_log()->str:
#contents=[]
try:
with UseDatabase(app.config['dbconfig']) as cursor:
_SELECT="""select phrase,letters,ip,browser_string,results from log"""
cursor.execute(_SELECT)
contents=cursor.fetchall()
titles=('Phrase','Letters','Remote_addr','User_agent','Results')
return render_template('viewlog.html',
the_title='View Log',
the_row_titles=titles,
the_data=contents,)
except ConnectionError as err:
print('Is your database switched on? Error:',str(err))
except Exception as err:
print('Something went wrong:',str(err))
return 'Error'
注意import 我们的异常类。
在最后return一个字符串。
另外,由于我不知道如何出现“无法找到后端数据库”的错误,因此我选择关闭mysql服务,它的异常名为mysql.connector.errors.DatabaseError,针对这个异常,只需要在DBcm中加入多一行except,如下:
import mysql.connector
class ConnectionError(Exception):
pass
class UseDatabase:
def __init__(self,dbconfig:dict)->None:
self.dbconfig=dbconfig
def __enter__(self)->'cursor':
try:
self.conn=mysql.connector.connect(**self.dbconfig)
self.cursor=self.conn.cursor()
return self.cursor
except mysql.connector.errors.InterfaceError as err:
raise ConnectionError(err)
except mysql.connector.errors.DatabaseError as err:
raise ConnectionError(err)
def __exit__(self,exc_type,exc_value,exc_trace)->None:
self.conn.commit()
self.cursor.close()
self.conn.close()
十分方便。效果如下:
接下来考虑下一个问题:
enter函数中出现异常,我们在函数内部捕获,那try代码组出现异常怎么办?总不能到代码组去捕获吧?
为什么不能呢?
因为我们这个代码组是运行SQL代码的,如果在代码组捕获,又需要import SQL了,再次紧耦合,因此不能在代码组捕获。这时候exit的后三个参数就派上用场了:若是try的代码组出现异常,会将这一异常的三元素传入exit的后三个参数中,在exit中可以对代码组中的异常进行处理。
接下来扩展两个定制异常:
CredentialsError:当enter方法中出现ProgrammingError错误时产生这个异常。
SQLError:当exit方法中出现ProgrammingError错误时产生这个异常。
ProgrammingError异常一般出现在访问数据库的凭据错误(字典中的密码错了什么的)或者是SQL语句出现语法错误时出现。这也是为什么enter函数中出现这个异常叫CredentialsError,因为enter函数需要用到凭据;而exit函数会接收try代码组中的错误,代码组中有SQL语句。
第一个定制异常很简单,原理和之前的一样,因此不用强调。然而第二个定制异常有一些问题。
第二个定制异常与代码组中的异常有关,代码组中的异常类型将传入exc_type,因此要在exit中判断exc_type是否是ProgrammingError。在哪里判断呢?一定要在exit的最后判断,也就是exit把自己当工作都做完了再判断。因为若是判断成功,则会引起一个异常,那其余代码就不会继续运行,对于我们的代码,连接就不会断开了,这是不可取的。另外,如果出现其他异常,可以在判断完ProgrammingError之后再进行其他判断。代码如下:
import mysql.connector
class ConnectionError(Exception):
pass
class CredentialsError(Exception):
pass
class SQLError(Exception):
pass
class UseDatabase:
def __init__(self,dbconfig:dict)->None:
self.dbconfig=dbconfig
def __enter__(self)->'cursor':
try:
self.conn=mysql.connector.connect(**self.dbconfig)
self.cursor=self.conn.cursor()
return self.cursor
except mysql.connector.errors.InterfaceError as err:
raise ConnectionError(err)
except mysql.connector.errors.DatabaseError as err:
raise ConnectionError(err)
except mysql.connector.errors.ProgrammingError as err:
raise CredentialsError(err)
def __exit__(self,exc_type,exc_value,exc_trace)->None:
self.conn.commit()
self.cursor.close()
self.conn.close()
if exc_type is mysql.connector.errors.ProgrammingError:
raise SQLError(exc_value)
elif exc_type:
raise exc_type(exc_value)
@app.route('/viewlog')
@check_logged_in
def view_the_log()->str:
#contents=[]
try:
with UseDatabase(app.config['dbconfig']) as cursor:
_SELECT="""select phrase,letters,ip,browser_string,results from log"""
cursor.execute(_SELECT)
contents=cursor.fetchall()
titles=('Phrase','Letters','Remote_addr','User_agent','Results')
return render_template('viewlog.html',
the_title='View Log',
the_row_titles=titles,
the_data=contents,)
except ConnectionError as err:
print('Is your database or your mysql service switched on? Error:',str(err))
except CredentialsError as err:
print('User-id/Password issues.Error:',str(err))
except SQLError as err:
print('Is your query correct?Error:',str(err))
except Exception as err:
print('Something went wrong:',str(err))
return 'Error'
在这里,if的判断使用了is,而我自己改成==也能够正常运行,is和==的区别参考该网址。
效果如下:
密码错误时:
SQL错误时:
现在,只剩下那些“需要长时间等待”的问题等待我们处理了。
这留到下一章。