【问题标题】:Call by reference in R (using function to modify an object)在 R 中通过引用调用(使用函数修改对象)
【发布时间】:2013-03-19 11:15:06
【问题描述】:

我刚刚在 R 中弄湿了我的脚,很惊讶地发现函数不会修改对象,至少看起来这是默认值。例如,我编写了一个函数,只是为了在表格的一个标签上粘贴一个星号;它在函数内部工作,但表本身没有改变。 (我主要来自 Ruby)

那么,在 R 中使用函数更改对象的常规、公认的方法是什么?如何在表格标题中添加星号?

  • 替换整个对象:myTable = title.asterisk(myTable)

  • 使用变通方法通过引用进行调用(例如,如 TszKin Julian 在 Call by reference in R 中所述?

  • 使用函数以外的结构?对象方法?

【问题讨论】:

  • myTable是什么对象类型?
  • 详细说明:“表格标题”是您的myTable 对象的属性,如果是,您首先使用什么函数来设置该属性?
  • 好吧,我对通用答案更感兴趣,尽管我目前正在修改的实际表是来自 descr 包的 CrossTable。标签是在创建对象时设置的,我说 "attr(tbl$t,'dimnames')[[2]][level.index] = paste0(level.val, marker)" 添加星号到其中之一。
  • @MikeBlyth 我已经为您编写了一个通用答案,您可能会发现它提供了丰富的信息,即使它可能无法完全解决您的问题。我希望它有助于您过渡到 R。

标签: r function


【解决方案1】:

您遇到问题的原因是您将对象传递到函数的本地命名空间中。这是关于 R 的伟大/可怕的事情之一:它允许隐式变量声明,然后随着命名空间变得更深而实现取代。

这会影响您,因为函数会在当前命名空间中创建一个新命名空间。我假设对象“myTable”最初是在全局命名空间中创建的,但是当它被传递到函数“title.asterisk”时,一个新的函数本地命名空间现在具有一个具有相同属性的对象。这样做是这样的:

title.asterisk <- function(myTable){ do some stuff to 'myTable' }

在这种情况下,函数“title.asterisk”不会对全局对象“myTable”进行任何更改。相反,会创建一个同名的本地对象,因此本地对象会取代全局对象。如果我们以这种方式调用函数title.asterisk(myTable),函数只对局部变量进行更改。

修改全局对象有两种直接方式(也有很多间接方式)。

选项 1:正如您提到的,第一个是让函数返回对象并覆盖全局对象,如下所示:

title.asterisk <- function(myTable){
    do some stuff to 'myTable'
    return(myTable)
}
myTable <- title.asterisk(myTable)

这没关系,但您的代码仍然有点难以理解,因为实际上有两个不同的“myTable”对象,一个是全局对象,一个是函数本地对象。许多编码人员通过添加句点“。”来澄清这一点。在变量参数前面,像这样:

title.asterisk <- function(.myTable){
    do some stuff to '.myTable'
    return(.myTable)
}
myTable <- title.asterisk(myTable)

好的,现在我们有一个视觉提示,这两个变量是不同的。这很好,因为我们不想在以后尝试调试代码时依赖于不可见的东西,例如命名空间取代。它只会让事情变得比他们必须的更难。

选项 2:您可以只在函数内修改对象。当您想要对对象进行破坏性编辑并且不希望内存膨胀时,这是更好的选择。如果您正在进行破坏性编辑,则无需保存原始副本。此外,如果您的对象足够大,您不想在不需要时复制它。要对全局命名空间对象进行编辑,只需不要将其传递给函数或从函数中声明它。

title.asterisk <- function(){ do some stuff to 'myTable' }

现在我们正在函数内直接编辑对象“myTable”。我们没有传递对象的事实使我们的函数寻找更高级别的命名空间来尝试解析变量名。瞧,它发现了一个更高的“myTable”对象!函数中的代码对对象进行更改。

注意事项:我讨厌调试。我的意思是我真的很讨厌调试。这在 R 中对我来说意味着一些事情:

  • 我将几乎所有内容都包装在一个函数中。当我编写代码时,一旦我得到一个工作,我将它包装在一个函数中并将它放在一边。我大量使用'.'为我所有的函数参数添加前缀,并且对于它所在的命名空间的本地内容不使用任何前缀。
  • 我尽量不在函数内修改全局对象。我不喜欢这会导致什么。如果需要修改一个对象,我会在声明它的函数中对其进行修改。这通常意味着我有多层函数调用函数,但这使我的工作既模块化又易于理解。
  • 我注释了我的所有代码,解释了每一行或每一块的用途。这似乎有点无关,但我发现这三件事对我来说是一起的。一旦你开始在函数中包装编码,你会发现自己想要重用更多的旧代码。这就是好的评论的用武之地。对我来说,这是一个必要的部分。

【讨论】:

  • 博文末尾的明智的 cmets -- 绝对值得 +1。
  • 感谢您的精彩解释。由于被 Ruby 深深灌输,我什至很少想到可以在函数中修改全局变量这一事实,当我想到它时会不寒而栗。但是不同的语言需要不同的技术,所以最好为 R 保留这一点。
【解决方案2】:

这两种范式正在替换整个对象,如您所指出的,或者编写“替换”函数,例如

`updt<-` <- function(x, ..., value) {
    ## x is the object to be manipulated, value the object to be assigned
    x$lbl <- paste0(x$lbl, value)
    x
}

> d <- data.frame(x=1:5, lbl=letters[1:5])
> d
  x lbl
1 1   a
2 2   b
3 3   c
> updt(d) <- "*"
> d
  x lbl
1 1  a*
2 2  b*
3 3  c*

这是例如$&lt;- 的行为——就地更新$ 访问的元素。 Here 是一个相关问题。可以将替换函数视为

updt1 <- function(x, ..., value) {
    x$lbl <- paste0(x$lbl, value)
    x
}
d <- updt1(d, value="*")

但在我看来,“句法糖”这个标签并不能真正体现所涉及的中心范式。它支持方便的就地更新,这与 R 通常维护的更改时复制错觉不同,它实际上是更新对象的“R”方式(而不是使用 ?ReferenceClasses,例如,它有更多其他语言的感觉,但会让期待更改时复制语义的 R 用户感到惊讶)。

【讨论】:

  • 感谢您的快速回答。如果我从您链接到的相关问题“什么是 R 中的替换函数?”中理解正确,它们只是 x
【解决方案3】:

对于将来寻找一种简单方法(不知道是否更合适)来解决此问题的任何人:

在函数内部创建对象以临时保存要更改的对象的修改版本。使用deparse(substitute()) 获取已传递给函数参数的变量的名称,然后使用assign() 覆盖您的对象。您需要在 assign() 内使用 envir = parent.frame() 来让您的对象在函数外部的环境中定义。

(MyTable <- 1:10)

[1] 1 2 3 4 5 6 7 8 9 10

title.asterisk <- function(table) {
  tmp.table <- paste0(table, "*")
  name      <- deparse(substitute(table))
  assign(name, tmp.table, envir = parent.frame())
}

(title.asterisk(MyTable))

[1]“1*”“2*”“3*”“4*”“5*”“6*”“7*”“8*”“9*”“10*”

在定义对象时使用括号比定义然后打印更有效(对我来说,更好看)。

【讨论】:

    猜你喜欢
    • 2023-03-14
    • 2021-09-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多