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.exit
在 func
的主体中调用也必须使用 add = TRUE
。如果你自己写了func
,那完全没有什么限制,通过add = TRUE
也是一个好习惯。
最终,没有完全安全的方法将代码注入函数,但是,在交互式设置中,我认为这种“不安全”程度是可以的。
给定一个常规 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.exit
在 func
的主体中调用也必须使用 add = TRUE
。如果你自己写了func
,那完全没有什么限制,通过add = TRUE
也是一个好习惯。
最终,没有完全安全的方法将代码注入函数,但是,在交互式设置中,我认为这种“不安全”程度是可以的。