5

我对 R 相当陌生,在阅读手册时,我偶然发现了一段关于词法作用域的文章以及以下代码示例:

 open.account <- function(total) {
   list(
     deposit = function(amount) {
       if(amount <= 0)
         stop("Deposits must be positive!\n")
       total <<- total + amount
       cat(amount, "deposited.  Your balance is", total, "\n\n")
     },
     withdraw = function(amount) {
       if(amount > total)
         stop("You don't have that much money!\n")
       total <<- total - amount
       cat(amount, "withdrawn.  Your balance is", total, "\n\n")
     },
     balance = function() {
       cat("Your balance is", total, "\n\n")
     }
   )
 }

 ross <- open.account(100)
 robert <- open.account(200)

 ross$withdraw(30)
 ross$balance()
 robert$balance()

 ross$deposit(50)
 ross$balance()
 ross$withdraw(500)

所以,我理解上面的代码做了什么,我想我仍然对它的工作原理感到困惑。如果在函数执行完成后您仍然可以访问函数的“局部”变量,那么预测何时不再需要变量不是很难或不可能吗?在上面的代码中,如果它被用作更大程序的一部分,“total”是否会保存在内存中,直到整个程序完成?(本质上成为一个全局变量内存)如果这是真的,不会这会导致内存使用问题吗?

我在这个网站上查看了另外两个问题:“词汇范围是如何实现的?” 和“为什么编译器更喜欢词法范围?”。那里的答案在我脑海中浮现,但它让我想知道:如果(正如我猜测的那样)编译器不只是使所有变量成为全局变量(内存方面),而是使用某种技术来预测某些变量何时不会不再需要并且可以删除,做这项工作实际上不会使编译器更难而不是更容易吗?

我知道这是很多不同的问题,但任何帮助都会很好,谢谢。

4

2 回答 2

5

OP 似乎正在寻找有关环境的说明。

在 R 中,每个函数 [1] 都有一个封闭的环境。这是它所知道的对象的集合,除了作为其参数传入的对象之外,或者它在其代码中创建的对象的集合。

在提示符下创建函数时,它的环境是全局环境。这只是您工作区中的对象集合,您可以通过键入来查看ls()。例如,如果您的工作区包含一个数据框Df,您可以创建如下函数:

showDfRows <- function()
{
    cat("The number of rows in Df is: ", nrow(Df, "\n")
    return(NULL)
}

Df即使您没有将其作为参数传递,您的函数也知道;它存在于函数的环境中。环境可以嵌套,这就是包命名空间之类的工作方式。例如lm(y ~ x, data=Df),即使您的工作区不包含任何名为lm. 这是因为全局环境的父链包含stats包,这是lm函数所在的位置。 [2]

当函数在另一个函数中创建时,它们的封闭环境是其父函数的评估框架。这意味着子函数可以访问父函数已知的所有对象。例如:

f <- function(x)
{
    g <- function()
    {
        cat("The value of x is ", x, "\n")
    }
    return(NULL)
}

请注意,g它不包含任何名为 的对象x,也不包含任何名为 的参数x。但是,这一切仍然有效,因为它将x从其 parent 的评估框架中检索f

这是上面代码使用的技巧。当您运行时open_account,它会创建一个评估框架来执行其代码。open_account然后创建 3 个函数depositwithdrawbalance。这 3 个中的每一个都有作为其封闭环境的评估框架open_account。在此评估框架中有一个名为 的变量total,其值由您传入,并将由depositwithdraw操作balance

完成open_account后,它会返回一个列表。如果这是一个常规函数,它的评估框架现在将由 R 处理。然而,在这种情况下,R 可以看到返回的列表包含需要使用该评估框架的函数;所以框架继续存在。

那么,为什么罗斯和罗伯特的账户不冲突呢?每次执行时open_account,R 都会创建一个的评估框架。开罗斯和罗伯特账户的框架是完全分开的,就像,如果你跑lm(y ~ x, data=Df),就会有一个单独的框架,如果你跑lm(y ~ x, data=Df2)。每次open_account返回时,它都会带来一个新的环境来存储刚刚创建的余额。(它还将包含, 和 函数的新副本depositwithdrawbalance通常我们可以忽略用于此的内存。)

[1] 从技术上讲,每次关闭,但我们不要把事情弄糊涂

[2] 同样,命名空间和环境之间存在技术区别,但在这里并不重要

于 2013-06-28T16:22:34.450 回答
3

您应该将其open.account视为生成“帐户”的命名单个实例的生成器函数。每个单独的帐户都有一个本地“总计”和一组对该特定总计进行操作的函数。(没有编译器;R 被解释。)局部变量 'total' 会占用空间,直到持有它的对象被删除。我不认为“全球”是谈论这个的好方法(尽管帮助页面中的语言。)。如果您在命令行(即“查看” .GlobalEnv)并执行了一个ls()调用,您将看不到任何未结账户“总计”。

如果你想创建一个可检查代码的版本,@G. Grothendieck 在 2011 年在 R-help 中建议的策略可能会很有趣:

open.account <- function(total) {
   this<-environment()
   list(this,
     deposit = function(amount) {
       if(amount <= 0)
         stop("Deposits must be positive!\n")
       total <<- total + amount
       cat(amount, "deposited.  Your balance is", total, "\n\n")
     },
     withdraw = function(amount) {
       if(amount > total)
         stop("You don't have that much money!\n")
       total <<- total - amount
       cat(amount, "withdrawn.  Your balance is", total, "\n\n")
     },
     balance = function() {
       cat("Your balance is", total, "\n\n")
     }
   )
 }

 ross <- open.account(100)
ross$deposit(200)
ross[[1]]$total
[1] 300

如果您命名了第一个列表元素。“这个”你可以这样做:

> ross$deposit(200)
200 deposited.  Your balance is 300 

> ross$this$total
[1] 300

有一段时间我对“词汇”这个词有疑问。很长一段时间我都想不通它为什么叫这个名字。最终,我想到了一个字典类比,也许是不同版本的字典。一个词在某个特定的出版时间从其他词集合中的定义中获得意义。如果出版了新词典,其含义可能会发生变化,但是研究与第一版同时出版的材料的人应该根据较早的词典版本而不是较新的词典来解释它。

于 2013-06-28T15:40:56.503 回答