【跟着Head First学python】11、异常处理:出问题了怎么办

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错误时:

现在,只剩下那些“需要长时间等待”的问题等待我们处理了。

这留到下一章。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值