R:捕获所有分配的仪器功能

R: instrument function to capture all assignments

给定一个常规 R 函数 f,我希望能够创建一个新函数 f_debug,其作用与 f 相同,但让我跟踪所有在其中发生的对函数局部变量的赋值。

例如:

f <- function(x, y) {
  z <- x + y
  df <- data.frame(z=z)
  df
}

# This function doesn't work as intended - would like it to (in the case of `f` above)
# write out a list containing `z` and `df` to an RDS file
capturing <- function(func) {
  e <- new.env()
  altered <- function(...) {
    parent <- parent.frame()
    e <- something...(func, environment(), parent, etc., etc.)
    result <- func(...)
    saveRDS(as.list(e), 'foo.rds')
    result
  }
  environment(func) <- e
  altered
}

f_debug <- capturing(f)

我不确定我做这件事的知识差距是大还是小,有人有解决方案吗?

解决方案 1:窃取函数代码

这里的解决方案不是 return 捕获中间计算的新函数,而是在内部调用给定函数的代码。有一些限制,比如它可能只适用于命名参数。它不是将中间计算存储为 RDS,而是将它们作为属性附加。

capturing <- function(fun, ...) { 
  fun <- match.fun(fun)
  code <- body(fun)
  parent <- environment(fun)
  env <- new.env(parent = parent)
  for (val in names(list(...))) {
    env[[val]] <- list(...)[[val]]
  }
  result <- eval(code, envir = env, enclos = parent.frame())
  attr(result, "intermediate") <- env
  result
}

my_add <- function(x, y) {
  z <- x+y
  u <- x-y
  w <- x*y
  x + y
}

intermediates <- function(x) {
  attr(x, "intermediate", exact = TRUE)
}

value <- capturing(my_add, x = 1, y = 7)
ls(envir = intermediates(value))
#> [1] "u" "w" "x" "y" "z"
intermediates(value)$x
#> [1] 1
# Created on 2022-02-08 by the reprex package (v2.0.1)

方案二:修改函数代码

此解决方案的一个弱点是,如果所选函数具有对 on.exit(add=FALSE) 的调用,则需要做一些额外的工作来修改函数,以便捕获内部环境。但是,当函数接受 ... 个参数时它确实有效。

my_add <- function(x, y) {
  z <- x+y
  u <- x-y
  w <- x*y
  x + y
}

insert_capture <- function(code) {
  # `<<-` assigns into the global environment if no variable of the given name is found
  # while traveling up to the global environment. If you need this assignment to go elsewhere,
  # I'd recommend passing in `assign()`. Of course, you could also modify the `on.exit()`
  # to use saveRDS.
  parse(text=append(deparse(code), 
                            "on.exit(._last_capture <<- environment(), add = TRUE)",
                            after = 1L))
}
capturing2 <- function(fun) {
  fun <- match.fun(fun)
  code <- insert_capture(body(fun))
  body(fun) <- code
  fun 
}

my_add2 <- capturing2(my_add)

my_add2(1, 7)
#> [1] 8
ls(envir = ._last_capture)
#> [1] "u" "w" "x" "y" "z"
._last_capture$u
#> [1] -6

reprex package (v2.0.1)

于 2022-02-08 创建

您所描述的内容已经在 utils::dump.frames 基础 R 中以更复杂的方式实现。它将调用堆栈中与每个调用关联的帧(环境)保存到 class "dump.frames" 的对象中,您可以使用 utils::debugger 追溯探索,就好像您实际上 运行 ] 你的代码在调试器下。

capturing <- function(func, ...) {
    cc <- as.call(c(quote(utils::dump.frames), list(...)))
    cc <- call("on.exit", cc, add = TRUE)
    body(func) <- call("{", cc, body(func))
    func
}

capturing 将调用 on.exit(utils::dump.frames(...), add = TRUE) 注入到 func 和 returns 修改函数的主体中。 这里,...dump.frames:

的参数列表
  • dumpto,一个字符串,给出要用于 "dump.frames" 对象的名称
  • to.file,一个逻辑标志,指示 "dump.frames" 对象是否应分配到全局环境中或 save-ed 到当前工作目录中的 paste0(dumpto, ".rda")
  • include.GlobalEnv,一个逻辑标志,指示是否也应保存全局环境

一个简单的例子,你应该自己试试:

tmp <- tempfile()
dir.create(tmp)
cwd <- setwd(tmp)

f <- function(x, y) {
    z <- x + y
    z + 1
}
g <- capturing(f, dumpto = "zzz", to.file = TRUE)
h <- function(a, b) {
    d <- g(a, b)
    d + 1
}
h12 <- h(1, 2)

load("zzz.rda")
zzz
## $`h(1, 2)`
## <environment: 0x14c16cb58>
## 
## $`#2: g(a, b)`
## <environment: 0x14c16ca40>
## 
## attr(,"error.message")
## [1] ""
## attr(,"class")
## [1] "dump.frames"

ls(zzz[[1L]])
## [1] "a" "b"

ls(zzz[[2L]])
## [1] "z" "x" "y"

utils::debugger(zzz)
## Message:  Available environments had calls:
## 1: h(1, 2)
## 2: #2: g(a, b)
## 
## Enter an environment number, or 0 to exit  
## Selection: 2
## Browsing in the environment with call:
##    #2: g(a, b)
## Called from: debugger.look(ind)
## Browse[1]> ls()
## [1] "x" "y" "z"
## Browse[1]> x == 1 && y == 2 && z == x + y
## [1] TRUE
## Browse[1]> Q

setwd(cwd)
unlink(tmp, recursive = TRUE)

如果您不熟悉 R 的环境浏览器,请参阅 ?browser

我的 capturing 函数有一个限制,即 on.exitfunc 的主体中调用也必须使用 add = TRUE。如果你自己写了func,那完全没有什么限制,通过add = TRUE也是一个好习惯。

最终,没有完全安全的方法将代码注入函数,但是,在交互式设置中,我认为这种“不安全”程度是可以的。