Python 和 R 之间的互操作性

interoperability between Python and R

从 R 函数内部从 Python(使用 reticulate)读取变量的假设方法是什么? 由于 Python 会话无法访问函数环境中的变量,将它们复制到全局环境中是唯一的方法吗?

library(reticulate)
library(glue)
library(tidyverse)

df1 <- data.frame(col1 = c(123, 234), col2 = c(233, 283))

py_run_string("print (r.df1)")
#>     col1   col2
#> 0  123.0  233.0
#> 1  234.0  283.0

fun <- function(x) {
  # create a new variable with a random name in the global environment
  tmp_var_name <- str_c(sample(letters, 30, replace = TRUE), collapse = "")
  message(tmp_var_name)
  assign(tmp_var_name, x, envir = .GlobalEnv)
  
  # Python can read from that global variable
  py_run_string(glue("print (r.{tmp_var_name})"))
  
  # finally, delete the variable
  remove(list = tmp_var_name, envir = .GlobalEnv)
}

fun(df1)
#> vemcjnbxvnfvbdgushqkjcmtgzwhpu
#>     col1   col2
#> 0  123.0  233.0
#> 1  234.0  283.0

只需在函数内部做一个赋值,然后用py$

提取list
fun <- function(x) {
  # create a new variable with a random name in the global environment
  tmp_var_name <- str_c(sample(letters, 30, replace = TRUE), collapse = "")
  message(tmp_var_name)
  assign(tmp_var_name, x, envir = .GlobalEnv)
  
  # Python can read from that global variable
  py_run_string(glue("x1 = r.{tmp_var_name}"))
  
  # finally, delete the variable
  remove(list = tmp_var_name, envir = .GlobalEnv)
  py$x1
}

-测试

fun(df1)
#galdlxvgjpxuzkvmwznspxjdrftcmu
#$col2
#[1] 233 283

#$col1
#[1] 123 234

文档没有帮助,所以我找到了源代码,这让我发现 the internal py_resolve_envir() function 在问题的示例中 return R 全局环境,但不会'总是。

特别是,它的第一部分是

  # if an environment has been set, use it
  envir <- getOption("reticulate.engine.environment")
  if (is.environment(envir))
    return(envir)

意味着您可以将环境传递给名为 reticulate.engine.environment 的选项,当您尝试子集化到 r 时,网状结构将使用它而不是全局环境作为搜索的位置 python.

因此,你可以这样写:

set.seed(47L)
df1 <- data.frame(col1 = c(123, 234), col2 = c(233, 283))

fun <- function(x) {
  e <- new.env()
  options("reticulate.engine.environment" = e)
  
  # create a new variable with a random name
  tmp_var_name <- paste(sample(letters, 30, replace = TRUE), collapse = "")
  message(tmp_var_name)
  assign(tmp_var_name, x, envir = e)
  
  res <- reticulate::py_run_string(glue::glue("print( r.{tmp_var_name} )"))
  options("reticulate.engine.environment" = NULL)  # unset option
  invisible(res)
}

fun(df1)
#> zjtvorkmoydsepnxkabmeondrjaanu
#>     col1   col2
#> 0  123.0  233.0
#> 1  234.0  283.0

以避免需要将所有内容都放在全局环境中。


Coda:从 Python

执行任意 R 代码

如果您在 r 上调用 dir(),它会定义一个 __getitem__ 方法,当您执行 r.{tmp_var_name}:

时将调用该方法
reticulate::py_run_string('print( dir(r) )')
#> ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattr__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']

reticulate::py_run_string('print( r.__getitem__ )')
#> <bound method make_python_function.<locals>.python_function of <__main__.R object at 0x111b0b6a0>>

x <- 47L
reticulate::py_run_string('print( r.__getitem__("x") )')
#> 47

这个的定义是here。值得注意的是,getter() 调用它的第二个参数 code 是有原因的:

  getter <- function(self, code) {
    envir <- py_resolve_envir()
    object <- eval(parse(text = as_r_value(code)), envir = envir)
    r_to_py(object, convert = is.function(object))
  }

——它被传递给 eval(parse(text = ...)),它会将任何字符串转换为 R 代码并 运行 它。这意味着您可以将 任何 R 代码 传递给 r.__getitem__(),但有一个限制,即它必须 return 可以转换为 python 类型的东西(例如不是环境或模型),以及它不能包含换行符的更表面的限制。但这仍然可以让你在 R:

中执行任意代码
reticulate::py_run_string("print( r.__getitem__('head(iris)') )")
#>    Sepal.Length  Sepal.Width  Petal.Length  Petal.Width Species
#> 0           5.1          3.5           1.4          0.2  setosa
#> 1           4.9          3.0           1.4          0.2  setosa
#> 2           4.7          3.2           1.3          0.2  setosa
#> 3           4.6          3.1           1.5          0.2  setosa
#> 4           5.0          3.6           1.4          0.2  setosa
#> 5           5.4          3.9           1.7          0.4  setosa

reticulate::py_run_string("print( r.__getitem__('broom::tidy(lm(mpg ~ hp, mtcars))') )")
#>           term   estimate  std.error  statistic       p.value
#> 0  (Intercept)  30.098861   1.633921  18.421246  6.642736e-18
#> 1           hp  -0.068228   0.010119  -6.742389  1.787835e-07

# this method also gets called if you subset `r` with `[]`:
reticulate::py_run_string("print( r['library(tidyverse); mtcars %>% group_by(cyl) %>% summarise(across(everything(), mean))'] )")
#>    cyl        mpg        disp    ...           am      gear      carb
#> 0  4.0  26.663636  105.136364    ...     0.727273  4.090909  1.545455
#> 1  6.0  19.742857  183.314286    ...     0.428571  3.857143  3.428571
#> 2  8.0  15.100000  353.100000    ...     0.142857  3.285714  3.500000
#> 
#> [3 rows x 11 columns]

此代码将从任何环境 py_resolve_envir() returns 调用,但如果您可以从那里访问(或制作!)您想要的东西,您可以获取它。

此外,这感觉很像 SQL 注入攻击,表明您 真的 不应该让用户选择变量名,如果您是 运行在 Shiny 或类似版本中使用它,但我认为这不太可能。