為那些還不清楚它的人,Python的assert是用來檢查壹個條件,如果它為真,就不做任何事。如果它為假,則會拋出AssertError並且包含錯誤信息。例如:
py> x = 23
py> assert x > 0, "x is not zero or negative"
py> assert x%2 == 0, "x is not an even number"
Traceback (most recent call last):
File "", line 1, in
AssertionError: x is not an even number
很多人用assert作為壹個很快和容易的方法來在參數錯誤的時候拋出異常。但這樣做是錯的,非常錯誤,有兩個原因。首先AssertError不是在測試參數時應該拋出的錯誤。妳不應該像這樣寫代碼:
if not isinstance(x, int):
raise AssertionError("not an int")
妳應該拋出TypeError的錯誤,assert會拋出錯誤的異常。
但是,更危險的是,有壹個關於assert的困擾:它可以被編譯好然後從來不執行,如果妳用 –O 或 –oo
選項運行Python,結果不保證assert表達式會運行到。當適當的使用assert時,這是未來,但是當assert不恰當的使用時,它會讓代碼用
-O執行時出錯。
那什麽時候應該使用assert?沒有特定的規則,斷言應該用於:
防禦型的編程
運行時檢查程序邏輯
檢查約定
程序常量
檢查文檔
(在測試代碼的時候使用斷言也是可接受的,是壹種很方便的單元測試方法,妳接受這些測試在用-O標誌運行時不會做任何事。我有時在代碼裏使用
assert False來標記沒有寫完的代碼分支,我希望這些代碼運行失敗。盡管拋出NotImplementedError可能會更好。)
關於斷言的意見有很多,因為它能確保代碼的正確性。如果妳確定代碼是正確的,那麽就沒有用斷言的必要了,因為他們從來不會運行失敗,妳可以直接移除這些斷言。如果妳確定檢查會失敗,那麽如果妳不用斷言,代碼就會通過編譯並忽略妳的檢查。
在以上兩種情況下會很有意思,當妳比較肯定代碼但是不是絕對肯定時。可能妳會錯過壹些非常古怪的情況。在這個情況下,額外的運行時檢查能幫妳確保任何錯誤都會盡早地被捕捉到。
另壹個好的使用斷言的方式是檢查程序的不變量。壹個不變量是壹些妳需要依賴它為真的情況,除非壹個bug導致它為假。如果有bug,最好能夠盡早發現,所以我們為它進行壹個測試,但是又不想減慢代碼運行速度。所以就用斷言,因為它能在開發時打開,在產品階段關閉。
壹個非變量的例子可能是,如果妳的函數希望在它開始時有數據庫的連接,並且承諾在它返回的時候仍然保持連接,這就是函數的不變量:
Python
def some_function(arg):
assert not DB.closed()
... # code goes here
assert not DB.closed()
return result
斷言本身就是很好的註釋,勝過妳直接寫註釋:
# when we reach here, we know that n > 2
妳可以通過添加斷言來確保它:
assert n > 2
斷言也是壹種防禦型編程。妳不是讓妳的代碼防禦現在的錯誤,而是防止在代碼修改後引發的錯誤。理想情況下,單元測試可以完成這樣的工作,可是需要面
對的現實是,它們通常是沒有完成的。人們可能在提交代碼前會忘了運行測試代碼。有壹個內部檢查是另壹個阻擋錯誤的防線,尤其是那些不明顯的錯誤,卻導致了
代碼出問題並且返回錯誤的結果。
加入妳有壹些if…elif 的語句塊,妳知道在這之前壹些需要有壹些值:
# target is expected to be one of x, y, or z, and nothing else.
if target == x:
run_x_code()
elif target == y:
run_y_code()
else:
run_z_code()
假設代碼現在是完全正確的。但它會壹直是正確的嗎?依賴的修改,代碼的修改。如果依賴修改成 target = w
會發生什麽,會關系到run_w_code函數嗎?如果我們改變了代碼,但沒有修改這裏的代碼,可能會導致錯誤的調用 run_z_code
函數並引發錯誤。用防禦型的方法來寫代碼會很好,它能讓代碼運行正確,或者立馬執行錯誤,即使妳在未來對它進行了修改。
在代碼開頭的註釋很好的壹步,但是人們經常懶得讀或者更新註釋。壹旦發生這種情況,註釋會變得沒用。但有了斷言,我可以同時對代碼塊的假設書寫文檔,並且在它們違反的時候觸發壹個幹凈的錯誤
assert target in (x, y, z)
if target == x:
run_x_code()
elif target == y:
run_y_code()
else:
assert target == z
run_z_code()
這樣,斷言是壹種防禦型編程,同時也是壹種文檔。我想到壹個更好的方案:
if target == x:
run_x_code()
elif target == y:
run_y_code()
elif target == z:
run_z_code()
else:
# This can never happen. But just in case it does...
raise RuntimeError("an unexpected error occurred")
按約定進行設計是斷言的另壹個好的用途。我們想象函數與調用者之間有個約定,比如下面的:
“如果妳傳給我壹個非空字符串,我保證傳會字符串的第壹個字母並將其大寫。”
如果約定被函數或調用這破壞,代碼就會出問題。我們說函數有壹些前置條件和後置條件,所以函數就會這麽寫:
def first_upper(astring):
assert isinstance(astring, str) and len(astring) > 0
result = astring[0].upper()
assert isinstance(result, str) and len(result) == 1
assert result == result.upper()
return result
按約定設計的目標是為了正確的編程,前置條件和後置條件是需要保持的。這是斷言的典型應用場景,因為壹旦我們發布了沒有問題的代碼到產品中,程序會是正確的,並且我們能安全的移除檢查。
下面是建議的不要用斷言的場景:
不要用它測試用戶提供的數據
不要用斷言來檢查妳覺得在妳的程序的常規使用時會出錯的地方。斷言是用來檢查非常罕見的問題。妳的用戶不應該看到任何斷言錯誤,如果他們看到了,這是壹個bug,修復它。
有的情況下,不用斷言是因為它比精確的檢查要短,它不應該是懶碼農的偷懶方式。
不要用它來檢查對公***庫的輸入參數,因為它不能控制調用者,所以不能保證調用者會不會打破雙方的約定。
不要為妳覺得可以恢復的錯誤用斷言。換句話說,不用改在產品代碼裏捕捉到斷言錯誤。
不要用太多斷言以至於讓代碼很晦澀。