mongotemplate中save拋出異常_異常處理的三個好習慣 | Python 工匠

487313986533bdae21a11a67c6ed24e9.png

? 文 | piglei? 編輯 | EarlGrey

推薦 | 編程派(微信ID:codingpy)

前言

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

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

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

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

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

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

40269888a82ba1adb9b5baab0d34a34c.png

三個好習慣

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'(.*)', 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'(.*)', 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 error_codes.UNABLE_TO_UPVOTE

raise error_codes.USER_HAS_BEEN_BANNED

... ...

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

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

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

# 在某個處理圖像的模塊內部

# /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?高級異常

修改后的代碼:

# /util/image/processor.py

class ImageOpenError(Exception):

pass

def process_image(...):

try:

image = Image.open(fp)

except Exception as e:

raise ImageOpenError(exc=e)

... ...

# /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. 異常處理不應該喧賓奪主

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

讓我們看一段例子:

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

Hint:建議閱讀 PEP 343 -- The "with" Statement | Python.org,了解與上下文管理器有關的更多知識。

模塊 contextlib 也提供了非常多與編寫上下文管理器相關的工具函數與樣例。

總結

在這篇文章中,我分享了與異常處理相關的三個建議。最后再總結一下要點:

  • 只捕獲可能會拋出異常的語句,避免含糊的捕獲邏輯

  • 保持模塊異常類的抽象一致性,必要時對底層異常類進行包裝

  • 使用“上下文管理器”可以簡化重復的異常處理邏輯

看完文章的你,有沒有什么想吐槽的?請留言或者在 項目 Github Issues 告訴我吧。

-- 完 --

本文經作者授權發布,如需轉載,請聯系原作者。

回復下方「關鍵詞」,獲取優質資源

回復關鍵詞「?pybook03」,立即獲取主頁君與小伙伴一起翻譯的《Think Python 2e》電子版

回復關鍵詞「pybooks02」,立即獲取 O'Reilly 出版社推出的免費 Python 相關電子書合集

回復關鍵詞「書單02」,立即獲取主頁君整理的 10 本 Python 入門書的電子版

4feff117a1f6b5db91355bbce5dcbd7f.png

印度小伙寫了套深度學習教程,Github上星標已經5000+

GitHub熱榜第四!這套Python機器學習課,免費獲取還易吸收

《流暢的 Python》到底好在哪?

如何系統化學習 Python ?

GitHub標星2.6萬!Python算法新手入門大全

使用 Vue.js 和 Flask 實現全棧單頁面應用

Python 實現一個自動化翻譯和替換的工具

使用 Python 制作屬于自己的 PDF 電子書

12步輕松搞定Python裝飾器

200 行代碼實現 2048 游戲

題圖:pexels,CC0 授權。

8b723bf3d25331cbdac2859297133352.png

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/534843.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/534843.shtml
英文地址,請注明出處:http://en.pswp.cn/news/534843.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

java 百度網盤上傳_使用pcs api往免費的百度網盤上傳下載文件的方法

百度個人云盤空間大&#xff0c;完全免費&#xff0c;而且提供了pcs api供調用操作文件&#xff0c;在平時的項目里往里面保存一些文件是很實用的。環境準備&#xff1a;開通讀寫網盤的權限及獲取access_token:http://blog.csdn.net/langyuezhang/article/details/47206621百度…

python縮進教學_Python縮進和選擇學習

縮進Python最具特色的是用縮進來標明成塊的代碼。我下面以if選擇結構來舉例。if后面跟隨條件&#xff0c;如果條件成立&#xff0c;則執行歸屬于if的一個代碼塊。先看C語言的表達方式(注意&#xff0c;這是C&#xff0c;不是Python!)if ( i > 0 ){ x 1; y 2;}如果i …

php如何新建xml文件,PHP中的生成XML文件的4種方法分享

生成如下XML串Xml代碼title1content12009-10-11title2content22009-11-11方法I.【直接生成字符串】使用純粹的PHP代碼生成字符串&#xff0c;并把這個字符串寫入一個以XML為后綴的文件。這是最原始的生成XML的方法&#xff0c;不過有效&#xff01;$data_array array(array(ti…

組態王能直接讀取儀表數據嗎_液晶多功能網絡電力儀表PD800H

液晶多功能網絡電力儀表PD800H-H44三相三線多功用電力表面&#xff0c;一般也被稱作網絡電力表面&#xff0c;它是一種數字化的監控設備&#xff0c;其功用集成了電量測量&#xff0c;情況監控&#xff0c;遠程通訊為一體&#xff0c;作業原理上選用了現代核算機技術和數字信號…

python程序顯示自己的版權_手把手教你Pycharm皮膚主題及個性化設置,python程序員必備-Go語言中文社區...

1.設置IDE皮膚主題File -> Settings -> Appearance -> Theme -> 選擇“Alloy.IDEA Theme”根據自己的喜好設置字體大小&#xff0c;以及樣式。2.修改字體大小File -> Settings > Editor -> Colors & Fonts -> Font -> Size -> 設置為“14”3…

java多線程activemq,多線程JMS客戶端ActiveMQ

我正在使用以下代碼創建多個JMS會話&#xff0c;以供多個使用者使用消息。我的問題是代碼以單線程方式運行。即使消息存在于隊列中&#xff0c;第二個線程也無法接收任何內容&#xff0c;而是繼續輪詢。同時&#xff0c;第一個線程完成對第一批的處理&#xff0c;然后返回并使用…

python cnn 實例_基于CNN的紋理合成實踐【附python實現】

Q0: Preliminary knowledge of Texture SynthesisBaseline請見此處&#xff0c;下文所有的代碼修改均建立此代碼基礎之上。1. 紋理合成簡述?紋理合成(Texture Systhesis)技術主要應用于計算機圖形學等領域&#xff0c;被用于模擬幾何模型的表面細節、增強繪制模型的真實感。不…

php使用jasperreport,php-報表引擎指南(Pentaho,JasperReports,BIRT)

我在各種論壇和他們的網站上花費了大約4-5個小時,研究可以幫助我發展的報告工具.我是使用這種工具的新手,可以使用一些特定的指導.我正在開發一個Web應用程序,該應用程序將托管在一臺服務器上,但是多個用戶可以通過登錄進行訪問.每個用戶將擁有自己的帳戶,并且只能訪問僅與與其…

python中dlib庫_python 基于dlib庫的人臉檢測的實現

本周暫時比較清閑&#xff0c;可以保持每日一更的速度。國外身份證項目新增需求&#xff0c;檢測出身份證正面的人臉。最開始考慮mobilenet-ssd&#xff0c;經同事提醒&#xff0c;有現成的人臉庫dlib&#xff0c;那就用傳統方法嘗試一下。dlib安裝dlib的安裝小費一波周折&…

php養老院管理系統,XYCMS養老院建站系統 v3.8

XYCMS養老院建站系統是一個專為養老院而設計的養老院建筑系統。中心信息管理&#xff1a;包括基本信息管理&#xff0c;添加&#xff0c;問答中心信息管理新聞動態管理&#xff1a;管理新聞信息內容&#xff0c;管理相關分類&#xff0c;添加或者刪除生活環境內容管理&#xff…

php 修改文件訪問時間,PHP中獲取文件創建日期、修改日期、訪問時間的方法

php獲取文件創建時間、修改時間常用代碼filemtime ( string filename )返回文件上次被修改的時間&#xff0c;出錯時返回 FALSE。時間以 Unix 時間戳的方式返回&#xff0c;可用于 date()。例如&#xff1a;$afilemtime("log.txt");echo "修改時間&#xff1a;&…

超過響應緩沖區限制_Nginx如何限制并發連接數和連接請求數?

全網最全1500份Java學習資料、500份BAT面試真題&#xff1a;關注公眾號&#xff0c;輸入“面試題”&#xff0c;獲取提取碼&#xff01;首先講解兩個算發&#xff1a;算法思想是&#xff1a;令牌以固定速率產生&#xff0c;并緩存到令牌桶中&#xff1b;令牌桶放滿時&#xff0…

Php公鑰加密data是空,php 生成加密公鑰加密私鑰實例詳解

php 生成加密公鑰加密私鑰實例詳解生成公鑰私鑰 win下必須要openssl.cof支持 liunx一般已自帶安裝$config array(//"digest_alg" > "sha512","private_key_bits" > 512, //字節數 512 1024 2048 4096 等"private_key_type" >…

python句柄無效_subprocess.Popen 運行windows命令出現“句柄無效”報錯的解決方法

【代碼筆記】iOS-關于UIFont的一些define一,效果圖. 二,工程圖. 三,代碼. RootViewController.h #import interface RootViewController ...JQuery Datatables Dom 和 Language 參數詳細說明http://linleizi.iteye.com/blog/2086435 *********************************** Data …

java日期函數精確到日,java日期時間函數分享

前言&#xff1a;對于新手程序員的我來說&#xff0c;寫業務代碼是現在的日常&#xff0c;在此過程中經常需要對日期時間進行處理&#xff0c;我挑了幾個較有用的日期處理函數分享給大家。正文&#xff1a;1、將某格式的時間字符串轉化成毫秒時間戳表示的字符串&#xff1a;pub…

python變量區變量保存與加載_python – Flask:使用全局變量將數據文件加載到內存中...

我有一個大的XML文件,它被打開,加載到內存中,然后由Python類關閉.簡化示例如下所示&#xff1a;class Dictionary():def __init__(self,filename):f open(filename)self.contents f.readlines()f.close()def getDefinitionForWord(self,word):# returns a word,using etree p…

php 順豐接口實例,順豐BSP接口PHP開發注意事項

1&#xff0c;xml報文不用附加"<?xml version"1.0" encoding"UTF-8" ?>"&#xff0c;不支持&#xff0c;會報格式錯誤。2&#xff0c;提交服務器時要重點注意。直接使用curl方式的&#xff0c;一定要把數據做http_build_query&#xff0…

跨域產生的原因和解決方法_板式家具開料機加工過程產生崩邊原因及解決方法...

家具廠數控開料機加工材料的時候會遇到材料崩邊的問題&#xff0c;下面我們系統的分析下產生的原因以及解決的辦法產生崩邊現象的原因&#xff1f;其一是材料本身問題。目前除了實木家具&#xff0c;目前使用較多的就是 板式貼皮的材料&#xff0c;板材的優點就是標準化生產&am…

facade 門面 php,php設計模式之門面(Facade)模式

該模式屬于結構型模式什么是門面模式&#xff1f;定義&#xff1a;門面模式(有時候也稱為外觀模式)是指提供一個統一的接口去訪問多個子系統的多個不同的接口&#xff0c;它為子系統中的一組接口提供一個統一的高層接口。外部與子系統的通信是通過一個門面(Facade)對象進行。其…

架構師一般做到多少歲_《迷茫中的我們該如何突破瓶頸——成長為一名架構師》...

如何成長為一名架構師&#xff1f;架構師是一個既需要掌控整體又需要洞悉局部瓶頸并依據具體的業務場景給出解決方案的團隊領導型人物。一個架構師得需要足夠的想像力,能把各種目標需求進行不同維度的擴展&#xff0c;為目標客戶提供更為全面的需求清單。很多程序員想成為一名架…