小編給大家分享一下Python異常處理的小技巧,希望大家閱讀完這篇文章后大所收獲,下面讓我們一起去探討吧!
創(chuàng)新互聯(lián)公司是一家集網(wǎng)站建設(shè),望奎企業(yè)網(wǎng)站建設(shè),望奎品牌網(wǎng)站建設(shè),網(wǎng)站定制,望奎網(wǎng)站建設(shè)報(bào)價(jià),網(wǎng)絡(luò)營(yíng)銷,網(wǎng)絡(luò)優(yōu)化,望奎網(wǎng)站推廣為一體的創(chuàng)新建站企業(yè),幫助傳統(tǒng)企業(yè)提升企業(yè)形象加強(qiáng)企業(yè)競(jìng)爭(zhēng)力??沙浞譂M足這一群體相比中小企業(yè)更為豐富、高端、多元的互聯(lián)網(wǎng)需求。同時(shí)我們時(shí)刻保持專業(yè)、時(shí)尚、前沿,時(shí)刻以成就客戶成長(zhǎng)自我,堅(jiān)持不斷學(xué)習(xí)、思考、沉淀、凈化自己,讓我們?yōu)楦嗟钠髽I(yè)打造出實(shí)用型網(wǎng)站。
如果你用 Python 編程,那么你就無法避開異常,因?yàn)楫惓T谶@門語言里無處不在。打個(gè)比方,當(dāng)你在腳本執(zhí)行時(shí)按 ctrl+c 退出,解釋器就會(huì)產(chǎn)生一個(gè) KeyboardInterrupt 異常。而 KeyError、 ValueError、 TypeError 等更是日常編程里隨處可見的老朋友。
異常處理工作由“捕獲”和“拋出”兩部分組成?!安东@”指的是使用 try...except 包裹特定語句,妥當(dāng)?shù)耐瓿慑e(cuò)誤流程處理。而恰當(dāng)?shù)氖褂?raise 主動(dòng)“拋出”異常,更是優(yōu)雅代碼里必不可少的組成部分。
在這篇文章里,我會(huì)分享與異常處理相關(guān)的 3 個(gè)好習(xí)慣。繼續(xù)閱讀前,我希望你已經(jīng)了解了下面這些知識(shí)點(diǎn):
異常的基本語法與用法(建議閱讀官方文檔 “Errors and Exceptions”)
為什么要使用異常代替錯(cuò)誤返回(建議閱讀《讓函數(shù)返回結(jié)果的技巧》)
為什么在寫 Python 時(shí)鼓勵(lì)使用異常 (建議閱讀 “Write Cleaner Python: Use Exceptions”)
三個(gè)好習(xí)慣
1. 只做最精確的異常捕獲
假如你不夠了解異常機(jī)制,就難免會(huì)對(duì)它有一種天然恐懼感。你可能會(huì)覺得:異常是一種不好的東西,好的程序就應(yīng)該捕獲所有的異常,讓一切都平平穩(wěn)穩(wěn)的運(yùn)行。而抱著這種想法寫出的代碼,里面通常會(huì)出現(xiàn)大段含糊的異常捕獲邏輯。
讓我們用一段可執(zhí)行腳本作為樣例:
# -*- coding: utf-8 -*- import requests import re def save_website_title(url, filename): """獲取某個(gè)地址的網(wǎng)頁標(biāo)題,然后將其寫入到文件中 :returns: 如果成功保存,返回 True,否則打印錯(cuò)誤,返回 False """ try: resp = requests.get(url) obj = re.search(r'<title>(.*)</title>', resp.text) if not obj: print('save failed: title tag not found in page content') return False title = obj.grop(1) with open(filename, 'w') as fp: fp.write(title) return True except Exception: print(f'save failed: unable to save title of {url} to {filename}') return False def main(): save_website_title('https://www.qq.com', 'qq_title.txt') if __name__ == '__main__': main()
腳本里的 save_website_title 函數(shù)做了好幾件事情。它首先通過網(wǎng)絡(luò)獲取網(wǎng)頁內(nèi)容,然后利用正則匹配出標(biāo)題,最后將標(biāo)題寫在本地文件里。而這里有兩個(gè)步驟很容易出錯(cuò):網(wǎng)絡(luò)請(qǐng)求 與 本地文件操作。所以在代碼里,我們用一個(gè)大大的 try...except 語句塊,將這幾個(gè)步驟都包裹了起來。安全第一 。
那么,這段看上去簡(jiǎn)潔易懂的代碼,里面藏著什么問題呢?
如果你旁邊剛好有一臺(tái)安裝了 Python 的電腦,那么你可以試著跑一遍上面的腳本。你會(huì)發(fā)現(xiàn),上面的代碼是不能成功執(zhí)行的。而且你還會(huì)發(fā)現(xiàn),無論你如何修改網(wǎng)址和目標(biāo)文件的值,程序仍然會(huì)報(bào)錯(cuò) “save failed: unable to...”。為什么呢?
問題就藏在這個(gè)碩大無比的 try...except 語句塊里。假如你把眼睛貼近屏幕,非常仔細(xì)的檢查這段代碼。你會(huì)發(fā)現(xiàn)在編寫函數(shù)時(shí),我犯了一個(gè)小錯(cuò)誤,我把獲取正則匹配串的方法錯(cuò)打成了 obj.grop(1),少了一個(gè) 'u'( obj.group(1))。
但正是因?yàn)槟莻€(gè)過于龐大、含糊的異常捕獲,這個(gè)由打錯(cuò)方法名導(dǎo)致的原本該被拋出的 AttibuteError 卻被吞噬了。從而給我們的 debug 過程增加了不必要的麻煩。
異常捕獲的目的,不是去捕獲盡可能多的異常。假如我們從一開始就堅(jiān)持:只做最精準(zhǔn)的異常捕獲。那么這樣的問題就根本不會(huì)發(fā)生,精準(zhǔn)捕獲包括:
永遠(yuǎn)只捕獲那些可能會(huì)拋出異常的語句塊
盡量只捕獲精確的異常類型,而不是模糊的 Exception
依照這個(gè)原則,我們的樣例應(yīng)該被改成這樣:
from requests.exceptions import RequestException def save_website_title(url, filename): try: resp = requests.get(url) except RequestException as e: print(f'save failed: unable to get page content: {e}') return False # 這段正則操作本身就是不應(yīng)該拋出異常的,所以我們沒必要使用 try 語句塊 # 假如 group 被誤打成了 grop 也沒關(guān)系,程序馬上就會(huì)通過 AttributeError 來 # 告訴我們。 obj = re.search(r'<title>(.*)</title>', resp.text) if not obj: print('save failed: title tag not found in page content') return False title = obj.group(1) try: with open(filename, 'w') as fp: fp.write(title) except IOError as e: print(f'save failed: unable to write to file {filename}: {e}') return False else: return True
2. 別讓異常破壞抽象一致性
大約四五年前,當(dāng)時(shí)的我正在開發(fā)某移動(dòng)應(yīng)用的后端 API 項(xiàng)目。如果你也有過開發(fā)后端 API 的經(jīng)驗(yàn),那么你一定知道,這樣的系統(tǒng)都需要制定一套“API 錯(cuò)誤碼規(guī)范”,來為客戶端處理調(diào)用錯(cuò)誤時(shí)提供方便。
一個(gè)錯(cuò)誤碼返回大概長(zhǎng)這個(gè)樣子:
// HTTP Status Code: 400 // Content-Type: application/json { "code": "UNABLE_TO_UPVOTE_YOUR_OWN_REPLY", "detail": "你不能推薦自己的回復(fù)" }
在制定好錯(cuò)誤碼規(guī)范后,接下來的任務(wù)就是如何實(shí)現(xiàn)它。當(dāng)時(shí)的項(xiàng)目使用了 Django 框架,而 Django 的錯(cuò)誤頁面正是使用了異常機(jī)制實(shí)現(xiàn)的。打個(gè)比方,如果你想讓一個(gè)請(qǐng)求返回 404 狀態(tài)碼,那么只要在該請(qǐng)求處理過程中執(zhí)行 raiseHttp404 即可。
所以,我們很自然的從 Django 獲得了靈感。首先,我們?cè)陧?xiàng)目?jī)?nèi)定義了錯(cuò)誤碼異常類: APIErrorCode。然后依據(jù)“錯(cuò)誤碼規(guī)范”,寫了很多繼承該類的錯(cuò)誤碼。當(dāng)需要返回錯(cuò)誤信息給用戶時(shí),只需要做一次 raise 就能搞定。
用戶時(shí),只需要做一次 raise 就能搞定。
raise error_codes.UNABLE_TO_UPVOTE raise error_codes.USER_HAS_BEEN_BANNED ... ...
毫無意外,所有人都很喜歡用這種方式來返回錯(cuò)誤碼。因?yàn)樗闷饋矸浅7奖?,無論調(diào)用棧多深,只要你想給用戶返回錯(cuò)誤碼,調(diào)用 raiseerror_codes.ANY_THING 就好。
隨著時(shí)間推移,項(xiàng)目也變得越來越龐大,拋出 APIErrorCode 的地方也越來越多。有一天,我正準(zhǔn)備復(fù)用一個(gè)底層圖片處理函數(shù)時(shí),突然碰到了一個(gè)問題。
我看到了一段讓我非常糾結(jié)的代碼:
# 在某個(gè)處理圖像的模塊內(nèi)部 # <PROJECT_ROOT>/util/image/processor.py def process_image(...): try: image = Image.open(fp) except Exception: # 說明(非項(xiàng)目原注釋):該異常將會(huì)被 Django 的中間件捕獲,往前端返回 # "上傳的圖片格式有誤" 信息 raise error_codes.INVALID_IMAGE_UPLOADED ... ...
process_image 函數(shù)會(huì)嘗試解析一個(gè)文件對(duì)象,如果該對(duì)象不能被作為圖片正常打開,就拋出 error_codes.INVALID_IMAGE_UPLOADED(APIErrorCode子類) 異常,從而給調(diào)用方返回錯(cuò)誤代碼 JSON。
讓我給你從頭理理這段代碼。最初編寫 process_image 時(shí),我雖然把它放在了 util.image 模塊里,但當(dāng)時(shí)調(diào)這個(gè)函數(shù)的地方就只有 “處理用戶上傳圖片的 POST 請(qǐng)求” 而已。為了偷懶,我讓函數(shù)直接拋出 APIErrorCode 異常來完成了錯(cuò)誤處理工作。
再來說當(dāng)時(shí)的問題。那時(shí)我需要寫一個(gè)在后臺(tái)運(yùn)行的批處理圖片腳本,而它剛好可以復(fù)用 process_image 函數(shù)所實(shí)現(xiàn)的功能。但這時(shí)不對(duì)勁的事情出現(xiàn)了,如果我想復(fù)用該函數(shù),那么:
我必須去捕獲一個(gè)名為 INVALID_IMAGE_UPLOADED 的異常
哪怕我的圖片根本就不是來自于用戶上傳
我必須引入 APIErrorCode 異常類作為依賴來捕獲異常
哪怕我的腳本和 Django API 根本沒有任何關(guān)系
這就是異常類抽象層級(jí)不一致導(dǎo)致的結(jié)果。APIErrorCode 異常類的意義,在于表達(dá)一種能夠直接被終端用戶(人)識(shí)別并消費(fèi)的“錯(cuò)誤代碼”。它在整個(gè)項(xiàng)目里,屬于最高層的抽象之一。但是出于方便,我們卻在底層模塊里引入并拋出了它。這打破了 image.processor 模塊的抽象一致性,影響了它的可復(fù)用性和可維護(hù)性。
這類情況屬于“模塊拋出了高于所屬抽象層級(jí)的異?!?。避免這類錯(cuò)誤需要注意以下幾點(diǎn):
讓模塊只拋出與當(dāng)前抽象層級(jí)一致的異常
比如 image.processer 模塊應(yīng)該拋出自己封裝的 ImageOpenError 異常
在必要的地方進(jìn)行異常包裝與轉(zhuǎn)換
比如,應(yīng)該在貼近高層抽象(視圖 View 函數(shù))的地方,將圖像處理模塊的 ImageOpenError 低級(jí)異常包裝轉(zhuǎn)換為 APIErrorCode 高級(jí)異常
修改后的代碼:
# <PROJECT_ROOT>/util/image/processor.py class ImageOpenError(Exception): pass def process_image(...): try: image = Image.open(fp) except Exception as e: raise ImageOpenError(exc=e) ... ... # <PROJECT_ROOT>/app/views.py def foo_view_function(request): try: process_image(fp) except ImageOpenError: raise error_codes.INVALID_IMAGE_UPLOADED
除了應(yīng)該避免拋出高于當(dāng)前抽象級(jí)別的異常外,我們同樣應(yīng)該避免泄露低于當(dāng)前抽象級(jí)別的異常。
如果你用過 requests 模塊,你可能已經(jīng)發(fā)現(xiàn)它請(qǐng)求頁面出錯(cuò)時(shí)所拋出的異常,并不是它在底層所使用的 urllib3 模塊的原始異常,而是通過 requests.exceptions 包裝過一次的異常。
>>> try: ... requests.get('https://www.invalid-host-foo.com') ... except Exception as e: ... print(type(e)) ... <class 'requests.exceptions.ConnectionError'>
這樣做同樣是為了保證異常類的抽象一致性。因?yàn)?urllib3 模塊是 requests 模塊依賴的底層實(shí)現(xiàn)細(xì)節(jié),而這個(gè)細(xì)節(jié)有可能在未來版本發(fā)生變動(dòng)。所以必須對(duì)它拋出的異常進(jìn)行恰當(dāng)?shù)陌b,避免未來的底層變更對(duì) requests 用戶端錯(cuò)誤處理邏輯產(chǎn)生影響。
3. 異常處理不應(yīng)該喧賓奪主
在前面我們提到異常捕獲要精準(zhǔn)、抽象級(jí)別要一致。但在現(xiàn)實(shí)世界中,如果你嚴(yán)格遵循這些流程,那么很有可能會(huì)碰上另外一個(gè)問題:異常處理邏輯太多,以至于擾亂了代碼核心邏輯。具體表現(xiàn)就是,代碼里充斥著大量的 try、 except、 raise 語句,讓核心邏輯變得難以辨識(shí)。
讓我們看一段例子:
def upload_avatar(request): """用戶上傳新頭像""" try: avatar_file = request.FILES['avatar'] except KeyError: raise error_codes.AVATAR_FILE_NOT_PROVIDED try: resized_avatar_file = resize_avatar(avatar_file) except FileTooLargeError as e: raise error_codes.AVATAR_FILE_TOO_LARGE except ResizeAvatarError as e: raise error_codes.AVATAR_FILE_INVALID try: request.user.avatar = resized_avatar_file request.user.save() except Exception: raise error_codes.INTERNAL_SERVER_ERROR return HttpResponse({})
這是一個(gè)處理用戶上傳頭像的視圖函數(shù)。這個(gè)函數(shù)內(nèi)做了三件事情,并且針對(duì)每件事都做了異常捕獲。如果做某件事時(shí)發(fā)生了異常,就返回對(duì)用戶友好的錯(cuò)誤到前端。
這樣的處理流程縱然合理,但是顯然代碼里的異常處理邏輯有點(diǎn)“喧賓奪主”了。一眼看過去全是代碼縮進(jìn),很難提煉出代碼的核心邏輯。
早在 2.5 版本時(shí),Python 語言就已經(jīng)提供了對(duì)付這類場(chǎng)景的工具:“上下文管理器(context manager)”。上下文管理器是一種配合 with 語句使用的特殊 Python 對(duì)象,通過它,可以讓異常處理工作變得更方便。
那么,如何利用上下文管理器來改善我們的異常處理流程呢?讓我們直接看代碼吧。
class raise_api_error: """captures specified exception and raise ApiErrorCode instead :raises: AttributeError if code_name is not valid """ def __init__(self, captures, code_name): self.captures = captures self.code = getattr(error_codes, code_name) def __enter__(self): # 剛方法將在進(jìn)入上下文時(shí)調(diào)用 return self def __exit__(self, exc_type, exc_val, exc_tb): # 該方法將在退出上下文時(shí)調(diào)用 # exc_type, exc_val, exc_tb 分別表示該上下文內(nèi)拋出的 # 異常類型、異常值、錯(cuò)誤棧 if exc_type is None: return False if exc_type == self.captures: raise self.code from exc_val return False
在上面的代碼里,我們定義了一個(gè)名為 raise_api_error 的上下文管理器,它在進(jìn)入上下文時(shí)什么也不做。但是在退出上下文時(shí),會(huì)判斷當(dāng)前上下文中是否拋出了類型為 self.captures 的異常,如果有,就用 APIErrorCode 異常類替代它。
使用該上下文管理器后,整個(gè)函數(shù)可以變得更清晰簡(jiǎn)潔:
def upload_avatar(request): """用戶上傳新頭像""" with raise_api_error(KeyError, 'AVATAR_FILE_NOT_PROVIDED'): avatar_file = request.FILES['avatar'] with raise_api_error(ResizeAvatarError, 'AVATAR_FILE_INVALID'),\ raise_api_error(FileTooLargeError, 'AVATAR_FILE_TOO_LARGE'): resized_avatar_file = resize_avatar(avatar_file) with raise_api_error(Exception, 'INTERNAL_SERVER_ERROR'): request.user.avatar = resized_avatar_file request.user.save() return HttpResponse({})
看完了這篇文章,相信你對(duì)Python異常處理的小技巧有了一定的了解,想了解更多相關(guān)知識(shí),歡迎關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道,感謝各位的閱讀!
標(biāo)題名稱:Python異常處理的小技巧
URL地址:http://jinyejixie.com/article6/jdoiog.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站營(yíng)銷、網(wǎng)站內(nèi)鏈、網(wǎng)站設(shè)計(jì)、用戶體驗(yàn)、App設(shè)計(jì)、網(wǎng)站制作
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)