42

在《数据分析软件:使用 R 编程》一书中,约翰·钱伯斯强调,通常不应为函数的副作用而编写函数;相反,一个函数应该在不修改其调用环境中的任何变量的情况下返回一个值。相反,使用 data.table 对象编写好的脚本应该特别避免使用对象赋值 with <-,通常用于存储函数的结果。

首先,是一个技术问题。想象一个被调用的 R 函数proc1,它接受一个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
> 

此外,似乎 usingproc1(x=x1)并不比直接在 x 上执行该过程慢,这表明我对 Promise 的模糊理解是错误的,并且它们以传递引用的方式工作:

> 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 程序?

4

2 回答 2

30

是的,s 中列的添加、修改、删除data.table是由reference. 从某种意义上说,这是一件好事,因为 adata.table通常会保存大量数据,并且每次对其进行更改时都重新分配它会非常耗费内存和时间。另一方面,这是一件坏事,因为它违背了no-side-effectR 试图pass-by-value通过默认使用来推广的函数式编程方法。使用无副作用编程,调用函数时无需担心:您可以放心,您的输入或您的环境不会受到影响,您可以只关注函数的输出。它很简单,因此很舒服。

当然,如果您知道自己在做什么,可以无视 John Chambers 的建议。关于编写“好的” data.tables 程序,如果我是你,我会考虑以下几条规则,作为限制复杂性和副作用数量的一种方式:

  • 一个函数不应修改多个表,即修改该表应该是唯一的副作用,
  • 如果一个函数修改了一个表,则将该表作为函数的输出。当然,您不会想重新分配它:只需运行do.something.to(table)而不是table <- do.something.to(table). 相反,如果该函数有另一个(“真实”)输出,那么在调用 时result <- do.something.to(table),很容易想象您如何将注意力集中在输出上,而忘记调用该函数会对您的桌子产生副作用。

虽然“一个输出/无副作用”功能是 R 中的规范,但上述规则允许“一个输出或副作用”。如果您同意副作用在某种程度上是一种输出形式,那么您就会同意我并没有过多地违反规则,松散地坚持 R 的单输出函数式编程风格。允许函数有多种副作用会有点牵强;并不是说你不能这样做,但如果可能的话,我会尽量避免它。

于 2012-12-07T03:53:15.573 回答
17

文档可以改进(非常欢迎提出建议),但这是目前的内容。也许它应该说“即使在功能内”?

?":="

:=、setkey 或任何其他 set* 函数不会在更改时复制 data.tables。见副本。

通过引用修改 DT 并返回新值。如果需要副本,请先获取副本(使用 DT2=copy(DT))。回想一下,这个包适用于大数据(混合列类型,具有多列键),其中通过引用进行更新可能比复制整个表快很多数量级。

并且在?copy(但我意识到这与 setkey 混淆了):

输入通过引用修改,并返回(不可见),因此可以在复合语句中使用;例如,setkey(DT,a)[J("foo")]。如果需要副本,请先获取副本(使用 DT2=copy(DT))。在 := 用于通过引用对列进行子分配之前,copy() 有时也可能很有用。见?复制。请注意, setattr 也在包位中。这两个包仅在 C 级别公开了 R 的内部 setAttrib 函数,但返回值不同。bit::setattr 返回 NULL(不可见)以提醒您该函数用于其副作用。data.table::setattr 返回更改的对象(不可见),用于复合语句。

有趣的是,最后两句话bit::setattr与弗洛德尔的第 2 点有关。

另请参阅这些相关问题:

准确了解 data.table 何时是对另一个 data.table 的引用(相对于副本)
通过引用传递:data.table 包
data.table 1.8.1 中的 := 运算符:“DT1 = DT2”不是与 DT1 = 复制(DT2)相同吗?

我非常喜欢你问题的这一部分:

这使得为​​ data.table 对象编写过程成为可能,同时结合了 data.table 的速度和函数的通用性。

是的,这绝对是意图之一。考虑数据库的工作方式:许多不同的用户/程序通过引用(插入/更新/删除)数据库中的一个或多个(大)表来更改。这在数据库领域工作得很好,更像是 data.table 的想法。因此主页上的 svSocket 视频,以及对 and 的渴望insertdelete通过引用,仅动词,副作用函数)。

于 2012-12-07T11:18:16.830 回答