在数据分析软件:使用R 编程>一书中,John Chambers强调通常不应该为其副作用编写函数;相反,函数应返回一个值而不修改其调用环境中的任何变量。相反,使用data.table对象编写好的脚本应该特别避免使用<-
的对象赋值,通常用于存储函数的结果。
首先,是一个技术问题。想象一个名为proc1
的R函数,它接受data.table
对象x
作为其参数(除此之外,可能还有其他参数)。 proc1
返回NULL,但使用x
修改:=
。据我所知,proc1
调用proc1(x=x1)
仅仅因为承诺的运作方式而制作了x1
的副本。但是,如下所示,原始对象x1
仍由proc1
修改。为什么/这是怎么回事?
> require(data.table)
> x1 <- CJ(1:2, 2:3)
> x1
V1 V2
1: 1 2
2: 1 3
3: 2 2
4: 2 3
> proc1 <- function(x){
+ x[,y:= V1*V2]
+ NULL
+ }
> proc1(x1)
NULL
> x1
V1 V2 y
1: 1 2 2
2: 1 3 3
3: 2 2 4
4: 2 3 6
>
此外,似乎使用proc1(x=x1)
并不比直接在x上执行过程慢,这表明我对promises的模糊理解是错误的,并且它们以引用的方式工作。 :
> x1 <- CJ(1:2000, 1:500)
> x1[, paste0("V",3:300) := rnorm(1:nrow(x1))]
> proc1 <- function(x){
+ x[,y:= V1*V2]
+ NULL
+ }
> system.time(proc1(x1))
user system elapsed
0.00 0.02 0.02
> x1 <- CJ(1:2000, 1:500)
> system.time(x1[,y:= V1*V2])
user system elapsed
0.03 0.00 0.03
因此,假设将data.table参数传递给函数不会增加时间,这使得编写data.table对象的过程成为可能,同时结合了data.table的速度和函数的普遍性。但是,考虑到John Chambers所说的那些函数不应该有副作用,在R中编写这种类型的过程编程真的“没问题”吗?为什么他认为副作用是“坏”?如果我不理会他的建议,我应该注意哪些陷阱?如何编写“好”的data.table程序?
答案 0 :(得分:26)
是的,data.table
中的列的添加,修改和删除由reference
完成。从某种意义上说,它是一个好的东西,因为data.table
通常包含大量数据,每次更改它时,重新分配所有内容都会非常耗费内存和时间。制作。另一方面,它是一个坏的东西,因为它违反了R试图通过默认使用no-side-effect
来尝试推广的pass-by-value
函数式编程方法。使用无副作用编程时,几乎不用担心何时调用函数:您可以放心,您的输入或环境不会受到影响,您可以专注于函数的输出。它很简单,因此很舒服。
当然,如果你知道自己在做什么,就可以忽略约翰·钱伯斯的建议。关于编写“好”data.tables程序,如果我是你,我会考虑一些规则,作为限制复杂性和副作用数量的方法:
do.something.to(table)
而不是table <- do.something.to(table)
。如果该函数具有另一个(“真实”)输出,那么在调用result <- do.something.to(table)
时,很容易想象如何将注意力集中在输出上并忘记调用该函数会对您的表产生副作用。 虽然“一个输出/无副作用”功能是R中的标准,但上述规则允许“一个输出或副作用”。如果你同意副作用在某种程度上是一种输出形式,那么你会同意我并没有过多地遵守R的单输出函数式编程风格而过分夸大规则。允许函数具有多个副作用会稍微延伸一点;并不是说你不能这样做,但如果可能的话我会尽量避免它。
答案 1 :(得分:16)
文档可以改进(建议非常受欢迎),但目前这里有什么。或许它应该说“甚至在功能内”?
在?":="
:
data.tables不会通过以下方式复制:=,setkey或任何其他set *函数。见副本。
通过引用修改DT并返回新值。如果您需要副本,请先复制一份(使用DT2 = copy(DT))。回想一下,这个包适用于大数据(混合列类型,多列密钥),其中通过引用进行的更新比复制整个表快许多个数量级。
并在?copy
中(但我意识到这与setkey混淆):
输入通过引用修改,并返回(不可见),以便它可以 用于复合语句;例如,setkey(DT,a)[J(“foo”)]。如果你 需要复印件,先取一份(使用DT2 =复印件(DT))。 copy()可能 在以下情况下有时也很有用:=用于通过分配给列 参考。看?复制。请注意,setattr也在包位中。都 软件包只是在C级公开R的内部setAttrib函数,但是 回报值不同。 bit :: setattr返回NULL(不可见) 提醒你这个功能用于它的副作用。 data.table :: setattr返回已更改的对象(不可见),以供使用 复合语句。
有趣的是,关于bit::setattr
的最后两句话与flodel的第2点有关。
另见这些相关问题:
Understanding exactly when a data.table is a reference to (vs a copy of) another data.table
Pass by reference: The := operator in the data.table package
data.table 1.8.1.: “DT1 = DT2” is not the same as DT1 = copy(DT2)?
我非常喜欢你问题的这一部分:
这使得编写data.table对象的过程成为可能, 结合data.table的速度和。的普遍性 一个功能。
是的,这绝对是其中一个意图。考虑数据库的工作原理:许多不同的用户/程序通过引用(插入/更新/删除)更改数据库中的一个或多个(大)表。这在数据库领域工作得很好,更像是data.table的思维方式。因此,主页上的svSocket视频,以及insert
和delete
的需求(通过引用,仅限动词,副作用函数)。