【问题标题】:What does it mean by formulas and closures being able to "capture the enclosing environment" in R?公式和闭包能够在 R 中“捕获封闭环境”是什么意思?
【发布时间】:2015-06-05 05:55:19
【问题描述】:

引用 Hadley Wickham 的 Advanced R 教科书,

f1 <- function() {
x <- 1:1e6
10
}

pryr::mem_change(x <- f1())
#> 1.43 kB
pryr::object_size(x)
#> 48 B

f2 <- function() {
x <- 1:1e6
a ~ b
}
pryr::mem_change(y <- f2())
#> 4 MB
pryr::object_size(y)
#> 4 MB

f3 <- function() {
  x <- 1:1e6
  function() 10
}
pryr::mem_change(z <- f3())
#> 4 MB
pryr::object_size(z)
#> 4.01 MB

f1()中,1:1e6只在函数内部被引用,所以当函数完成时返回内存,净内存变化为 0. f2()f3() 都返回捕获环境的对象,因此 x 是 函数完成时未释放。

我隐约能够理解捕获环境的闭包的概念。但是~ 是怎么做到的呢? 而且,在这种情况下,“捕捉环境”究竟意味着什么?

【问题讨论】:

    标签: r


    【解决方案1】:

    波浪号运算符实际上是一个中缀函数(就像通常的数学运算符一样),因此它会在“激活”或调用时捕获其环境。

    > is.function(`~`)
    [1] TRUE
    > myform <- a ~ b
    > length(myform)
    [1] 3
    > myform[[1]]
    `~`
    > myform[[2]]
    a
    > myform[[3]]
    b
    
    > environment(myform)
    <environment: R_GlobalEnv>
    

    【讨论】:

      【解决方案2】:

      在 R 中,环境 只是一个数据对象,它以键/值对的形式包含其他数据;基本上它是一个哈希表。

      R 中有很多内置环境,包括全局环境globalenv()、空环境emptyenv()、包的公共和私有环境、Autoload 环境等。此外,还有一些动态生成的环境用于功能评估。换句话说,当您评估一个函数时,会动态生成一个环境,以保存在该函数评估期间创建的所有局部变量。该环境通常称为评估环境,它特定于对特定功能的特定评估。

      也可以通过new.env()创建自己的环境,显然不是内置环境,不关联任何函数评估;它们是独立的、用户定义的环境。

      在函数评估过程中定义新函数时,它会捕获发生该定义的评估环境。在此上下文中,对于动态定义的函数,此环境可以称为该函数的闭包环境封闭环境。动态定义的函数本身可以称为闭包

      过程与公式相同。在函数求值过程中定义公式时,它会捕获发生该定义的求值环境,您可以在此处使用相同的术语,尽管可能有 闭包函数闭包如果上下文没有明确说明,公式 将是消除不合格术语 closure 歧义的有用细节。

      如果一个函数或公式是在全局范围内定义的,它仍然围绕一个环境闭包,那个环境恰好是全局环境。这在支持闭包的编程语言中是不常见的;例如,在 Perl 中,如果匿名子例程在其主体中不引用任何非局部变量,则它不会成为闭包;见http://apache.perl.org/docs/general/perl_reference/perl_reference.html#Understanding_Closures____the_Easy_Way。由于这个事实,从技术上讲,R 中的每个函数和公式都是一个闭包,因为它们围绕一个环境闭包。因此,在谈论 R 时,使用术语 闭包函数闭包公式 无疑是多余的,但强调这些数据对象的“闭包”仍然很有用。 (其实这条规则有一个半例外:.Primitive()函数如ifwhilereturnfunction&lt;-[$@*&amp;:sum() 等没有封闭环境,但这并不是一个真正的例外,因为它们的代码是用 C 实现并编译成 R 可执行文件;因此,它们无论如何都不能是闭包。)

      然而,闭包函数和闭包公式之间有一个显着的区别:对于闭包公式,闭包环境是在公式对象上名为.Environment 的属性上捕获的,可通过attr()/attributes() 访问。对于闭包函数,闭包环境不是属性,而是包含函数的三个基本属性之一,另外两个是主体和参数。函数的这三个属性只能通过(最终是.Internal())函数environment()body()formals() 访问。还应该提到的是,environment() 也可以用于访问闭包公式的闭包环境,即使它们也可以作为普通属性访问。 (并且,当不带参数调用时,它也可以用于返回函数范围内的当前评估环境,或全局范围内的全局环境;一般来说,environment() 是一个非常通用的函数!)

      最后,一个重要的概念是父环境链。不仅函数和公式引用环境,environments 也引用环境。每个环境都引用一个环境,就像每个闭包都引用一个环境一样。在一个环境引用另一个环境的上下文中,标准术语是说引用的环境引用环境父环境(因此理论上你可能会说后者是前者的子环境,尽管可以有多个子环境,而且我还没有看到这个术语在任何地方使用过;没有人会向下导航链。 ) 您可以使用parent.env() 来获取给定环境的父环境。

      哪个环境用作给定环境的父级取决于我们所讨论的环境,但这里最重要的环境类型是评估环境。如前所述,当一个函数被执行时,一个评估环境被创建用于对该特定函数的特定评估。那时,新的求值环境的父环境挂接到函数本身的闭包环境。

      环境引用环境的能力显然引入了多个环境以链形式相互连接的可能性,而这正是R中发生的情况。实际上,它允许形成更复杂的结构,有向图。从技术上讲,您可以创建任意数量的环境有向图,方法是使用 new.env() 创建环境并适当地分配它们的父环境。您甚至可以创建圆形图。出于好奇想看看会发生什么,我只是创建了一个“环境圈”,将函数的闭包环境连接到圈中的一个环境,然后执行该函数,其主体试图使用超赋值运算符&lt;&lt;-开始对左值进行目标搜索(稍后会详细介绍)。它导致了一个无限循环并破坏了我的 R 会话。不要这样做!

      但除了用户定义的环境和图形之外,R 中还有一个基本的环境结构,它内置在语言的核心中,并在正常执行期间重复使用。由于这种结构几乎总是沿着一条线跟随,因此将其称为链是有意义的,而忽略更复杂的结构。下面我试图准确地解释哪些环境构成了这个重要的父环境链。

      全球环境处于链条中的重要纽带;您可能会说它位于链条的“中心”。 后面全局环境是你在会话中加载的所有包的公共环境,加上基础环境前面的自动加载环境,最后链被空环境终止。您可以使用search()(以及,稍后我将演示,parent.env())检查链的这一部分:

      search();
      ## [1] ".GlobalEnv"        "package:stats"     "package:graphics"  "package:grDevices" "package:utils"     "package:datasets"  "package:methods"   "Autoloads"         "package:base"
      

      在全局环境的前面默认是什么都没有。但是当你开始在其他函数的求值环境中动态定义函数/公式时,那些封闭函数的求值环境将被嵌套的函数/公式封闭,而那些封闭的求值环境将引用它们的封闭环境,或全局如果评估是在全局范围内定义的函数,则为环境。这在技术上形成了一棵环境树,但所有道路都通向全局环境,然后通向如上所示的搜索路径。

      在任何词法范围内,任何将变量引用为右值的尝试,以及任何使用超赋值&lt;&lt;- 运算符将变量分配为左值的尝试,都会通过环境链以查找具有与该变量名称匹配的键的第一个环境。对于右值用法,该变量的当前值被替换,而对于左值用法,变量的值被替换为赋值的 RHS 的返回值。如果未定义,则对右值的目标搜索将一直向后移动,通过链的闭包侧,通过全局环境,并通过整个搜索路径,直到它遇到空环境,此时您将获得经典Error: object 'whatever' not found 错误消息。如果未定义,则左值的目标搜索也将一直向后移动,通过链的闭包侧,通过全局环境,并通过整个搜索路径,此时将在全局环境中定义一个新变量RHS 的返回值作为它的值。尽管加载的包环境中的绑定通常是锁定的,但我发现超级赋值目标搜索确实通过了整个搜索路径,这有点奇怪。这会导致对与 any 公共包环境中的现有变量发生冲突的变量名进行超级分配失败。例如,如果你运行c &lt;&lt;- 3;,你会得到Error: cannot change value of locked binding for 'c'。现在,我已经验证可以将您自己的用户定义的环境插入到搜索路径中(例如e1 &lt;- new.env(); parent.env(e1) &lt;- baseenv(); parent.env(.AutoloadEnv) &lt;- e1; e1$v1 &lt;- 3;),在这种情况下,对该环境中已经定义的变量名的超分配成功(例如v1 &lt;&lt;- 4;),但是,一般来说,没有人这样做(他们也不应该这样做),所以它的效用是不存在的。也许这是避免使用超赋值运算符的另一个很好的理由(第一个原因是它会从函数调用中产生通常不必要的副作用,这与程序设计的函数范式相反)。

      最后,为了完成,本地赋值&lt;-操作符从不发起目标搜索;它始终分配给即时评估环境中的相应键/值对。向右赋值运算符(-&gt;&gt;-&gt;)的行为与其向左的朋友一样,只是 LHS 和 RHS 的含义相反。

      我编写了一段代码来尝试演示闭包,以及与闭包相关的重要函数environment()globalenv()parent.env(),以及赋值&lt;- 和超赋值&lt;&lt;-运营商。我最初是为this answer 编写的,但在这里我将对其进行扩展以演示闭包公式。在以下代码中,其 RHS 是环境的每个赋值都会分配包含要分配到的变量的实际环境。我还分配了三个公式(对于这些 RHS 不能是环境,因为它们必须是公式!)并在代码末尾显示它们的闭包环境:

      oldGlobal <- environment(); ## environment() is same as globalenv() in global scope
      f0 <- ~.;
      (function() {
          newLocal1 <- environment(); ## creates a new local variable in this function evaluation's evaluation environment
          print(newLocal1); ## <environment: 0x6008e2fe8> (different for every evaluation)
          oldGlobal <<- parent.env(environment()); ## target search hits oldGlobal in closure environment; RHS is same as globalenv()
          newGlobal1 <<- globalenv(); ## target search fails; creates a new variable in the global environment
          f1 <<- ~.;
          (function() {
              newLocal2 <- environment(); ## creates a new local variable in this function evaluation's evaluation environment
              print(newLocal2); ## <environment: 0x600874968> (different for every evaluation)
              newLocal1 <<- parent.env(environment()); ## target search hits the existing newLocal1 in closure environment
              print(newLocal1); ## same value that was already in newLocal1
              oldGlobal <<- parent.env(parent.env(environment())); ## target search hits oldGlobal two closure environments up in the chain; RHS is same as globalenv()
              newGlobal2 <<- globalenv(); ## target search fails; creates a new variable in the global environment
              f2 <<- ~.;
          })();
      })();
      oldGlobal; ## <environment: R_GlobalEnv>
      newGlobal1; ## <environment: R_GlobalEnv>
      newGlobal2; ## <environment: R_GlobalEnv>
      environment(f0); ## <environment: R_GlobalEnv>
      environment(f1); ## <environment: 0x6008e2fe8>
      environment(f2); ## <environment: 0x600874968>
      

      如您所见,这里有三个相关的环境:(1)全局环境R_GlobalEnv,(2)一级评估环境0x6008e2fe8,其父环境是全局环境,以及(3 ) 二级评估环境0x600874968,其父环境为一级评估环境。下面我总结了哪些变量引用了哪些环境:

      oldGlobal    assigned to the global environment R_GlobalEnv
      newGlobal1   assigned to the global environment R_GlobalEnv
      newGlobal2   assigned to the global environment R_GlobalEnv
      f0           closured around the global environment R_GlobalEnv
      newLocal1    assigned to the first-level evaluation environment 0x6008e2fe8
      f1           closured around the first-level evaluation environment 0x6008e2fe8
      newLocal2    assigned to the second-level evaluation environment 0x600874968
      f2           closured around the second-level evaluation environment 0x600874968
      

      最后,沿着父环境链从二级评估环境一路回溯到空环境是很有指导意义的,它准确地阐明了目标搜索是如何完成的。我们可以为此使用f2,因为它围绕二级评估环境关闭:

      environment(f2);
      ## <environment: 0x600874968>
      parent.env(environment(f2));
      ## <environment: 0x6008e2fe8>
      parent.env(parent.env(environment(f2)));
      ## <environment: R_GlobalEnv>
      parent.env(parent.env(parent.env(environment(f2))));
      ## <environment: package:stats>
      ## attr(,"name")
      ## [1] "package:stats"
      ## attr(,"path")
      ## [1] "/usr/lib/R/library/stats"
      Reduce(function(a,b) b(a),c(environment(f2),replicate(4,parent.env)));
      ## <environment: package:graphics>
      ## attr(,"name")
      ## [1] "package:graphics"
      ## attr(,"path")
      ## [1] "/usr/lib/R/library/graphics"
      Reduce(function(a,b) b(a),c(environment(f2),replicate(5,parent.env)));
      ## <environment: package:grDevices>
      ## attr(,"name")
      ## [1] "package:grDevices"
      ## attr(,"path")
      ## [1] "/usr/lib/R/library/grDevices"
      Reduce(function(a,b) b(a),c(environment(f2),replicate(6,parent.env)));
      ## <environment: package:utils>
      ## attr(,"name")
      ## [1] "package:utils"
      ## attr(,"path")
      ## [1] "/usr/lib/R/library/utils"
      Reduce(function(a,b) b(a),c(environment(f2),replicate(7,parent.env)));
      ## <environment: package:datasets>
      ## attr(,"name")
      ## [1] "package:datasets"
      ## attr(,"path")
      ## [1] "/usr/lib/R/library/datasets"
      Reduce(function(a,b) b(a),c(environment(f2),replicate(8,parent.env)));
      ## <environment: package:methods>
      ## attr(,"name")
      ## [1] "package:methods"
      ## attr(,"path")
      ## [1] "/usr/lib/R/library/methods"
      Reduce(function(a,b) b(a),c(environment(f2),replicate(9,parent.env)));
      ## <environment: 0x60019a7f8>
      ## attr(,"name")
      ## [1] "Autoloads"
      Reduce(function(a,b) b(a),c(environment(f2),replicate(10,parent.env)));
      ## <environment: base>
      Reduce(function(a,b) b(a),c(environment(f2),replicate(11,parent.env)));
      ## <environment: R_EmptyEnv>
      Reduce(function(a,b) b(a),c(environment(f2),replicate(12,parent.env)));
      ## Error in b(a) : the empty environment has no parent
      

      【讨论】:

        猜你喜欢
        • 2015-01-10
        • 1970-01-01
        • 1970-01-01
        • 2015-11-27
        • 1970-01-01
        • 2016-07-08
        • 1970-01-01
        • 2019-04-03
        相关资源
        最近更新 更多