《Head First Python》Ch11:異常處理

版權(quán)聲明:本文為CSDN博主「一笑照夜」的原創(chuàng)文章,遵循CC 4.0 BY-SA版權(quán)協(xié)議,轉(zhuǎn)載請(qǐng)附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/erwugumo/article/details/96146119

1、為什么需要異常處理

先看一下我們?cè)诘谑轮髮懲甑拇a:

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'
 
#無(wú)法連接到數(shù)據(jù)庫(kù)怎么辦?
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)"""
        #在執(zhí)行SQL代碼的時(shí)候卡死怎么辦?
        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))
    #函數(shù)調(diào)用失敗怎么辦?
    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)
        #在執(zhí)行SQL代碼的時(shí)候卡死怎么辦?
        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()

在注釋中我提出了以下幾個(gè)問(wèn)題:

無(wú)法連接到SQL數(shù)據(jù)庫(kù)怎么辦?遭受SQL注入攻擊怎么辦?處理時(shí)間過(guò)長(zhǎng)怎么辦?函數(shù)調(diào)用出錯(cuò)怎么辦?

這些都屬于異常,我們應(yīng)對(duì)這些異常的出現(xiàn)事先做好準(zhǔn)備。如何做好準(zhǔn)備呢?一般是給出通知,記錄下錯(cuò)誤的類型,出現(xiàn)時(shí)間等,便于定位錯(cuò)誤甚至復(fù)現(xiàn)。

下面分別分析這四個(gè)異常:

①數(shù)據(jù)庫(kù)連接失?。?/h4>

我們?cè)诤笈_(tái)關(guān)閉SQL服務(wù),就會(huì)出現(xiàn)如下的InterfaceError錯(cuò)誤。


image

很常見,只要你的代碼依賴的外部資源不可用,就會(huì)出現(xiàn)錯(cuò)誤。出現(xiàn)這種情況時(shí),解釋器會(huì)報(bào)錯(cuò)“InterfaceError”,可以使用python的內(nèi)置異常處理機(jī)制發(fā)現(xiàn)這個(gè)錯(cuò)誤并做出反應(yīng)。

②數(shù)據(jù)庫(kù)受到攻擊

暫時(shí)不用考慮這點(diǎn),python的DB-API已經(jīng)有了對(duì)常見攻擊的防范。他們做的比我們好得多。

③代碼運(yùn)行時(shí)間過(guò)長(zhǎng)

這其實(shí)不是異常,只是代碼優(yōu)化問(wèn)題或者單純因?yàn)榉?wù)器太差了。但是用戶可能認(rèn)為是網(wǎng)站已經(jīng)崩潰,為了讓用戶知道網(wǎng)站不是崩潰而是在很努力的處理用戶的請(qǐng)求,我們需要一些措施。

④函數(shù)調(diào)用出錯(cuò)

這是我自己的問(wèn)題了,太菜導(dǎo)致代碼本身有問(wèn)題,解釋器會(huì)給出錯(cuò)誤提示,我們只需要記錄這個(gè)錯(cuò)誤即可,本質(zhì)上與第一個(gè)問(wèn)題是相同的。

比如說(shuō)常見的RuntimeError錯(cuò)誤等。

2、開始異常處理——保護(hù)log_request函數(shù)

由于問(wèn)題①和問(wèn)題④的特點(diǎn)類似,我們就先從這倆入手。

python實(shí)際上是有一組非常豐富的內(nèi)置異常類型的,它涵蓋了許多我們使用python時(shí)可能出現(xiàn)的錯(cuò)誤。我們看到的這些所有異常都屬于一個(gè)名為exception的類,這些異常按層次結(jié)構(gòu)組織。

如果一個(gè)錯(cuò)誤不存在于內(nèi)置異常該怎么辦?這就需要我們定制異常。第一種錯(cuò)誤報(bào)的錯(cuò)“InterfaceError”就是mysql.connector的一個(gè)定制異常。

怎么發(fā)現(xiàn)一個(gè)異常呢?需要使用Python的try語(yǔ)句,學(xué)過(guò)java應(yīng)該會(huì)好理解一些,Java中也有類似的try-catch語(yǔ)句。運(yùn)行時(shí)如果出現(xiàn)問(wèn)題,try可以幫助你處理異常。來(lái)看下面幾行有問(wèn)題的代碼:

with open('myfile.txt') as fh:
    file_data=fh.read()
print(file_data)

看上去好像沒(méi)什么問(wèn)題,然而,如果你所在的用戶組沒(méi)有讀取權(quán)限,或者myfile這個(gè)文件現(xiàn)在還不存在,那么就會(huì)產(chǎn)生錯(cuò)誤。運(yùn)行一下試試看。


image

嗯,報(bào)錯(cuò)FileNotFoundError。python很懂啊,看來(lái)這是個(gè)常見的異常,總會(huì)有人蠢蠢的在建文件之前就去讀取文件,以至于python的內(nèi)置異常中有了這么一條。

出現(xiàn)運(yùn)行時(shí)錯(cuò)誤時(shí),就會(huì)產(chǎn)生一個(gè)異常,如果我們忽略這個(gè)異常,就稱為這個(gè)異常未捕獲,解釋器就會(huì)強(qiáng)行終止我們的代碼,然后顯示一個(gè)運(yùn)行時(shí)錯(cuò)誤消息,就是上面紅的四行。當(dāng)然我們可以選擇用try來(lái)捕獲這個(gè)異常,但是只捕獲還不夠,還得去進(jìn)一步說(shuō)明該養(yǎng)啊該殺啊燉了吃肉還是燒烤什么的。因此在用try捕獲之后還要寫代碼來(lái)描述之后干嘛,不然捕獲和未捕獲沒(méi)什么兩樣。

在捕獲之后,可以選擇:忽略異常(那你捕獲它干啥),運(yùn)行另外一些代碼來(lái)代替出錯(cuò)的代碼,記錄出現(xiàn)的異常等,無(wú)論選擇哪種處理方式,都要使用try。

為了用try保護(hù)代碼,就要把代碼放在try的代碼組中。如果產(chǎn)生了一個(gè)異常,try代碼組中的代碼會(huì)終止,然后運(yùn)行except中的代碼,在這個(gè)except的代碼組中定義如何處理。如下:

try:
    with open('myfile.txt') as fh:
        file_data=fh.read()
    print(file_data)
except FileNotFoundError:
    print('The data file is missing.')

這時(shí)再運(yùn)行上面代碼,發(fā)現(xiàn)錯(cuò)誤信息發(fā)生了改變:


image

說(shuō)明try的確捕獲到了這個(gè)異常,并返回了通知。

那我們新建myfile文件,并設(shè)置為只讀。對(duì)其執(zhí)行寫操作,如下:

try:
    with open('myfile.txt','w') as fh:
        file_data=fh.read()
    print(file_data)
except FileNotFoundError:
    print('The data file is missing.')

會(huì)報(bào)錯(cuò)PermisssionError,如下:


image

這次try沒(méi)能捕獲這個(gè)異常,因?yàn)檫@個(gè)異常在except中沒(méi)有對(duì)應(yīng)的處理方式,既然知道了原因,增加這種異常的處理即可:

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

再次運(yùn)行如下:


image

現(xiàn)在問(wèn)題來(lái)了,我不可能預(yù)見到所有的異常,一旦出現(xiàn)未預(yù)見到的異常,就會(huì)導(dǎo)致未捕獲,這對(duì)于用戶的使用體驗(yàn)影響很大。因此我們還是需要一個(gè)能夠捕獲所有異常的異常處理器,但是只對(duì)那些常見的異常有對(duì)應(yīng)的通知,對(duì)于不常見的異常,我們均返回相同的通知即可。只需要在最后加兩行代碼即可:

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一樣,不過(guò)switch的參數(shù)是我們輸入的,然后去case找對(duì)應(yīng);而try-except則是解釋器給參數(shù),也在except中找對(duì)應(yīng)。

但是捕獲所有異常這種方法有一個(gè)缺點(diǎn):除了FileNotFoundError和PermisssionError以外,我們不知道出了什么錯(cuò)誤,因?yàn)檫@兩種錯(cuò)誤是特殊的,我們可以立刻反應(yīng)過(guò)來(lái),其他的通知都是一樣的,所以沒(méi)法知道。那該怎么辦呢?try可以知道發(fā)生了什么錯(cuò)誤,然后在except中對(duì)應(yīng),能不能讓try先記錄下來(lái),然后再對(duì)應(yīng)呢?

可以的。有兩種方法:使用sys模塊的功能,使用擴(kuò)展的try/except技術(shù)。

sys模塊可以用于訪問(wèn)解釋器的內(nèi)部信息,其中有一個(gè)函數(shù)exc_info,它會(huì)提供當(dāng)前處理的異常的有關(guān)信息。調(diào)用該函數(shù)時(shí),它會(huì)返回一個(gè)包括三個(gè)值的元組,第一個(gè)值是異常的類型,第二個(gè)字詳細(xì)描述異常的值,第三個(gè)值包含一個(gè)回溯跟蹤對(duì)象,通過(guò)該對(duì)象可以訪問(wèn)回溯跟蹤消息。如果當(dāng)前沒(méi)有異常,則會(huì)返回三個(gè)None。

舉例如下:


image

首先必須import sys模塊,不然會(huì)報(bào)錯(cuò)。

然后在try中寫下一個(gè)會(huì)報(bào)異常的代碼,這里是除零異常。

最后在except中調(diào)用exc_info函數(shù),并打印該元組。

元組元素第一個(gè)是異常的類型,可以看出是ZeroDivisionError,即除零錯(cuò)誤類;第二個(gè)是異常的值;第三個(gè)是對(duì)象。

雖然我們通過(guò)查詢回溯跟蹤對(duì)象可以更深入的了解,但是現(xiàn)在只需要知道異常類型就足夠用了,也即是說(shuō),現(xiàn)在只需要元組的第一個(gè)元素。

也就是說(shuō)我們只需要儲(chǔ)存err[0]就可以咯。實(shí)際上更簡(jiǎn)單,由于這種方式十分常用,python擴(kuò)展了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))

也就是說(shuō),在遇到其他異常,會(huì)把這個(gè)異常對(duì)象賦給一個(gè)變量,一般稱為err,然后就可以輸出這個(gè)變量了。

接下來(lái)進(jìn)入正題:如果我們應(yīng)用中的log_request函數(shù)調(diào)用失敗怎么辦?

當(dāng)然是把這個(gè)函數(shù)寫進(jìn)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部分寫進(jìn)代碼組。

在修改之后,即使log_request函數(shù)調(diào)用失敗,也不會(huì)阻礙網(wǎng)頁(yè)上顯示結(jié)果,而只會(huì)在日志記錄上失敗。極大提高了用戶的使用體驗(yàn),用戶根本不會(huì)知道你的網(wǎng)頁(yè)有過(guò)問(wèn)題,而這個(gè)報(bào)錯(cuò)信息也會(huì)被隱藏在后臺(tái)。因?yàn)閜rint輸出在后臺(tái),而不是在網(wǎng)頁(yè):


image

因此,即使代碼出錯(cuò)也不會(huì)導(dǎo)致整個(gè)應(yīng)用的崩潰,提高了應(yīng)用的魯棒性。

3、進(jìn)階——保護(hù)view_the_log函數(shù)

上文我們保護(hù)了log_request函數(shù),很簡(jiǎn)單,只需要把該函數(shù)的調(diào)用部分卸載try的代碼組里就可以。接下來(lái)看view_the_log函數(shù)。

@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,)  

這個(gè)函數(shù)用于查看日志,它并不是我們自己調(diào)用的,也就是說(shuō)我們沒(méi)法寫一個(gè)try把它放在里面。因?yàn)樗c一個(gè)url直接相連,真正調(diào)用它的地方在flask內(nèi)部。那該怎么保護(hù)它呢?

如果沒(méi)法保護(hù)它的調(diào)用,至少要保護(hù)它的代碼。就是這樣。

代碼會(huì)出哪些問(wèn)題?比如說(shuō)后端數(shù)據(jù)庫(kù)不可用,比如說(shuō)可能無(wú)法登陸,比如說(shuō)查詢失敗等等等等。

我們當(dāng)然可以把函數(shù)的代碼全放在try的代碼組中,在return下面再寫一個(gè)except,但是這樣做不太好。比如說(shuō)如果我想針對(duì)數(shù)據(jù)庫(kù)不可用這一異常做出特定的反映,這種捕獲所有異常的方法顯然無(wú)法實(shí)現(xiàn)這個(gè)功能。

那好辦,為這個(gè)異常定制一個(gè)返回不就可以了。

當(dāng)然,像下面這樣:

@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))

注意,這里定制異常的時(shí)候,不能直接寫InterfaceError,因?yàn)檫@個(gè)異常的定義在connector中,而不像之前文件權(quán)限異常等是默認(rèn)異常。因此需要import mysql.connector模塊來(lái)識(shí)別出這個(gè)異常。

現(xiàn)在就能夠給出特定的異常通知了。

但是這并不好。為什么?我們更改的代碼和mysql這個(gè)數(shù)據(jù)庫(kù)耦合的太緊了。這可能有點(diǎn)難理解。通俗一點(diǎn)來(lái)說(shuō)就是我們現(xiàn)在的代碼和mysql糾纏太深,如果我們想更換別的數(shù)據(jù)庫(kù),需要改的地方太多了,不能很快的改過(guò)去。這對(duì)于主程序來(lái)說(shuō)是一個(gè)很大的缺點(diǎn)。

如何改進(jìn)呢?

在DBcm接口中使用緊耦合的代碼,并提供一個(gè)接口,主程序通過(guò)這個(gè)接口就能實(shí)現(xiàn)和import mysql.connector一樣的功能,若是想更改數(shù)據(jù)庫(kù),根本不用改主程序,因?yàn)樗褂玫氖荄Bcm的接口,只需要改DBcm的代碼就可以了。這樣就實(shí)現(xiàn)了主程序和數(shù)據(jù)庫(kù)的解耦。

之前我們?cè)趯慏Bcm的代碼時(shí),目的是寫一個(gè)上下文管理器,它的exit函數(shù)有四個(gè)參數(shù),后三個(gè)參數(shù)就是用來(lái)做這個(gè)的?,F(xiàn)在終于可以用上了:exc_type、exc_value、exc_trace,正好對(duì)應(yīng)元組中的三個(gè)元素。我們來(lái)看原來(lái)的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()

如果出問(wèn)題,會(huì)有什么后果呢?

如果enter出問(wèn)題,那with會(huì)直接終止,后續(xù)的exit處理也會(huì)取消。因?yàn)?strong>enter都出問(wèn)題了,上下文正確配置好的概率微乎其微,連接可能還沒(méi)建立,你斷開個(gè)毛線。

enter會(huì)出什么問(wèn)題呢?最大的問(wèn)題應(yīng)該是后端數(shù)據(jù)庫(kù)不可用,連接建立失敗,要針對(duì)這個(gè)生成一個(gè)定制異常。

如何創(chuàng)建一個(gè)定制異常?

首先重申一下定制異常是什么。定制異常是python的Exception類中沒(méi)有的異常,因?yàn)闆](méi)有這種異常,因此需要我們自己寫,也就是定制。一般是針對(duì)一種情況,給他起個(gè)別名,這就是定制異常。InterfaceError就是一個(gè)定制異常。然而為了脫耦,我們要把這個(gè)定制異常寫成我們自己的定制異常,從而讓主程序捕獲我們的異常,當(dāng)數(shù)據(jù)庫(kù)改變時(shí),就直接改我們的定制異常就可以,主程序捕獲的不變,這就是脫耦的原理。

定制異常也是異常,因此它要繼承Exception這個(gè)類。下面做一個(gè)簡(jiǎn)單的實(shí)驗(yàn):

class ConnectionError(Exception):
    pass

我們定義了一個(gè)名為ConnectionError的異常,它繼承了Exception的類,繼承某個(gè)類A只需要定義的時(shí)候在類名后加上(A)即可。

這是一個(gè)空類,但并不代表它什么都做不了,至少它具有Exception類的所有功能,因此看上去就好像是在Exception類中新加了一個(gè)成員一樣。

如何引發(fā)這個(gè)異常呢?他是個(gè)空類,也沒(méi)告訴我什么時(shí)候可能會(huì)出現(xiàn)這個(gè)異常啊。

使用raise產(chǎn)生這個(gè)異常。如下:


image

會(huì)產(chǎn)生一個(gè)回溯跟蹤消息,表明產(chǎn)生了一個(gè)異常。

也可以使用try-except來(lái)捕獲這個(gè)異常,如下:


image

可以看到,try成功捕獲了這個(gè)異常。

還可以看到這一點(diǎn):即用raise產(chǎn)生一個(gè)異常時(shí),異常名后面括號(hào)里的字符串實(shí)際上就是異常的類型,可以調(diào)整這里更改err輸出的內(nèi)容。

接下來(lái)我們要修改DBcm的代碼,定制一個(gè)自己的異常,用于反映數(shù)據(jù)庫(kù)連接失敗。如下:

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()

套路是一樣的,首先定義一個(gè)新類,然后當(dāng)enter運(yùn)行時(shí),把內(nèi)部代碼放在try的代碼組內(nèi),注意三句都要放進(jìn)去,而不是只放建立連接那句。因?yàn)橹环拍且痪涞脑?,如果連接建立失敗,雖然會(huì)捕獲異常,但是剩下兩句還是會(huì)繼續(xù)運(yùn)行,還是會(huì)報(bào)錯(cuò)導(dǎo)致應(yīng)用崩潰。如果三句都放進(jìn)去,第一句出錯(cuò)的話,后面兩句直接不會(huì)運(yùn)行。

然后用except捕獲因無(wú)法連接數(shù)據(jù)庫(kù)而產(chǎn)生的異常mysql.connector.errors.InterfaceError,將它的類型儲(chǔ)存在err中,然后產(chǎn)生我們自己的異常ConnectionError,我們的異常的類型就是err。

簡(jiǎn)而言之,這是一個(gè)接力:連接數(shù)據(jù)庫(kù)出錯(cuò)→mysql.connector產(chǎn)生異常InterfaceError→捕獲該異常→產(chǎn)生異常ConnectionError。

接下來(lái)修改view_the_log函數(shù)如下:

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一個(gè)字符串。

另外,由于我不知道如何出現(xiàn)“無(wú)法找到后端數(shù)據(jù)庫(kù)”的錯(cuò)誤,因此我選擇關(guān)閉mysql服務(wù),它的異常名為mysql.connector.errors.DatabaseError,針對(duì)這個(gè)異常,只需要在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()

十分方便。效果如下:

接下來(lái)考慮下一個(gè)問(wèn)題:

enter函數(shù)中出現(xiàn)異常,我們?cè)诤瘮?shù)內(nèi)部捕獲,那try代碼組出現(xiàn)異常怎么辦?總不能到代碼組去捕獲吧?

為什么不能呢?

因?yàn)槲覀冞@個(gè)代碼組是運(yùn)行SQL代碼的,如果在代碼組捕獲,又需要import SQL了,再次緊耦合,因此不能在代碼組捕獲。這時(shí)候exit的后三個(gè)參數(shù)就派上用場(chǎng)了:若是try的代碼組出現(xiàn)異常,會(huì)將這一異常的三元素傳入exit的后三個(gè)參數(shù)中,在exit中可以對(duì)代碼組中的異常進(jìn)行處理。

接下來(lái)擴(kuò)展兩個(gè)定制異常:

CredentialsError:當(dāng)enter方法中出現(xiàn)ProgrammingError錯(cuò)誤時(shí)產(chǎn)生這個(gè)異常。

SQLError:當(dāng)exit方法中出現(xiàn)ProgrammingError錯(cuò)誤時(shí)產(chǎn)生這個(gè)異常。

ProgrammingError異常一般出現(xiàn)在訪問(wèn)數(shù)據(jù)庫(kù)的憑據(jù)錯(cuò)誤(字典中的密碼錯(cuò)了什么的)或者是SQL語(yǔ)句出現(xiàn)語(yǔ)法錯(cuò)誤時(shí)出現(xiàn)。這也是為什么enter函數(shù)中出現(xiàn)這個(gè)異常叫CredentialsError,因?yàn)?strong>enter函數(shù)需要用到憑據(jù);而exit函數(shù)會(huì)接收try代碼組中的錯(cuò)誤,代碼組中有SQL語(yǔ)句。

第一個(gè)定制異常很簡(jiǎn)單,原理和之前的一樣,因此不用強(qiáng)調(diào)。然而第二個(gè)定制異常有一些問(wèn)題。

第二個(gè)定制異常與代碼組中的異常有關(guān),代碼組中的異常類型將傳入exc_type,因此要在exit中判斷exc_type是否是ProgrammingError。在哪里判斷呢?一定要在exit的最后判斷,也就是exit把自己當(dāng)工作都做完了再判斷。因?yàn)槿羰桥袛喑晒?,則會(huì)引起一個(gè)異常,那其余代碼就不會(huì)繼續(xù)運(yùn)行,對(duì)于我們的代碼,連接就不會(huì)斷開了,這是不可取的。另外,如果出現(xiàn)其他異常,可以在判斷完P(guān)rogrammingError之后再進(jìn)行其他判斷。代碼如下:

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,而我自己改成==也能夠正常運(yùn)行,is和==的區(qū)別參考該網(wǎng)址。

效果如下:

密碼錯(cuò)誤時(shí):


image

SQL錯(cuò)誤時(shí):


image

現(xiàn)在,只剩下那些“需要長(zhǎng)時(shí)間等待”的問(wèn)題等待我們處理了。

這留到下一章。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容