中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Python異常處理的小技巧

發布時間:2020-11-02 13:01:14 來源:億速云 閱讀:144 作者:小新 欄目:編程語言

小編給大家分享一下Python異常處理的小技巧,希望大家閱讀完這篇文章后大所收獲,下面讓我們一起去探討吧!

如果你用 Python 編程,那么你就無法避開異常,因為異常在這門語言里無處不在。打個比方,當你在腳本執行時按 ctrl+c 退出,解釋器就會產生一個 KeyboardInterrupt 異常。而 KeyError、 ValueError、 TypeError 等更是日常編程里隨處可見的老朋友。

異常處理工作由“捕獲”和“拋出”兩部分組成。“捕獲”指的是使用 try...except 包裹特定語句,妥當的完成錯誤流程處理。而恰當的使用 raise 主動“拋出”異常,更是優雅代碼里必不可少的組成部分。

在這篇文章里,我會分享與異常處理相關的 3 個好習慣。繼續閱讀前,我希望你已經了解了下面這些知識點:

異常的基本語法與用法(建議閱讀官方文檔 “Errors and Exceptions”)

為什么要使用異常代替錯誤返回(建議閱讀《讓函數返回結果的技巧》)

為什么在寫 Python 時鼓勵使用異常 (建議閱讀 “Write Cleaner Python: Use Exceptions”)

三個好習慣

1. 只做最精確的異常捕獲

假如你不夠了解異常機制,就難免會對它有一種天然恐懼感。你可能會覺得:異常是一種不好的東西,好的程序就應該捕獲所有的異常,讓一切都平平穩穩的運行。而抱著這種想法寫出的代碼,里面通常會出現大段含糊的異常捕獲邏輯。

讓我們用一段可執行腳本作為樣例:

# -*- coding: utf-8 -*-	
import requests	
import re	
def save_website_title(url, filename):	
    """獲取某個地址的網頁標題,然后將其寫入到文件中	
    :returns: 如果成功保存,返回 True,否則打印錯誤,返回 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 函數做了好幾件事情。它首先通過網絡獲取網頁內容,然后利用正則匹配出標題,最后將標題寫在本地文件里。而這里有兩個步驟很容易出錯:網絡請求 與 本地文件操作。所以在代碼里,我們用一個大大的 try...except 語句塊,將這幾個步驟都包裹了起來。安全第一 。

那么,這段看上去簡潔易懂的代碼,里面藏著什么問題呢?

如果你旁邊剛好有一臺安裝了 Python 的電腦,那么你可以試著跑一遍上面的腳本。你會發現,上面的代碼是不能成功執行的。而且你還會發現,無論你如何修改網址和目標文件的值,程序仍然會報錯 “save failed: unable to...”。為什么呢?

問題就藏在這個碩大無比的 try...except 語句塊里。假如你把眼睛貼近屏幕,非常仔細的檢查這段代碼。你會發現在編寫函數時,我犯了一個小錯誤,我把獲取正則匹配串的方法錯打成了 obj.grop(1),少了一個 'u'( obj.group(1))。

但正是因為那個過于龐大、含糊的異常捕獲,這個由打錯方法名導致的原本該被拋出的 AttibuteError 卻被吞噬了。從而給我們的 debug 過程增加了不必要的麻煩。

異常捕獲的目的,不是去捕獲盡可能多的異常。假如我們從一開始就堅持:只做最精準的異常捕獲。那么這樣的問題就根本不會發生,精準捕獲包括:

永遠只捕獲那些可能會拋出異常的語句塊

盡量只捕獲精確的異常類型,而不是模糊的 Exception

依照這個原則,我們的樣例應該被改成這樣:

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	
# 這段正則操作本身就是不應該拋出異常的,所以我們沒必要使用 try 語句塊	
# 假如 group 被誤打成了 grop 也沒關系,程序馬上就會通過 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. 別讓異常破壞抽象一致性

大約四五年前,當時的我正在開發某移動應用的后端 API 項目。如果你也有過開發后端 API 的經驗,那么你一定知道,這樣的系統都需要制定一套“API 錯誤碼規范”,來為客戶端處理調用錯誤時提供方便。

一個錯誤碼返回大概長這個樣子:

// HTTP Status Code: 400	
// Content-Type: application/json	
{	
"code": "UNABLE_TO_UPVOTE_YOUR_OWN_REPLY",	
"detail": "你不能推薦自己的回復"	
}

在制定好錯誤碼規范后,接下來的任務就是如何實現它。當時的項目使用了 Django 框架,而 Django 的錯誤頁面正是使用了異常機制實現的。打個比方,如果你想讓一個請求返回 404 狀態碼,那么只要在該請求處理過程中執行 raiseHttp404 即可。

所以,我們很自然的從 Django 獲得了靈感。首先,我們在項目內定義了錯誤碼異常類: APIErrorCode。然后依據“錯誤碼規范”,寫了很多繼承該類的錯誤碼。當需要返回錯誤信息給用戶時,只需要做一次 raise 就能搞定。

用戶時,只需要做一次 raise 就能搞定。

raise error_codes.UNABLE_TO_UPVOTE	
raise error_codes.USER_HAS_BEEN_BANNED	
... ...

毫無意外,所有人都很喜歡用這種方式來返回錯誤碼。因為它用起來非常方便,無論調用棧多深,只要你想給用戶返回錯誤碼,調用 raiseerror_codes.ANY_THING 就好。

隨著時間推移,項目也變得越來越龐大,拋出 APIErrorCode 的地方也越來越多。有一天,我正準備復用一個底層圖片處理函數時,突然碰到了一個問題。

我看到了一段讓我非常糾結的代碼:

# 在某個處理圖像的模塊內部	
# <PROJECT_ROOT>/util/image/processor.py	
def process_image(...):	
try:	
image = Image.open(fp)	
except Exception:	
# 說明(非項目原注釋):該異常將會被 Django 的中間件捕獲,往前端返回	
# "上傳的圖片格式有誤" 信息	
raise error_codes.INVALID_IMAGE_UPLOADED	
... ...

process_image 函數會嘗試解析一個文件對象,如果該對象不能被作為圖片正常打開,就拋出 error_codes.INVALID_IMAGE_UPLOADED(APIErrorCode子類) 異常,從而給調用方返回錯誤代碼 JSON。

讓我給你從頭理理這段代碼。最初編寫 process_image 時,我雖然把它放在了 util.image 模塊里,但當時調這個函數的地方就只有 “處理用戶上傳圖片的 POST 請求” 而已。為了偷懶,我讓函數直接拋出 APIErrorCode 異常來完成了錯誤處理工作。

再來說當時的問題。那時我需要寫一個在后臺運行的批處理圖片腳本,而它剛好可以復用 process_image 函數所實現的功能。但這時不對勁的事情出現了,如果我想復用該函數,那么:

我必須去捕獲一個名為 INVALID_IMAGE_UPLOADED 的異常

哪怕我的圖片根本就不是來自于用戶上傳

我必須引入 APIErrorCode 異常類作為依賴來捕獲異常

哪怕我的腳本和 Django API 根本沒有任何關系

這就是異常類抽象層級不一致導致的結果。APIErrorCode 異常類的意義,在于表達一種能夠直接被終端用戶(人)識別并消費的“錯誤代碼”。它在整個項目里,屬于最高層的抽象之一。但是出于方便,我們卻在底層模塊里引入并拋出了它。這打破了 image.processor 模塊的抽象一致性,影響了它的可復用性和可維護性。

這類情況屬于“模塊拋出了高于所屬抽象層級的異常”。避免這類錯誤需要注意以下幾點:

讓模塊只拋出與當前抽象層級一致的異常

比如 image.processer 模塊應該拋出自己封裝的 ImageOpenError 異常

在必要的地方進行異常包裝與轉換

比如,應該在貼近高層抽象(視圖 View 函數)的地方,將圖像處理模塊的 ImageOpenError 低級異常包裝轉換為 APIErrorCode 高級異常

修改后的代碼:

# <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

除了應該避免拋出高于當前抽象級別的異常外,我們同樣應該避免泄露低于當前抽象級別的異常。

如果你用過 requests 模塊,你可能已經發現它請求頁面出錯時所拋出的異常,并不是它在底層所使用的 urllib3 模塊的原始異常,而是通過 requests.exceptions 包裝過一次的異常。

>>> try:	
...     requests.get('https://www.invalid-host-foo.com')	
... except Exception as e:	
...     print(type(e))	
...	
<class 'requests.exceptions.ConnectionError'>

這樣做同樣是為了保證異常類的抽象一致性。因為 urllib3 模塊是 requests 模塊依賴的底層實現細節,而這個細節有可能在未來版本發生變動。所以必須對它拋出的異常進行恰當的包裝,避免未來的底層變更對 requests 用戶端錯誤處理邏輯產生影響。

3. 異常處理不應該喧賓奪主

在前面我們提到異常捕獲要精準、抽象級別要一致。但在現實世界中,如果你嚴格遵循這些流程,那么很有可能會碰上另外一個問題:異常處理邏輯太多,以至于擾亂了代碼核心邏輯。具體表現就是,代碼里充斥著大量的 try、 except、 raise 語句,讓核心邏輯變得難以辨識。

讓我們看一段例子:

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

這是一個處理用戶上傳頭像的視圖函數。這個函數內做了三件事情,并且針對每件事都做了異常捕獲。如果做某件事時發生了異常,就返回對用戶友好的錯誤到前端。

這樣的處理流程縱然合理,但是顯然代碼里的異常處理邏輯有點“喧賓奪主”了。一眼看過去全是代碼縮進,很難提煉出代碼的核心邏輯。

早在 2.5 版本時,Python 語言就已經提供了對付這類場景的工具:“上下文管理器(context manager)”。上下文管理器是一種配合 with 語句使用的特殊 Python 對象,通過它,可以讓異常處理工作變得更方便。

那么,如何利用上下文管理器來改善我們的異常處理流程呢?讓我們直接看代碼吧。

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):	
# 剛方法將在進入上下文時調用	
return self	
def __exit__(self, exc_type, exc_val, exc_tb):	
# 該方法將在退出上下文時調用	
# exc_type, exc_val, exc_tb 分別表示該上下文內拋出的	
# 異常類型、異常值、錯誤棧	
if exc_type is None:	
return False	
if exc_type == self.captures:	
raise self.code from exc_val	
return False

在上面的代碼里,我們定義了一個名為 raise_api_error 的上下文管理器,它在進入上下文時什么也不做。但是在退出上下文時,會判斷當前上下文中是否拋出了類型為 self.captures 的異常,如果有,就用 APIErrorCode 異常類替代它。

使用該上下文管理器后,整個函數可以變得更清晰簡潔:

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

看完了這篇文章,相信你對Python異常處理的小技巧有了一定的了解,想了解更多相關知識,歡迎關注億速云行業資訊頻道,感謝各位的閱讀!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

十堰市| 三明市| 余江县| 鄂托克前旗| 渝北区| 潼关县| 太康县| 长兴县| 丹凤县| 曲阜市| 苍南县| 白水县| 陵川县| 民县| 岑溪市| 琼中| 浙江省| 巨野县| 稻城县| 黎平县| 乐亭县| 古交市| 五台县| 宁城县| 关岭| 靖远县| 观塘区| 宁晋县| 沙河市| 泽普县| 都昌县| 化德县| 都匀市| 华安县| 承德县| 鹤山市| 东阳市| 泸州市| 凉城县| 东兴市| 通渭县|