在 R 中捕获警告,而无需重复计算或文件连接

Catching a warning without repeated evaluation or file connections in R

提问人:Andreï V. Kostyrka 提问时间:3/23/2023 最后编辑:Andreï V. Kostyrka 更新时间:3/24/2023 访问量:67

问:

假设有一个挑剔的函数,其评估成本很高。它可能是一种优化算法,如果它终止时没有错误但未通过某些检查(例如,达到.max.迭代),则可能会引发警告。我想保存输出和警告(如果有),因为这些警告包含有价值的信息。f()f()

这些解决方案依赖于第二次调用函数或使用 。坦率地说,网上的文档或示例太少了,我无法通过查看定义来理解这个函数的作用。withCallingHandlerswithCallingHandlers

该解决方案依赖于创建一个临时文件,然后从中读取行。问题是,我试图从数百万次调用中捕获警告(函数内部外观的小优化问题,它被调用数百万次,如果不是数十亿次),因此每次创建一次可能是不稳定的(这些函数是并行调用的)。purrr::capture_output()file()

下面是一个 MWE:有一个慢速函数(1 秒),可能会或可能不会抛出警告,捕获警告需要第二次评估(下面的代码生成 2 个警告和 2 个无警告结果 = 2 + 2*2 = 6 秒):

f <- function(i) {
  Sys.sleep(1)
  if (i < 3) warning(paste0("i is too small, took ", sample(3:10, 1),
                            " iterations, code ", sample(2:5, 1), "."))
  return(i)
}

set.seed(1)
f(1) # Warning
f(6) # No warning

saveWarnSlow <- function(i) {
  tryCatch(f(i), warning = function(w) list(output = f(i), warning = w))
}
system.time(out <- lapply(1:4, saveWarnSlow)) # Runs for 6 seconds
sapply(out, function(x) if (length(x) == 1) "OK" else as.character(x$warning))

# [1] "simpleWarning in f(i): i is too small, took 9 iterations, code 2.\n"
# [2] "simpleWarning in f(i): i is too small, took 9 iterations, code 4.\n"
# [3] "OK"                                                         
# [4] "OK"  

也许这是由于我对病情处理和恢复的了解不足,但是......ON如何摆脱重复的功能求值来保存警告?

R 异常 事件处理 警告

评论


答:

2赞 user2554330 3/23/2023 #1

我认为你不能用 ,但你可以用 .这个想法是将警告消息保存在调用方可见的某个位置,然后将警告附加到结果中。tryCatchwithCallingHandlers

f <- function(i) {
  Sys.sleep(1)
  if (i < 3) warning(paste0("i is too small, took ", sample(3:10, 1),
                            " iterations, code ", sample(2:5, 1), "."))
  return(i)
}

set.seed(1)

f(1) # Warning, but it won't be saved
#> Warning in f(1): i is too small, took 3 iterations, code 5.
#> [1] 1
f(6) # No warning
#> [1] 6

saveWarnSlow <- function(i) {
  thewarning <- ""
  
  wrapper <- function(value) {
    # Lazy evaluation means value hasn't been computed yet.
    thewarning <<- ""
    list(value = value, warning = thewarning)
  }
  
  withCallingHandlers(wrapper(f(i)),
    warning = 
      function(w) {
        thewarning <<- conditionMessage(w)
        invokeRestart("muffleWarning")
      })
}
system.time(out <- lapply(1:4, saveWarnSlow)) # Runs for 4 seconds
#>    user  system elapsed 
#>   0.013   0.000   4.024
out
#> [[1]]
#> [[1]]$value
#> [1] 1
#> 
#> [[1]]$warning
#> [1] "i is too small, took 9 iterations, code 2."
#> 
#> 
#> [[2]]
#> [[2]]$value
#> [1] 2
#> 
#> [[2]]$warning
#> [1] "i is too small, took 4 iterations, code 2."
#> 
#> 
#> [[3]]
#> [[3]]$value
#> [1] 3
#> 
#> [[3]]$warning
#> [1] ""
#> 
#> 
#> [[4]]
#> [[4]]$value
#> [1] 4
#> 
#> [[4]]$warning
#> [1] ""

创建于 2023-03-23,使用 reprex v2.0.2

2赞 Mikael Jagan 3/23/2023 #2

我不确定如何回答您的问题,除非更详细地解释使用 and 的方法(您和@user2554330已经提到过)。withCallingHandlersinvokeRestart

在不失去普遍性的情况下,我们可以将注意力限制在 而不是 发出的警告上。后一种情况在概念上是相同的,但它涉及一层 C 代码(用于将消息组件粘贴在一起),这对于理解条件处理并不重要。warning(<condition>)warning(<character>)

w.list <- list()
f <- function() { 
    w <- simpleWarning("hello", sys.call())
    for (i in 1:3) warning(w)
    0
}
h <- function(w) { 
    w.list <<- c(w.list, list(w))
    invokeRestart("muffleWarning")
}
list(value = withCallingHandlers(f(), warning = h), warnings = w.list)
$value
[1] 0

$warnings
$warnings[[1]]
<simpleWarning in f(): hello>

$warnings[[2]]
<simpleWarning in f(): hello>

$warnings[[3]]
<simpleWarning in f(): hello>


withCallingHandlers乍一看与.两者都将处理程序推送到处理程序堆栈上,计算表达式,并捕获它们为其找到处理程序的信号条件。当处理程序捕获条件时,在堆栈上清除该处理程序及其上方的处理程序,然后使用捕获的条件作为参数来评估对处理程序的调用。tryCatch

主要区别在于捕获条件时将控制权转移到的上下文。

当 建立的处理程序捕获条件时,控制权将转移到调用的上下文中。计算 halts,使用捕获条件作为参数调用处理程序,并返回此处理程序调用的值。如果不重新评估,就无法恢复评估,因为其第一次评估的背景已经“丢失”。tryCatch(expr, ...)tryCatchexprtryCatchexpr

当 的处理程序捕获条件时,控件“保留”在发出条件信号的上下文中,在对 、 、 或 的调用中(在调用堆栈中的某个位置与 的计算相关联)。在 的情况下,该上下文就在这里,在这两个调用之间:withCallingHandlers(expr, ...)signalConditionmessagewarningstopexprwarning.Internal

> body(warning)
## ... TRUNCATED ...
        withRestarts({
            .Internal(.signalCondition(cond, message, call))
            .Internal(.dfltWarn(message, call))
        }, muffleWarning = function() NULL)
        invisible(message)
## ... TRUNCATED ...

使用捕获条件作为参数调用处理程序,但返回此处理程序调用的值。处理程序返回后,将依次尝试任何剩余的处理程序,直到没有剩余的处理程序。最后,对简历进行评估并返回 的值。在 的情况下,使用上面的第二个调用评估简历,它打印警告消息和呼叫。withCallingHandlersexprwithCallingHandlersexprwarningexpr.Internal

因此,在此示例中有两个目的:invokeRestart("muffleWarning")

  1. 它停止处理程序调用的序列,以便忽略 建立的任何剩余处理程序。(嗯,这里没有。withCallingHandlers
  2. 它“跳过”上面的第二个调用,因此不会打印警告消息和调用。.Internal

它通过将控制权转移到建立处理程序的调用来实现此目的。 返回调用不带参数的处理程序的结果(即 ),返回 和 继续的计算结果。withRestartsmuffleWarningwithRestartsmuffleWarningNULLwarninginvisible(message)expr

如果更清楚地记录 and 处理程序,那就太好了。目前,您必须查看 的主体,并查看它们是否存在并且它们只是返回 .help("conditions")muffleMessagemuffleWarningmessagewarningNULL