UPDATE: 2023-05-20 20:40:39

ブログからの引っ越し記事。

はじめに

ここでは関数を作成する上で役立つ例外処理についてまとめておく。下記でまとめる予定だったのがやれてなかったので。

例外処理

関数を実行すると予期してないエラーが発生する。そのことを「例外」と呼んだりする。その例外をどのように扱うかうかを考えるのが例外処理。下記の関数を動かすと、エラーメッセージが出力される。エラーメッセージが出力された後のprint(3)は実行されず、そこで実行は終了となる。これがRでのエラーの挙動。

f <- function(){
  print(1)
  print(x) # Error
  print(3)
}

f()
[1] 1
 print(x) でエラー:  オブジェクト 'x' がありません 

意図的にエラーやワーニングを発生させるには、stop()warning()を利用する。warning()は実行を中断しない。

f <- function(){
  print(1)
  warning("Warning!!")
  print(2)
  stop("Error!! Stop!!")
  print(3)
}

f()
[1] 1
[1] 2
 f() でエラー: Error!! Stop!!
 追加情報:  警告メッセージ: 
 f() で:  Warning!!

このような場合にエラーが発生していることを出力しつつ、関数を実行したい場合に、try()を使う。

f <- function(x){
  print(x)
  stop("Error!! Stop!!")
}

for (i in 1:5) {
  f(x = i)
}
[1] 1
Error in f(x = i) : Error!! Stop!!

for (i in 1:5) {
  try(
    f(x = i)
  )
}

[1] 1
Error in f(x = i) : Error!! Stop!!
[1] 2
Error in f(x = i) : Error!! Stop!!
[1] 3
Error in f(x = i) : Error!! Stop!!
[1] 4
Error in f(x = i) : Error!! Stop!!
[1] 5
Error in f(x = i) : Error!! Stop!!

tryChach()を使うと、エラーが発生した際に詳細な情報をユーザーに提供できる。errorにはエラーが発生した時のハンドラを渡し、finallyにはtryChach()から戻る直前の式を渡すことができる。 エラーが無ければ問題なく関数が実行されるが、エラーの場合にtryChach()がある時とないときでは、出力に違いをもたせることができる。つまり、tryChach()はエラーオブジェクトをキャッチして返すもの。

f <- function(x){
  res <- 1 + x
  return(res)
}

tryCatch(expr = f(x = 10),
         error = function(x){ cat(gettext(x)); NA})
[1] 11

tryCatch(expr = f(x = "a"),
         error = function(x){ cat(gettext(x)); NA})
Error in 1 + x:  二項演算子の引数が数値ではありません 
[1] NA

f(x = "a")
 1 + x でエラー:  二項演算子の引数が数値ではありません 

 tryCatch(expr = f(x = "a"),
         error = function(x){"Result is Error, So Retrun this message"})
[1] "Result is Error, So Retrun this message"

この値の機能を改善したのが{rlang}にあるabort()warn()inform()とのこと。

これらの関数は関数が入れ子になっている際などに役立つ。Handling R errors the rlang wayの例を参考にする。下記は標準正規分布から1つ値をサンプリングして、マイナスならエラー、プラスならさらに10を足すという関数。

get_val <- function(){
  val <- rnorm(n = 1, mean = 0, sd = 1)
  if (val < 0){
    stop("Returns an error because `x` is negative.")
  } else {
    val
  }
}

plus <- function(plus_num) {
  x <- get_val()
  x + plus_num
}

エラーが出ないときはいいが、get_val()を実行すると、get_val()のエラーが返る一方で、plus()を実行するとget_val()のエラーが返ってきて、何が原因なのか、関数を作った人であればわかるかもしれないが、関数を利用する側は何がエラーなのかわからない。

 get_val()
[1] 2.138602

get_val()
[1] 1.117739

get_val()
get_val() でエラー: Returns an error because `x` is negative.

plus()
 get_val() でエラー: Returns an error because `x` is negative. 

その問題を{rlang}の関数で解決できる。{base}{rlang}の関数の対応関係は下記の通り。

rlang base
abort() stop()
warn() warning()
inform() message()

さきほどのget_val()の中身をabort()でエラー処理の部分を書き換える。abort()には下記を渡す。エラーの出力で返るのではなく、エラーオブジェクトの中身が詳細になる。

  • messagestop()と同じでエラーメッセージを渡す。
  • .subclass:エラーを区別するための条件のサブクラス。
  • val:エラーの原因となった特定の値。
library(rlang) # 0.4.2

get_val <- function(){
  val <- rnorm(n = 1, mean = 0, sd = 1)
  if (val < 0){
    rlang::abort(message = "Returns an error because `x` is negative.", 
                 .subclass ="get_val_error", 
                 val = val)
  } else {
    val
  }
}

エラーが出ないときは先程変わらないが、エラーが出たときは、エラーの詳細が追加されている。

  • message:“Returns an error because x is negative.” ←エラーメッセージ
  • val:num -1.61 ←実際の値
  • attr:get_val_error ←どこのエラーなのか
res <- tryCatch(error = function(x) x, get_val())
res
[1] 1.512988

res <- tryCatch(error = function(x) x, get_val())
res
<error/get_val_error>
  Returns an error because `x` is negative.
Backtrace:
1. base::tryCatch(error = function(x) x, get_val())
5. global::get_val()

str(res, max.level = 1)
List of 4
 $ message: chr "Returns an error because `x` is negative."
 $ trace  :List of 4
  ..- attr(*, "class")= chr "rlang_trace"
 $ parent : NULL
 $ val    : num -1.61
 - attr(*, "class")= chr [1:4] "get_val_error" "rlang_error" "error" "condition"

さらにget_val_errorの場合に、エラーを詳細にすることが可能。つまり、plus()の関数内部でtryCatch()を利用し、get_val_errorに対応するハンドラを定義する。

get_val_handler <- function(cnd) {
  msg <- "Can't calculate value"
  
  if (inherits(cnd, "get_val_error")) {
    msg <- paste0(msg, " as `val` passed to `get_val()` equals (", cnd$val,")")
  }
  
  rlang::abort(msg, "plus_val_error")
}

plus <- function(plus_num = 10) {
  x <- tryCatch(error = get_val_handler, get_val())
  x + plus_num
}

実行してみる。

 plus()
[1] 10.07435

plus()
 エラー: Can't calculate value as `val` passed to `get_val()` equals (-0.314411928757639)
Run `rlang::last_error()` to see where the error occurred. 

エラーオブジェクトを調べるとき、このエラーオブジェクトがどこの関数のエラーなのかなど、詳細を確認できる。

str(res, max.level = 1)
 num 10.5

res <- tryCatch(error = function(x) x, plus())
str(res, max.level = 1)
List of 3
 $ message: chr "Can't calculate value as `val` passed to `get_val()` equals (-0.812052099646003)"
 $ trace  :List of 4
  ..- attr(*, "class")= chr "rlang_trace"
 $ parent : NULL
 - attr(*, "class")= chr [1:4] "plus_val_error" "rlang_error" "error" "condition"

res
<error/plus_val_error>
Can't calculate value as `val` passed to `get_val()` equals (-0.812052099646003)
Backtrace:
 1. base::tryCatch(error = function(x) x, plus())
 5. global::plus()
 6. base::tryCatch(error = get_val_handler, get_val())
 7. base:::tryCatchList(expr, classes, parentenv, handlers)
 8. base:::tryCatchOne(expr, names, parentenv, handlers[[1L]])
 9. value[[3L]](cond)

このように関数を作成する際に、エラーハンドリングを適切に記述することで、利用するユーザーが困らなくて済むとのこと。