美文网首页Head First Python
《Head First Python》Ch11:异常处理

《Head First Python》Ch11:异常处理

作者: 老A不加V | 来源:发表于2021-03-02 21:44 被阅读0次

    版权声明:本文为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错误。


    image

    很常见,只要你的代码依赖的外部资源不可用,就会出现错误。出现这种情况时,解释器会报错“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这个文件现在还不存在,那么就会产生错误。运行一下试试看。


    image

    嗯,报错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.')
    

    这时再运行上面代码,发现错误信息发生了改变:


    image

    说明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,如下:


    image

    这次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.')
    

    再次运行如下:


    image

    现在问题来了,我不可能预见到所有的异常,一旦出现未预见到的异常,就会导致未捕获,这对于用户的使用体验影响很大。因此我们还是需要一个能够捕获所有异常的异常处理器,但是只对那些常见的异常有对应的通知,对于不常见的异常,我们均返回相同的通知即可。只需要在最后加两行代码即可:

    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。

    举例如下:


    image

    首先必须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输出在后台,而不是在网页:


    image

    因此,即使代码出错也不会导致整个应用的崩溃,提高了应用的鲁棒性。

    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产生这个异常。如下:


    image

    会产生一个回溯跟踪消息,表明产生了一个异常。

    也可以使用try-except来捕获这个异常,如下:


    image

    可以看到,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和==的区别参考该网址。

    效果如下:

    密码错误时:


    image

    SQL错误时:


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

    这留到下一章。

    相关文章

      网友评论

        本文标题:《Head First Python》Ch11:异常处理

        本文链接:https://www.haomeiwen.com/subject/obaxqltx.html