确切地了解data.table何时是另一个data.table的引用(vs另一个副本)
我在理解data.table
的传递引用属性时遇到了一些麻烦。 一些操作似乎“打破”了参考,我想要明白到底发生了什么事情。
从另一个data.table
创build一个data.table
(通过<-
,然后更新新的表:=
,原来的表也被改变,这是预期的,按照:
?data.table::copy
和stackoverflow:通过引用传入数据表中的操作符包
这是一个例子:
library(data.table) DT <- data.table(a=c(1,2), b=c(11,12)) print(DT) # ab # [1,] 1 11 # [2,] 2 12 newDT <- DT # reference, not copy newDT[1, a := 100] # modify new DT print(DT) # DT is modified too. # ab # [1,] 100 11 # [2,] 2 12
但是,如果我在上面的<-
assignment和<-
:=
行之间插入非基于:=
的修改,现在不再修改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) # ab # [1,] 1 11 # [2,] 2 12
因此,似乎新的newDT$b[2] <- 200
线以某种方式“打破”了参考。 我想这会调用一个副本,但我想完全理解R是如何处理这些操作,以确保我不会在我的代码中引入潜在的错误。
如果有人能向我解释这一点,我将非常感激。
是的,它在R中使用<-
(或=
或->
)进行次级分配,使整个对象的副本。 您可以使用tracemem(DT)
和.Internal(inspect(DT))
来跟踪,如下所示。 data.table
特点:=
和set()
通过引用来分配它们传递的任何对象。 因此,如果该对象先前被复制(通过copy(DT)
<-
或显式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
向量是如何被复制的(不同的hex值表示向量的新副本),尽pipea
没有被改变。 即使是整个b
也是被复制的,而不是仅仅改变需要改变的元素。 这对于避免大数据很重要,为什么:=
和set()
被引入data.table
。
现在,通过我们复制的newDT
我们可以通过引用来修改它:
newDT # ab # [1,] 1 11 # [2,] 2 200 newDT[2, b := 400] # ab # 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个hex值(列点向量和2列中的每一个)保持不变。 所以这是真正的修改,参考没有任何副本。
或者,我们可以通过参考修改原始DT
:
DT[2, b := 600] # ab # [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..
这些hex值与我们在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)。
只是一个快速的总结。
data.table
和base一样。 也就是说,直到用<-
(例如改变列名称或者改变一个元素,比如DT[i,j]<-v
)完成一个子分配之后才能进行复制。 然后它就像基地一样需要整个对象的副本。 这就是所谓的写入复制。 将更好地称为副本上的副本,我想! 当你使用特殊的:=
操作符或data.table
提供的set*
函数时,它不会被复制。 如果你有大量的数据,你可能想要使用它们。 :=
并且set*
不会复制data.table
,即使在函数内。
给出这个例子的数据:
DT <- data.table(a=c(1,2), b=c(11,12))
以下只是将另一个名称“ DT2
”绑定到当前绑定到名称DT
的同一个数据对象上:
DT2 <- DT
这从来没有复制,也从来没有在基地复制。 它只是标记数据对象,以便R知道两个不同的名称( DT2
和DT
)指向同一个对象。 所以如果任何一个被分配到后面 ,R将需要复制该对象。
对于data.table
也是完美的。 :=
不是为了做到这一点。 所以下面是一个故意的错误:=
不仅仅是绑定对象名称:
DT2 := DT # not what := is for, not defined, gives a nice error
:=
用于通过引用进行分配 。 但是你不会像在基地那样使用它:
DT[3,"foo"] := newvalue # not like this
你这样使用它:
DT[3,foo:=newvalue] # like this
这通过引用改变了DT
。 假设你通过引用数据对象添加一个新的列,不需要这样做:
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
devise决定。
当然可以复印。 您只需要通过使用copy()
函数来告诉data.table您确定要复制20GB数据集:
DT3 <- copy(DT) # rather than DT3 <- DT DT[,new3:=3L] # now, this just changes DT3 because it's a copy, not DT too.
为避免复制,请勿使用基本types分配或更新:
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
就像允许你通过组参考进行分配。 您可以按组引用添加新列。 所以这就是为什么:=
在内部完成: [...]
:
DT[, newcol:=mean(x), by=group]