将脚本与 .GlobalEnv 分开:源脚本的源脚本

Separate scripts from .GlobalEnv: Source script that source scripts

这个问题类似于 ,但有一个关键的转折。

考虑一个源另一个脚本的脚本:

# main.R
source("funs.R")
x <- 1
# funs.R
hello <- function() {message("Hi")}

我想获取脚本 main.R 的源代码并将所有内容保存在“本地”环境中,比如 env <- new.env()。通常,可以调用 source("main.R", local = env) 并期望一切都在 env 环境中。然而,这里的情况并非如此:xenv 的一部分,但函数 hello 不是!它在 .GlobalEnv.

问题:如何在 R 中将一个脚本源到一个单独的环境,即使该脚本本身源其他脚本,并且不修改正在源的其他脚本?

感谢您的帮助,如果我能澄清任何问题,请告诉我。

编辑 1:更新了问题,明确指出无法修改作为源的脚本(假设它们不受您的控制)。

在所有嵌套的 source() 函数中使用参数 local = TRUE

文件 3:

# last.R
y <- 2

文件 2:

# funs.R
source("last.R", local = TRUE)
hello <- function() {message("Hi")}

文件 1:

# main.R
source("funs.R", local = TRUE)
x <- 1

采购文件 1:

env <- new.env()
source("main.R", local = env)

检查环境中的内容:

as.list(env)

$x
[1] 1

$y
[1] 2

$hello
function() {message("Hi")}
<environment: 0x000001d9dff719b8>

您可以使用trace在函数中注入代码, 所以你可以强制所有 source 调用设置 local = TRUE。 在这里,如果 localFALSE,我只是覆盖它,以防对 source 的任何嵌套调用由于它们自己的特殊逻辑而实际将其设置为其他环境。

env <- new.env()

# use !isTRUE if you want to support older R versions (<3.5.0)
tracer <- quote(
  if (isFALSE(local)) {
    local <- TRUE
  }
)

trace(source, tracer, print = FALSE, where = .GlobalEnv)

# if you're doing this inside a function, uncomment the next line
#on.exit(untrace(source, where = .GlobalEnv))

source("main.R", local = env)

如代码中所述, 如果你把这个逻辑包装在一个函数中, 考虑使用 on.exit 来确保你 untrace 即使有错误。

编辑:如评论中所述, 如果您要加载的某些脚本假设有 1 个(全局)环境,一切都结束了,这可能会出现问题。 我想你可以将示踪剂更改为

tracer <- quote(
  if (missing(local)) {
    local <- TRUE
  }
)

或者也许

tracer <- quote(
  if (isFALSE(local)) {
    # fetch the specific environment you created
    local <- get("env", .GlobalEnv)
  }
)

前者假定如果脚本根本没有指定 local, 它不关心哪个环境最终容纳了一切。 后者假定未指定 local 或将其设置为 FALSEsource 调用希望一切都在 1 环境中结束, 并修改逻辑以使用您的环境而不是全局环境。

免责声明:非常丑陋且有潜在危险,但无论如何。

重新定义source:

env<-new.env()
source<-function(...) base::source(..., local = env)
source("main.R")
#just remove your redefinition when you don't need it
rm(source)

保护自己免受无法控制的代码副作用的最佳方法是隔离。您可以使用 callr 轻松执行在单独的 R 会话中隔离的脚本:

使用环境:

env <- new.env()
env <- as.environment(callr::r(function(env) {
    list2env(env, .GlobalEnv)
    source("main.R")
    as.list(.GlobalEnv)
}, args = list(as.list(env))))
env
#> <environment: 0x0000000018124878>
env$hello()
#> Hi

坚持列表的更简单版本:

params <- list()
results <- callr::r(function(params) {
    list2env(params, .GlobalEnv)
    source("main.R")
    as.list(.GlobalEnv)
}, args = list(params))
results
#> $x
#> [1] 1
#> 
#> $hello
#> function () 
#> {
#>   message("Hi")
#> }
results$hello()
#> Hi

param 部分仅在您确实需要提供输入脚本时才需要(不用于您的示例)。 显然,这不适用于打开的连接和类似的东西。在这种情况下,您可能需要查看 callr::r_session.