准确了解data.table是何时引用另一个data.table(相对于另一个data.table的副本)


194

我在理解的传递引用属性时遇到了一些麻烦data.table。有些操作似乎“破坏”了参考,我想确切地了解正在发生的事情。

在创建data.table另一个表data.table(通过<-,然后通过来更新新表时:=,原始表也将被更改。这是预期的,具体如下:

?data.table::copystackoverflow:通过操作员在数据表包中的引用传递

这是一个例子:

library(data.table)

DT <- data.table(a=c(1,2), b=c(11,12))
print(DT)
#      a  b
# [1,] 1 11
# [2,] 2 12

newDT <- DT        # reference, not copy
newDT[1, a := 100] # modify new DT

print(DT)          # DT is modified too.
#        a  b
# [1,] 100 11
# [2,]   2 12

但是,如果我:=<-赋值和:=上面的行之间插入基于非基于的修改,DT则现在不再修改:

DT = data.table(a=c(1,2), b=c(11,12))
newDT <- DT        
newDT$b[2] <- 200  # new operation
newDT[1, a := 100]

print(DT)
#      a  b
# [1,] 1 11
# [2,] 2 12

因此,似乎该newDT$b[2] <- 200行以某种方式“破坏了”参考。我猜想这会以某种方式调用一个副本,但是我想充分理解R如何处理这些操作,以确保我不会在代码中引入潜在的错误。

如果有人可以向我解释这一点,我将非常感激。


1
我刚刚发现了这个“功能”,这令人震惊。在Internet上广泛提倡使用R <-代替=R 中的基本分配(例如Google:google.github.io/styleguide/Rguide.xml#assignment)。但这意味着data.table操作将无法与数据帧操作以相同的方式工作,因此与直接替换数据帧相去甚远。
cmo

Answers:


141

是的,它是R中的子分配,使用<-(或=->)复制整个对象。您可以使用tracemem(DT)和跟踪它.Internal(inspect(DT)),如下所示。这些data.table功能:=set()通过引用分配给传递的任何对象。因此,如果该对象先前已被复制(通过子分配<-或显式copy(DT)),则该副本将通过引用进行修改。

DT <- data.table(a = c(1, 2), b = c(11, 12)) 
newDT <- DT 

.Internal(inspect(DT))
# @0000000003B7E2A0 19 VECSXP g0c7 [OBJ,NAM(2),ATT] (len=2, tl=100)
#   @00000000040C2288 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 1,2
#   @00000000040C2250 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 11,12
# ATTRIB:  # ..snip..

.Internal(inspect(newDT))   # precisely the same object at this point
# @0000000003B7E2A0 19 VECSXP g0c7 [OBJ,NAM(2),ATT] (len=2, tl=100)
#   @00000000040C2288 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 1,2
#   @00000000040C2250 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 11,12
# ATTRIB:  # ..snip..

tracemem(newDT)
# [1] "<0x0000000003b7e2a0"

newDT$b[2] <- 200
# tracemem[0000000003B7E2A0 -> 00000000040ED948]: 
# tracemem[00000000040ED948 -> 00000000040ED830]: .Call copy $<-.data.table $<- 

.Internal(inspect(DT))
# @0000000003B7E2A0 19 VECSXP g0c7 [OBJ,NAM(2),TR,ATT] (len=2, tl=100)
#   @00000000040C2288 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 1,2
#   @00000000040C2250 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 11,12
# ATTRIB:  # ..snip..

.Internal(inspect(newDT))
# @0000000003D97A58 19 VECSXP g0c7 [OBJ,NAM(2),ATT] (len=2, tl=100)
#   @00000000040ED7F8 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 1,2
#   @00000000040ED8D8 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 11,200
# ATTRIB:  # ..snip..

请注意a,即使a未更改矢量,它也是如何被复制的(不同的十六进制值表示矢量的新副本)。甚至整个b副本都被复制了,而不仅仅是更改了需要更改的元素。这一点很重要,以避免大的数据,为什么:=set()被引入data.table

现在,复制后,newDT我们可以通过引用对其进行修改:

newDT
#      a   b
# [1,] 1  11
# [2,] 2 200

newDT[2, b := 400]
#      a   b        # See FAQ 2.21 for why this prints newDT
# [1,] 1  11
# [2,] 2 400

.Internal(inspect(newDT))
# @0000000003D97A58 19 VECSXP g0c7 [OBJ,NAM(2),ATT] (len=2, tl=100)
#   @00000000040ED7F8 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 1,2
#   @00000000040ED8D8 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 11,400
# ATTRIB:  # ..snip ..

请注意,所有3个十六进制值(列点的向量,以及2列的每一个)均保持不变。因此,它完全通过引用进行了修改,完全没有副本。

或者,我们可以DT通过引用修改原始文件:

DT[2, b := 600]
#      a   b
# [1,] 1  11
# [2,] 2 600

.Internal(inspect(DT))
# @0000000003B7E2A0 19 VECSXP g0c7 [OBJ,NAM(2),ATT] (len=2, tl=100)
#   @00000000040C2288 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 1,2
#   @00000000040C2250 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 11,600
#   ATTRIB:  # ..snip..

这些十六进制值与我们在DT上面看到的原始值相同。键入example(copy)使用更多的例子tracemem比较和data.frame

顺便说一句,如果您tracemem(DT)那么DT[2,b:=600]会看到一份报告。这是该print方法执行的前10行的副本。当用invisible()函数或脚本包裹或在函数或脚本中调用时,print不会调用该方法。

所有这些同样适用于函数内部。即,:=set()不写时复制,即使是在职能。如果需要修改本地副本,请x=copy(x)在函数开始处调用。但是,请记住data.table大数据(以及小数据的更快编程优势)。我们故意不希望复制大型对象。因此,我们不需要考虑通常的3 *工作记忆系数经验法则。我们尝试只需要多达一列的工作内存(即工作内存系数为1 / ncol而不是3)。


1
什么时候需要这种行为?
科林

有趣的是,对于data.frame对象,不会发生复制整个对象的行为。在复制的data.frame中,只有通过->分配直接更改的向量才能更改存储位置。不变的向量保持原始data.frame的向量的存储位置。data.table此处描述的s行为是自1.12.2起的当前行为。
lmo

105

快速总结一下。

<-data.table就像基; 也就是说,直到之后再进行子分配<-(例如更改列名称或更改元素,例如DT[i,j]<-v)之前,都不会进行任何复制。然后,它像base一样获取整个对象的副本。这就是所谓的写时复制。我认为最好将其称为“分配时复制”!当您使用特殊:=运算符或所set*提供的功能时,它不会进行复制data.table。如果您有大量数据,则可能要使用它们。:=并且即使在功能内set*也不会复制data.table

给定这个示例数据:

DT <- data.table(a=c(1,2), b=c(11,12))

以下只是将另一个名称“绑定” DT2到当前绑定到该名称的同一数据对象DT

DT2 <- DT

这从不复制,也从不复制。它只是标记数据对象,以便R知道两个不同的名称(DT2DT)指向同一对象。因此,如果其中一个对象后来分配给R,则R将需要复制该对象。

这也是完美的data.table。这:=不是这样做的。因此,以下是故意的错误,:=而不仅仅是绑定对象名称:

DT2 := DT    # not what := is for, not defined, gives a nice error

:=供参考转让。但是您不会像在base中那样使用它:

DT[3,"foo"] := newvalue    # not like this

您可以这样使用它:

DT[3,foo:=newvalue]    # like this

DT通过引用改变了。假设您new通过引用数据对象添加了新列,则无需这样做:

DT <- DT[,new:=1L]

因为RHS已经DT通过引用进行了更改。额外的DT <-是误解了什么:=。您可以在此处编写它,但这是多余的。

DT通过引用,:=甚至在函数内更改:

f <- function(X){
    X[,new2:=2L]
    return("something else")
}
f(DT)   # will change DT

DT2 <- DT
f(DT)   # will change both DT and DT2 (they're the same data object)

data.table请记住,适用于大型数据集。如果您有20GB data.table的内存,则需要一种方法来执行此操作。这是的非常慎重的设计决策data.table

当然可以复印。您只需要告诉data.table您确定要复制20GB数据集,方法是使用copy()函数:

DT3 <- copy(DT)   # rather than DT3 <- DT
DT3[,new3:=3L]     # now, this just changes DT3 because it's a copy, not DT too.

为了避免复制,请勿使用基本类型分配或更新:

DT$new4 <- 1L                 # will make a copy so use :=
attr(DT,"sorted") <- "a"      # will make a copy use setattr() 

如果要确保要通过引用使用进行更新,.Internal(inspect(x))并查看组成部分的内存地址值(请参阅Matthew Dowle的回答)。

:=j像,可以让你subassign参照按组。您可以按组引用添加新列。这就是为什么:=在内部这样进行[...]

DT[, newcol:=mean(x), by=group]
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.