深入對比資料科學工具箱:Python 和 R 的異常處理機制

發表於2016-08-09

概述

異常處理,是程式語言或計算機硬體裡的一種機制,用於處理軟體或資訊系統中出現的異常狀況(即超出程式正常執行流程的某些特殊條件)。Python和R作為一門程式語言自然也是有各自的異常處理機制的,異常處理機制在程式碼編寫中扮演著非常關鍵的角色,卻又是許多人容易混淆的地方。對於異常機制的合理運用是直接關係到碼農飯碗的事情!所以,本文將具體介紹一下Python和R的異常處理機制,闡明二者在異常處理機制上的異同。

異常安全

在瞭解Python和R的異常機制之前,我們有必要了解一下異常安全的概念。

根據WikiPedia的文獻,一段程式碼是異常安全的,如果這段程式碼執行時的失敗不會產生有害後果,如記憶體洩露、儲存資料混淆、或無效的輸出。我們可以知道一段程式碼的異常安全通常分為下面五類:

異常安全通常分為5個層次:

  1. 失敗透明:如果出現了異常,將不會對外進一步丟擲該異常。(一般比較複雜)
  2. 強異常安全:可以執行失敗,不過資料會回滾到程式碼執行前(無副作用)
  3. 基本異常安全:執行失敗導致的資料變更,使得程式碼執行前後資料不一致了(有副作用)
  4. 最小異常安全:執行失敗儲存了無效資料,但是還不會引起崩潰,資源不會洩露(程式不會掛)
  5. 異常不安全:沒有任何保證(程式可能會掛掉)

從上述的5個層次來看,我們可以知道,在平時寫程式碼的時候,對資料庫、檔案、網路等的IO操作都是需要儘量保證無副作用的,也就是強異常安全。具體來說就是,RDBS操作在失敗的時候需要回滾機制、所有IO操作在最後要保證IO連線資源關閉。

其實和多數語言的異常機制的語法是類似的:Python和R都是通過丟擲一個異常物件或一個列舉類的值來返回一個異常;異常處理程式碼的作用域由try開始,以第一個異常處理子句(catch, except等)結束;可連續出現若干個異常處理子句,每個處理特定型別的異常。最後通過finally子句,無論是否出現異常它都將執行,用於釋放異常處理所需的一些資源。

下面將具體介紹二者的異常處理機制。

Python 中的異常處理機制

深入對比資料科學工具箱:Python 和 R 的異常處理機制

首先,Python 是一門物件導向語言,所有的異常類都是通過繼承BaseException類來實現的,我們亦可以通過相應的繼承來實現自定義的異常類,比如在工作流排程中使用AirflowException,具體實現可以直接看Airflow的原始碼。

事實上,這些在我們程式碼處理範圍內的異常其實就是可以分成兩個部分:

  1. IO異常:由網路抖動、磁碟檔案位置變更、資料庫連線變更等引起的IO異常問題。
  2. 執行期異常:由於計算或者傳輸的引數引數型別有誤、引數值異常等等發生在執行期的異常,都統一被稱為執行期異常。正常來說,IO上的異常我們都要有相應的try-catch-finally機制,在Python也就是如下實現:
try:
   do something with IO
except:
   do something without IO
finally:
   close IO

這裡容易犯的一個錯誤就是在except中又引入了新的IO操作,比如在except中又引入了一個API的POST請求或者資料庫寫操作等等,這樣如果在except階段又發生了異常,將導致異常資訊的丟失。

另一方面,對於可能的執行期異常則需要我們根據具體應用場景的需求來做相應的處理,一般就是遇到一個新的問題加一個新的異常捕獲機制,當然這裡也就考驗到碼農程式設計的功利,是否能夠未雨綢繆。比如陣列長度的檢查,傳入字典的Key檢查等等。Python本身提供了豐富的異常處理型別並且易於擴充,正確使用將可以顯著提升程式的魯棒性(保住碼農的飯碗)。

使用try-catch-finally機制是足夠簡單的,但是在混入returnrasie操作之後,事情就看起來變得有點複雜。

舉一個例子:

def test():
    try:
        a = 1/0
    except:
        a = 0
        raise(ValueError,"value error, the division must greater than 0")
        return a
    finally:
        a = 1
        return a
test()

你看這裡的返回應該是什麼呢?

其實,這裡的返回最後應該是 1,而except中raise的異常則會被吃掉。這也是許多人錯誤使用finanlly的一個很好的例子。

Python在執行帶有fianlly的子句時會將except內丟擲的物件先快取起來,優先執行finally中丟擲的物件,如果finally中先丟擲了return或者raise,那麼except段丟擲的物件將看起來被吃掉了。

一個段正確的處理方式應該是這樣的:

try:
    do IO
    info = {"status":200}
except:
    info = {"status":400}
finally:
    try:
        write log(info)
    except:
        raise(SomeError,"error message")
    close IO

具體的呼叫棧的過程可以參考這個更加生動的例子:

R 中的異常處理機制

R和Python最大的不同就是 R 本質上是一門強動態型別的非純函數語言程式設計語言(所謂非純即存在副作用)而非面嚮物件語言。從函數語言程式設計語言的角度上講,R和Erlang、LISP的關係比較近一些。

既然是函式式語言,處理異常也是通過函式式的,而非直接通過物件導向的方式。R 從語法上來看就略顯突兀(花括號函式式語言的一大通病):

tryCatch({
  doStuff()
  doMoreStuff()
}, some_exception = function(se) {
  recover(se)
})

如果這段用Python來表達就變成:

try:
  doStuff()
  doMoreStuff()
except SomeException, se:
  recover(se)

事實上正確運用 R 的異常處理機制反而是比較負擔小的一種方式:(R 還支援用中文字符集命名變數)

tryCatch({
  結果 

下面是 Hadley 大神對R的異常處理機制優點的分析

One of R’s great features is its condition system. It serves a similar purpose to the exception handling systems in Java, Python, and C++ but is more flexible. In fact, its flexibility extends beyond error handling–conditions are more general than exceptions in that a condition can represent any occurrence during a program’s execution that may be of interest to code at different levels on the call stack. For example, in the section “Other Uses for Conditions,” you’ll see that conditions can be used to emit warnings without disrupting execution of the code that emits the warning while allowing code higher on the call stack to control whether the warning message is printed. For the time being, however, I’ll focus on error handling.

The condition system is more flexible than exception systems because instead of providing a two-part division between the code that signals an error and the code that handles it, the condition system splits the responsibilities into three parts–signaling a condition, handling it, and restarting. In this chapter, I’ll describe how you could use conditions in part of a hypothetical application for analyzing log files. You’ll see how you could use the condition system to allow a low-level function to detect a problem while parsing a log file and signal an error, to allow mid-level code to provide several possible ways of recovering from such an error, and to allow code at the highest level of the application to define a policy for choosing which recovery strategy to use.

我的理解是R通過條件機制,然我們可以選擇性的在低階函式中把warning吃掉,這樣就不至於影響高階函式的執行?條件機制將異常分為三階段而不是兩階段:

1.異常訊號捕獲
2.異常處理
3.重啟機制。

並且我們還可以看到在異常處理中,如何在中階函式中恢復低階函式的Error,並且在高階函式中選擇一定的恢復策略。

這段貌似個人理解有誤,還請看官指正。

參考資料

相關文章