【问题标题】:Writings functions (procedures) for data.table objects为 data.table 对象编写函数(过程)
【发布时间】:2012-11-25 05:21:56
【问题描述】:

Software for Data Analysis: Programming with 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 上执行该过程慢,这表明我对 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 程序?

【问题讨论】:

  • 修改参数在某些圈子中并没有被高度考虑,但在其他圈子中它不被认为是副作用(留下来指修改不在参数列表中的东西)。也就是说,我对这种行为很好奇。该函数没有注意到 [.data.table 正在修改参数?或者也许只有实际的赋值会触发局部变量的创建。
  • @MatthewLundberg 我会添加一个答案,但基本上data.table 故意偏离 R 的写时复制。 data.table 不是写时复制,即使在函数中也是如此。如果你真的想要复制一个20GB的data.table,你需要在函数的开头放置x=copy(x),或者在函数内部写上x=copy(x)[,y:=V1*V2]
  • @MatthewLundberg 我认为这在大多数圈子中被认为是副作用。
  • @hadley 这与我所写的并不矛盾。在 C 代码中,一个修改过的参数被视为正常活动(查看标准库中的数百个示例)。 C++ 消除了对此类结构的需求,但仍然使用 C,并且仍然传递非常量参数。
  • @hadley Matthew 说,iiuc,将某些内容传递给要通过引用修改的函数与超出其范围并修改未传递的函数略有不同。后者肯定是副作用。在 C 中可能会更改全局变量,在 R 中创建或更改 .GlobalEnv 中的变量。有些人会说修改显式传入的参数比这更安全,并且不要使用副作用来描述,其他人会这样做。

标签: r data.table


【解决方案1】:

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

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

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

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

【讨论】:

  • 好建议。我担心第 2 点,返回修改后的表格,它会在交互模式下用大表格向屏幕发送垃圾邮件。但不用担心,它不会打印整个内容。向 data.table 开发人员致敬。
  • +1 的好答案,弗洛德尔。但是,您能否详细说明为什么运行table &lt;- do.something.to(table) 是个坏主意?我一直认为&lt;- data.table 不会复制(见here)。使用返回 data.table 的函数时会损坏吗?
  • +1。但我不明白为什么修改多个表是不好的。当实现快速insert() 时,插入许多不同的表(如数据库)将是常见的做法(无论如何对我来说)。不过,出于您给出的原因,我同意不写table&lt;-。它没有坏处,但不写它有助于突出 do.something.to 通过副作用起作用。我尝试以动词开头命名这些功能。所以insert()delete() 将只是动词的副作用函数。
  • 副作用函数,如setkey,通过引用返回刚刚修改的data.table,因此可以在复合语法中使用;例如,setkey(DT,x)["foo"]。该值不会返回以与&lt;- 一起使用,尽管它可以。另外,如果需要写时复制,可以在函数内部写x &lt;- copy(x)[,y:=a*b]
【解决方案2】:

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

?":="

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

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

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

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

有趣的是,关于bit::setattr 的最后两句话与弗洛德尔的第 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 视频,以及对 insertdelete 的渴望(仅供参考,仅限动词,副作用函数)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-01-28
    • 2013-08-03
    • 1970-01-01
    • 2021-11-21
    • 2015-11-07
    • 2017-05-13
    • 2011-10-08
    相关资源
    最近更新 更多