为data.table对象编写函数(过程)

在“用于数据分析的软件:用R编程的软件”一书中,约翰·钱伯斯强调函数通常不应写为副作用; 相反,函数应该返回一个值,而不需要在其调用环境中修改任何变量。 相反,使用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上执行过程慢,这表明我对承诺的模糊理解是错误的,并且它们以逐行引用的方式工作:

> 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的速度和函数的普遍性。 但是,根据约翰钱伯斯的说法,这些函数不应该有副作用,在R中编写这种类型的过程编程真的是“好的”吗? 他为什么认为副作用是“坏”? 如果我要忽视他的建议,我应该注意哪些陷阱? 我能做些什么来写出“好”的data.table程序?


是的,在data.table添加,修改和删除列是通过reference完成的。 从某种意义上说,这是一件好事,因为data.table通常包含大量数据,并且每次对其进行更改时都会重新分配所有数据,这会非常耗费内存和时间。 另一方面,这是一件坏事,因为它违背了R通过默认使用pass-by-value来促进的no-side-effect函数编程方法。 无副作用编程时,调用函数时几乎不用担心:您可以放心,您的输入或环境不会受到影响,并且您可以专注于函数的输出。 这很简单,因此很舒服。

如果你知道你在做什么,当然可以不考虑约翰钱伯斯的建议。 关于编写“好”的data.tables程序,这里有几条规则我会考虑如果我是你,作为一种限制复杂性和副作用数量的方法:

  • 一个函数不应该修改多个表,即修改该表应该是唯一的副作用,
  • 如果一个函数修改一个表,那么将该表作为该函数的输出。 当然,你不想重新分配它:只需运行do.something.to(table)而不是table <- do.something.to(table) 。 如果函数有另一个(“真实”)输出,那么当调用result <- do.something.to(table) ,很容易想象如何将注意力集中在输出上,并忘记调用函数有一个副作用在你的桌子上。
  • 虽然“一个输出/无副作用”函数是R中的标准,但上述规则允许“一个输出或副作用”。 如果你同意副作用是某种形式的输出,那么你会同意我不会太松懈地遵守R的单输出函数式编程风格。 允许函数具有多种副作用会稍微延长一点; 不是你不能这样做,但我会尽可能地避免它。


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

    In ?":="

    data.tables不会通过:=,setkey或任何其他set *函数进行复制。 请参阅副本。

    DT通过引用进行修改,并返回新值。 如果您需要复印件,请先复印一份(使用DT2 =复印件(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点有关。

    另请参阅以下相关问题:

    确切地了解data.table何时是对另一个data.table的引用(vs副本)
    按引用传递:data.table包中的:=运算符
    data.table 1.8.1 .:“DT1 = DT2”与DT1 = copy(DT2)不一样?

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

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

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

    链接地址: http://www.djcxy.com/p/68793.html

    上一篇: Writings functions (procedures) for data.table objects

    下一篇: Add multiple columns to R data.table in one function call?