Используйте `rmarkdown::render` в ограниченном окружении

У меня есть следующее Rmd файл, который я назвал test.Rmd:

---
title: "test"
output: html_document
---

```{r}
print(y)
```

```{r}
x <- "don't you ignore me!"
print(x)
```

Я хочу вызвать рендер следующим образом:

render('test.Rmd', output_format = "html_document",
        output_file = 'test.html',
        envir = list(y="hello"))

но не получается

processing file: test.Rmd
  |................                                                 |  25%
  ordinary text without R code

  |................................                                 |  50%
label: unnamed-chunk-1
  |.................................................                |  75%
  ordinary text without R code

  |.................................................................| 100%
label: unnamed-chunk-2
Quitting from lines 11-13 (test.Rmd) 
Error in print(x) : object 'x' not found

Первый кусок прошел просто отлично, поэтому что-то сработало. Если я определю y в моей глобальной среде я могу запустить его без envir аргумент и все работает нормально.

Я понял возможно render не любит списки, поэтому давайте создадим правильную среду:

y_env <- as.environment(list(y="hello"))
ls(envir = y_env)
# [1] "y"

render('test.Rmd', output_format = "html_document",
       output_file = 'test.html',
       envir = y_env)

Но это еще хуже, он не находит print!

processing file: test.Rmd
  |................                                                 |  25%
  ordinary text without R code

  |................................                                 |  50%
label: unnamed-chunk-1
Quitting from lines 7-8 (test.Rmd) 
Error in eval(expr, envir, enclos) : could not find function "print"

Теперь в документах упоминается использование функции new.env так что от отчаяния я пытаюсь это:

y_env <- new.env()
y_env$y <- "hello"
render('test.Rmd', output_format = "html_document",
       output_file = 'test.html',
       envir = y_env)

И теперь это работает!

processing file: test.Rmd
  |................                                                 |  25%
  ordinary text without R code

  |................................                                 |  50%
label: unnamed-chunk-1
  |.................................................                |  75%
  ordinary text without R code

  |.................................................................| 100%
label: unnamed-chunk-2

output file: test.knit.md

"C:/Program Files/RStudio/bin/pandoc/pandoc" +RTS -K512m -RTS test.utf8.md --to html --from markdown+autolink_bare_uris+ascii_identifiers+tex_math_single_backslash --output test.html --smart --email-obfuscation none --self-contained --standalone --section-divs --template "**redacted**\RMARKD~1\rmd\h\DEFAUL~1.HTM" --no-highlight --variable highlightjs=1 --variable "theme:bootstrap" --include-in-header "**redacted**\AppData\Local\Temp\RtmpGm9aXz\rmarkdown-str3f6c5101cb3.html" --mathjax --variable "mathjax-url:https://mathjax.rstudio.com/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML" 

Output created: test.html

Итак, я запутался в нескольких вещах:

  • Почему render распознавать списки (первый чанк не потерпел неудачу), но затем игнорирует регулярные назначения в чанках
  • Почему моя вторая попытка не работает и чем она отличается от моей третьей попытки?
  • Это ошибка?
  • Какой идиоматический способ сделать это?

1 ответ

Решение

Ваши первые два примера терпят неудачу по разным причинам. Чтобы понять обе ошибки, сначала важно немного узнать о том, как куски кода оцениваются knitr и rmarkdown.


Процедура оценки общего кода блока Knitr

Когда вы звоните rmarkdown::render() в вашем файле каждый фрагмент кода в конечном итоге оценивается вызовом evaluate::evaluate(), С точки зрения его оценочного поведения и правил определения объема, evaluate() ведет себя почти так же, как базовая функция R eval(),

(Куда evaluate::evaluate() отличается больше всего от eval() в том, как он обрабатывает вывод каждого вычисленного выражения. Как объяснено в ?evaluateв дополнение к оценке выражения, переданного в качестве первого аргумента, он "захватывает всю информацию, необходимую для воссоздания вывода, как если бы вы скопировали и вставили код в R-терминал". Эта информация включает в себя графики, предупреждения и сообщения об ошибках, поэтому она так удобна в пакете, как knitr!)

В любом случае, возможный звонок evaluate()изнутри функции knitr:::block_exec()выглядит примерно так

evaluate::evaluate(code, envir = env, ...)

в котором:

  • code является вектором символьных строк, дающих (возможно, несколько) выражений, составляющих текущий фрагмент.

  • env это значение, которое вы поставили envir формальный аргумент в вашем первоначальном вызове rmarkdown::render(),


Ваш первый пример

В вашем первом примере envir это список, а не среда. В этом случае оценка выполняется в локальной среде, созданной вызовом функции. Неразрешенные символы (как описано в обоих ?eval а также ?evaluate) ищутся первыми в списке прошедших envirа затем в цепочке сред, начиная с того, что дано enclos аргумент. В основном, назначения являются локальными для среды временной оценки, которая прекращается после завершения вызова функции.

Так как evaluate() работает по одному на символьном векторе выражений, когда envir список, переменные, созданные в одном из этих выражений, не будут доступны для использования в последующих выражениях.

Когда envir аргумент rmarkdown::render() это список, ваш блок кода в конечном итоге оценивается с помощью вызова:

library(evaluate)
code <- c('x <- "don\'t you ignore me!"',
          'print(x)')
env <- list(y = 1:10)
evaluate(code, envir = env)

## Or, for prettier printing:
replay(evaluate(code, envir = env))
## > x <- "don't you ignore me!"
## > print(x)
## Error in print(x): object 'x' not found

Эффект точно такой же, как если бы вы сделали это с eval():

env <- list(y =1 :10)
eval(quote(x <- "don't you ignore me"), envir = env)
eval(quote(x), envir = env)
## Error in eval(quote(x), envir = env) : object 'x' not found

Ваш второй пример

когда envir= среда возвращается as.environment(list())Вы получаете ошибки по другой причине. В этом случае ваш кодовый блок в конечном счете оценивается вызовом, подобным этому:

library(evaluate)
code <- c('x <- "don\'t you ignore me!"',
          'print(x)')
env <- as.environment(list(y = 1:10))
evaluate(code, envir = env)

## Or, for prettier printing:
replay(evaluate(code, envir = env))
## > x <- "don't you ignore me!"
## Error in x <- "don't you ignore me!": could not find function "<-"
## > print(x)
## Error in print(x): could not find function "print"

Как вы заметили, это не удается, потому что as.environment() возвращает среду, окружающая среда которой является пустой (то есть среда, возвращаемая emptyenv()). evaluate() (лайк eval() бы) ищет символ <- в env и, когда он не находит его там, запускает цепочку окружающих сред, которые здесь не содержат никакого соответствия. (Напомним также, что когда envir это среда, а не список, enclos аргумент не используется.)


Рекомендуемое решение

Чтобы делать то, что вы хотите, вам нужно создать среду, которая: (1) содержит все объекты в вашем списке и это; (2) имеет в качестве окружающей среды родительскую среду вашего вызова render() (т.е. среда, в которой призыв к render() обычно оценивается). Самый краткий способ сделать это состоит в том, чтобы использовать изящный list2env() функция, вот так:

env <- list2env(list(y="hello"), parent.frame())
render('test.Rmd', output_format = "html_document",
        output_file = 'test.html',
        envir = env)

Это приведет к тому, что ваши куски кода будут оцениваться с помощью кода, подобного следующему, что вам и нужно:

library(evaluate)
code <- c('x <- "don\'t you ignore me!"',
          'print(x)')
env <- list2env(list(y = 1:10), envir = parent.frame())
evaluate(code, envir = env)
replay(evaluate(code, envir = env))
## > x <- "don't you ignore me!"
## > print(x)
## [1] "don't you ignore me!"
Другие вопросы по тегам