R如何处理函数调用中的对象?

8

我有Java和Python的背景,最近在学习R。

今天我发现,R似乎与Java和Python处理对象的方式有很大不同。

例如,以下代码:

x <- c(1:10)
print(x)
sapply(1:10,function(i){
            x[i] = 4
        })
print(x)

代码产生以下结果:
[1]  1  2  3  4  5  6  7  8  9 10
[1]  1  2  3  4  5  6  7  8  9 10

但是我希望输出的第二行全都是“4”,因为我在sapply函数中修改了向量。

这是否意味着R在函数调用中制作对象的副本而不是对对象的引用?

5个回答

18

x在全局环境中定义,不在您的函数中。

如果您试图修改一个非本地对象(如x)的值,则 R 会复制该对象并修改副本,因此每次运行匿名函数时都会创建一个 x 的副本,并将其第i个组件设置为4。当函数退出时,复制品就会永远消失。原始的x不会被修改。

如果我们写x[i] <<- i,或者写x[i] <- 4; assign("x", x, .GlobalEnv),则 R 会写回它。另一种写回的方法是将e设置为存储x的环境,并执行以下操作:

e <- environment()
sapply(1:10, function(i) e$x[i] <- 4)

或者可能是这样:

sapply(1:10, function(i, e) e$x[i] <- 4, e = environment())

通常在R中不会这样编写代码。相反,可以通过函数输出结果来实现:

x <- sapply(1:10, function(i) 4)

实际上,在这种情况下,可以写成 x[] <- 4

添加:

使用proto包,可以通过方法fx属性的第i个分量设置为4来完成此操作。

library(proto)

p <- proto(x = 1:10, f = function(., i) .$x[i] <- 4)

for(i in seq_along(p$x)) p$f(i)
p$x

新增:

上面添加了另一种选项,我们在其中显式地传递了存储 x 的环境。


谢谢!但为什么不用R编写这样的代码呢?是存在潜在风险还是只是一种惯例?我认为在其他语言中,在函数中修改全局对象是很正常的。 - Derrick Zhang
3
在函数式编程语言中,函数不能有副作用。R语言不是那么严格,但仍然限制了函数的副作用。最好按照其原意使用它,而不要试图写成另一种语言的样式。有几种对象系统(S3、S4、引用类)。S3是最常用的。S4更加复杂。引用类是最近添加的。您可能希望特别探索引用类。还有一些用户贡献的包提供了不同的范例:proto和R.oo(可能还有其他)。 - G. Grothendieck
@Spirit,此外您还可以使用parent.frame(3)来代替.GlobalEnv将x存储在闭包中,这样会更加安全。(为什么是3?1-匿名函数框架,2-sapply框架,3-sapply封闭) - mbq

7

是的,你说得对。请查看R语言定义:4.3.3 参数求值

据我所知,R语言在尝试修改数据之前并不会真正复制数据,因此遵循写时复制语义。


谢谢Anatoliy!但是如果复制的数据非常大,复制过程会花费太多时间和内存吗?还是它实际上并不复制数据,仅在函数调用结束时消除修改效果? - Derrick Zhang
有一个副本,但函数内部的“x”与函数外部的不是同一个对象。存在环境,调用环境中有一个x,在函数环境中有一个不同的x。只有通过结果的赋值才能在调用环境中看到更改。 - IRTFM

3

匿名函数内部的x不是全局环境(你的工作区)中的x。它是x的一个副本,仅限于匿名函数。在函数调用时,R不会简单地复制对象;如果可能,R将尽力避免复制,但一旦您修改了某个对象,R就必须复制该对象。

正如@DWin指出的那样,已经被修改的这个副本xsapply()调用返回的内容。你所声称得到的输出并非我所得到的结果:

> x <- c(1:10)
> print(x)
 [1]  1  2  3  4  5  6  7  8  9 10
> sapply(1:10,function(i){
+             x[i] = 4
+         })
 [1] 4 4 4 4 4 4 4 4 4 4
> print(x)
 [1]  1  2  3  4  5  6  7  8  9 10

显然,代码几乎做了你想要的事情。问题在于的输出没有被分配给一个对象,因此被打印并丢弃。
你的代码之所以能够工作是因为R的作用域规则。你真的应该将函数需要的任何对象作为参数传递给函数。然而,如果R在函数中找不到本地对象,它将搜索父环境以查找与名称匹配的对象,然后在适当的情况下搜索该环境的父级,最终命中全局环境,即工作空间。因此,你的代码之所以能够工作是因为它最终找到了一个可用的x,但是它立即被复制,这个副本在sapply()调用结束时返回。
在许多情况下,这种复制需要时间和内存。这就是人们认为在R中for循环速度较慢的原因之一;他们不会在循环之前为对象分配存储空间。如果您不分配存储空间,R必须修改/复制对象以添加循环的下一个结果。
但是,事情并不总是那么简单,在R的各个地方都是如此,例如在环境中,环境的副本实际上只是指向原始版本:
> a <- new.env()
> a
<environment: 0x1af2ee0>
> b <- 4
> assign("b", b, env = a)
> a$b
[1] 4
> c <- a ## copy the environment to `c`
> assign("b", 7, env = c) ## assign something to `b` in env `c`
> c$b ## as expected
[1] 7
> a$b ## also changed `b` in `a` as `a` and `c` are actually the same thing
[1] 7

如果您了解这些内容,可以阅读涵盖R语言底层细节的R语言定义手册。


抱歉 - 我几个小时之前就写好了(英国时间早上),但可能走神了,没有点击提交按钮。 - Gavin Simpson

0
你需要将sapply的输出分配给一个变量,否则它会消失。(实际上,你可以恢复它,因为它也被分配给了.Last.value
x <- c(1:10)
print(x)
 [1]  1  2  3  4  5  6  7  8  9 10
x <- sapply(1:10,function(i){
             x[i] = 4
        })
print(x)
 [1] 4 4 4 4 4 4 4 4 4 4

抱歉,这并没有回答问题,只会让人更加困惑。 - mbq
现在我也感到有些困惑了。问题提出者创建了一个由“4”组成的向量,但却未进行任何操作。如果他想要改变“x”的值,就需要使用赋值操作符。我认为我已经准确回答了这个问题。 - IRTFM
你似乎在暗示 v<-sapply(...,function(...){...v...}) 这个结构能够将 v 从函数的环境中导出到父级环境。更不用说问题本身就非常直接地询问 R 是否采用按值传递或按引用传递的方式了。 - mbq
是的,我建议 v <- sapply(...) 将 .Last.value 导出到 globalenv(),而赋值函数 <- 则是“使其生效”。严格来说,它是“按承诺传递”,但在实践中更像是“按值传递”。它不是“导出 x”,我也没有这么说。它正在导出值,并且 <- 以持久的方式对它们进行命名。 - IRTFM
但是 = 只返回 4,而不是 x[i] 的承诺;这样等同于 x<-sapply(1:10,function(ignoreIt) 4) - mbq
这不是我会说的方式。sapply返回一个值的承诺(没有“x”名称),而<-则将赋值给用户选择的名称。如果他希望这是对“x”的破坏性赋值,那么该选项是可用的。 - IRTFM

0
如果你想从函数内部改变一个“全局”对象,那么你可以使用非本地赋值。
x <- c(1:10)
# [1]  1  2  3  4  5  6  7  8  9 10
print(x)
sapply(1:10,function(i){
            x[i] <<- 4
        })
print(x)
# [1] 4 4 4 4 4 4 4 4 4 4

虽然在这种情况下,您可以更紧凑地将其写为x[]<-4

顺便说一句,这是 R 的一个不错的特点之一--与其使用 sapply(1:10,function(i) x[i] <<- 4 或者 for(i in 1:10) x[i]<-4for 不是函数,所以您不需要在这里使用 <<-),您可以直接编写 x[]<-4 :)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接