使用`withCallingHandlers`捕获任意条件

时间:2013-12-13 17:18:43

标签: r exception-handling try-catch

问题

我正在尝试编写一个函数来评估代码并存储结果,包括代码中发出的任何可能的条件。我的工作完全正常,除了我的函数(让我们称之为evalcapt)在错误处理表达式中运行的情况。

问题是withCallingHandlers将继续寻找匹配的条件处理程序,如果有人在我的函数之外定义了这样的处理程序,我的函数将失去对执行的控制。以下是问题的简化示例:

evalcapt <- function(expr) {
  conds <- list()
  withCallingHandlers(
    val <- eval(expr),
    condition=function(e) {
      message("Caught condition of class ", deparse(class(e)))
      conds <<- c(conds, list(e))
  } )
  list(val=val, conditions=conds)
}

myCondition <- simpleCondition("this is a custom condition")
class(myCondition) <- c("custom", class(myCondition))
expr <- expression(signalCondition(myCondition), 25)

tryCatch(evalcapt(expr))          

按预期工作

Caught condition of class c("custom", "simpleCondition", "condition")
$val
[1] 25

$conditions
$conditions[[1]]
<custom: this is a custom condition>

但:

tryCatch(
  evalcapt(expr),               
  custom=function(e) stop("Hijacked `evalcapt`!")  
)

不起作用:

Caught condition of class c("custom", "simpleCondition", "condition")
Error in value[[3L]](cond) : Hijacked `evalcapt`!

我不知道如何实施的解决方案

我真正需要的是一种在代码中发出条件信号后立即定义重启的方法,坦率地说就是withCallingHandlers看起来正常工作的方式(当我的处理程序是最后一个可用的处理程序时),但我我在处理函数中browse时看不到重新启动,并使用computeRestarts

看起来像无法解决的解决方案

使用tryCatch

tryCatchwithCallingHandlers没有相同的问题,因为它在找到第一个处理程序后不会继续查找处理程序。最大的问题是它还没有继续评估条件后的代码。如果您查看上面有效的示例,但tryCatchwithCallingHandlers,则不会返回值(25),因为执行将在条件之后返回到tryCatch帧处理完毕。

所以基本上,我正在寻找tryCatchwithCallingHandlers之间的混合,一个将控制权返回给条件信号器,但也会在第一个之后停止寻找更多的处理程序找到。

将表达式分解为子表达式,然后使用tryCatch

好的,但是你如何分手(以及更复杂的功能以及遍布信号的条件):

fun <- function(myCondition) {
  signalCondition(myCondition)
  25
}
expr <- expression(fun())

其它

我查找了与.Internal(.signalCondition())调用关联的源代码,看看我是否可以弄清楚是否有幕后重启设置,但我不在那里。好像是:

    void R_ReturnOrRestart(SEXP val, SEXP env, Rboolean restart)
    {
        int mask;
        RCNTXT *c;

        mask = CTXT_BROWSER | CTXT_FUNCTION;

        for (c = R_GlobalContext; c; c = c->nextcontext) {
        if (c->callflag & mask && c->cloenv == env)
            findcontext(mask, env, val);
        else if (restart && IS_RESTART_BIT_SET(c->callflag))
            findcontext(CTXT_RESTART, c->cloenv, R_RestartToken);
        else if (c->callflag == CTXT_TOPLEVEL)
            error(_("No function to return from, jumping to top level"));
        }
    }
来自src / main / errors.c的

正在进行一些重启调用,这由do_signalCondition调用,但我不知道如何搞乱这个。

4 个答案:

答案 0 :(得分:5)

我认为您要找的是在发出特殊情况时使用withRestarts,例如来自warning

    withRestarts({
        .Internal(.signalCondition(cond, message, call))
        .Internal(.dfltWarn(message, call))
    }, muffleWarning = function() NULL)

所以

evalcapt <- function(expr) {
  conds <- list()
  withCallingHandlers(
    val <- eval(expr),
    custom=function(e) {
      message("Caught condition of class ", deparse(class(e)))
      conds <<- c(conds, list(e))
      invokeRestart("muffleCustom")
  } )
  list(val=val, conditions=conds)
}

expr <- expression(withRestarts({
    signalCondition(myCondition)
}, muffleCustom=function() NULL), 25)

导致

> tryCatch(evalcapt(expr))   
Caught condition of class c("custom", "simpleCondition", "condition")
$val
[1] 25

$conditions
$conditions[[1]]
<custom: this is a custom condition>


> tryCatch(
+   evalcapt(expr),               
+   custom=function(e) stop("Hijacked `evalcapt`!")  
+ )
Caught condition of class c("custom", "simpleCondition", "condition")
$val
[1] 25

$conditions
$conditions[[1]]
<custom: this is a custom condition>

答案 1 :(得分:2)

据我所知,并不能解决这个问题(我很高兴被证明是错误的)。如果我们查看tryCatchwithCallingHandlers如何注册处理程序,可以看到问题的根源:

.Internal(.addCondHands(name, list(handler), parentenv, environment(), FALSE)) # tryCatch
.Internal(.addCondHands(classes, handlers, parentenv, NULL, TRUE)) # withCallingHandlers

关键点是最后一个参数,tryCatch中为FALSE,withCallingHandlers中为TRUE。此参数导致do_addCondHands&gt; gp bit设置{{3}} src / main / errors.c中的mkHandlerEntry

当发出条件信号时,do_signalCondition(仍在src / main / errors.c中)会查询相同的位:

// simplified code excerpt from `do_signalCondition

PROTECT(oldstack = R_HandlerStack);
while ((list = findConditionHandler(cond)) != R_NilValue) {
  SEXP entry = CAR(list);
  R_HandlerStack = CDR(list);
  if (IS_CALLING_ENTRY(entry)) {   // <<------------- Consult GP bit
    ... // Evaluate handler
  } else gotoExitingHandler(cond, ecall, entry);   // Evaluate handler and exit
}
R_HandlerStack = oldstack;
return R_NilValue;

基本上,如果设置了GP位,那么我们会评估处理程序,并继续迭代处理程序堆栈。如果没有设置,那么我们运行运行处理程序的gotExitingHandler,然后将控制返回到处理控制结构,而不是恢复发出条件信号的代码。

由于GP位只能告诉您执行以下两种操作之一,因此没有直接的方法来修改此调用的行为(即,如果使用withCallingHandlers,则要么遍历所有处理程序,要么停止在由tryCatch注册的第一个匹配的那个。

我玩弄了trace signalConditions {{1}}的想法,在那里添加了一个重启,但这似乎过于苛刻。

答案 2 :(得分:1)

使用一点C,您可以评估ToplevelExec()中的表达式,以将其与堆栈中注册的所有处理程序隔离开来。

我们将在下一个rlang版本中将它暴露在R级别。

答案 3 :(得分:0)

我可能会迟到一些,但是我也一直在研究条件系统,我想我已经找到了其他解决方案。

但是首先:这一定是一个难题的一些原因,而不是通常可以轻松解决的问题。
问题是哪个函数正在发出条件信号,并且如果该函数引发条件,该函数是否可以继续执行。错误也被实现为“只是一个条件”,但是大多数函数在抛出stop()之后都不希望继续执行。
而且某些功能可能会通过某种条件,希望不会再被它困扰。
通常,这意味着只有在函数明确表示可以接受时,才可以在停止后返回控件:提供重新启动。 可能还有其他严重的情况可以发出信号,如果函数希望总是捕获到这种情况,而您强迫它返回执行,则情况将严重恶化。
当您按照以下方式编写并恢复执行该怎么办?

myfun <- function(deleteFiles=NULL) {
  if (!all(haveRights(deleteFiles))) stop("Access denied")
  file.remove(deleteFiles)
}
withCallingHandlers(val <- eval(myfun(myProtectedFiles)),
  error=function(e) message("I'm just going to ignore everything..."))

如果没有调用其他处理程序(警告用户已停止调用),则即使该函数对此有(小的)防护措施,文件也将被删除。 在发生错误的情况下,这很明显,但在其他情况下也可能会发生情况,因此,我认为这是如果您停止传递条件,则R并不真正支持它的主要原因,除非这意味着停止。 >

尽管如此,我认为我发现了两种解决您的问题的方法。 首先是简单地逐步执行 expr ,这与Martin Morgans解决方案非常接近,但是将withRestarts移入了函数中:

evalcapt <- function(expr) {
  conds <- list()
  for (i in seq_along(expr)) {
    withCallingHandlers(
      val <- withRestarts(
        eval(expr[[i]]),
        muffleCustom = function()
          NULL
      ),
      custom = function(e) {
        message("Caught condition of class ", deparse(class(e)))
        conds <<- c(conds, list(e))
        invokeRestart(findRestart("muffleCustom"))
      })
  }
  list(val = val, conditions = conds)
}

主要缺点是它不能挖掘功能, expr 会在调用的每个级别上执行。
因此,如果调用evalcapt(myfun()),则for循环会将其视为一条指令。而这一条指令引发了一个条件->因此不会返回->因此,如果您没有捕获任何内容,就看不到任何输出。 OTOH,evalcapt(expression(signalCondition(myCondition),25))按要求工作,因为这是一个包含2个元素的表达式,每个元素都被调用。

如果您想成为铁杆,我认为您可以尝试逐步评估myfun(),但始终存在您想要深入的问题。如果myfun()调用了myotherfun(),又调用了myotherotherfun(),您是否想将控制权返回到myfun失败,myotherfun或myotherotherfun失败的地步?
基本上,这只是您要停止执行的级别以及要在何处恢复的猜测。

第二种解决方案:劫持对signalCondition的所有调用。这意味着您可能会进入一个很深的层次,尽管不是最深层次的层次(没有基元或调用.signalCondition的代码)。
我认为,如果您真的确定仅由您编写的代码抛出自定义条件,这将是最好的方法:这意味着执行直接在signalCondition之后执行。
这给了我这个功能:

evalcapt <- function(expr) {
  if(exists('conds', parent.frame(), inherits=FALSE)) {
    conds_backup <- get('conds', parent.frame(), inherits=FALSE)
    on.exit(assign('conds', conds_backup, parent.frame(), inherits=FALSE), add=TRUE)
  } else {
    on.exit(rm('conds', pos=parent.frame(), inherits=FALSE), add=TRUE)
  }
  assign('conds', list(), parent.frame(), inherits=FALSE)
  origsignalCondition <- signalCondition
  if(exists('signalCondition', parent.frame(), inherits=FALSE)) {
    signal_backup <- get('signalCondition', parent.frame(), inherits=FALSE)
    on.exit(assign('signalCondition', signal_backup, parent.frame(), inherits=FALSE), add=TRUE)
  } else {
    on.exit(rm('signalCondition', pos=parent.frame(), inherits=FALSE), add=TRUE)
  }
  assign('signalCondition', function(e) {
    if(is(e, 'custom')) {
      message("Caught condition of class ", deparse(class(e)))
      conds <<- c(conds, list(e))
    } else {
      origsignalCondition(e)
    }
  }, parent.frame())
  val <- eval(expr, parent.frame())
  list(val=val, conditions=conds)
}

它看起来更混乱,但这主要是因为使用哪种环境存在更多问题。区别在于,在这里,我将调用环境用作上下文,并劫持了也需要存在的signalCondition()。然后我们需要清理。
但是主要用途是覆盖signalCondition:如果看到自定义错误,则将其记录下来并返回控制。如果是其他情况,我们会继续控制。

这里可能存在一些较小的缺点:

  • 您可能会遇到更深层的功能,该错误就是myfun调用myotherfun的方式,但是您最终遇到了myotherfun(或更深层)。
  • 它仅捕获调用signalCondition的事件。如果您致电例如警告(myCondition),什么也没发现。
  • 如果另一个程序包/另一个环境中的函数调用signalCondition,则它使用其自己的搜索路径,这意味着我们的signalCondition可能会被绕过,而使用base :: signalCondition代替。
  • 调试时,它非常难看。在不期望变量的环境中分配变量(然后在退出函数时消失),不同函数的范围可能不清楚,parent.frame()可能会给您其他结果,以此类推,等等。< / li>
  • 并且如前所述:引发条件后,所有函数都必须能够处理重入。