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

Understanding exactly when a data.table is a reference to (vs a copy of) another data.table

提问人:Peter Fine 提问时间:4/19/2012 最后编辑:smciPeter Fine 更新时间:5/29/2018 访问量:44030

问:

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

在从另一个(通过 ,然后通过 更新新表时,原始表也会被更改。这是预期的,如下所示:data.tabledata.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

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

如果有人能向我解释这一点,我将不胜感激。

r 引用 copy data.table 赋值运算符

评论

5赞 cmo 3/1/2018
我刚刚发现了这个“功能”,它太可怕了。在互联网上,它被广泛提倡用于R中的基本赋值(例如,Google:google.github.io/styleguide/Rguide.xml#assignment)。但这意味着 data.table 操作的功能与数据帧操作不同,因此远非数据帧的直接替代品。<-=

答:

153赞 Matt Dowle 4/19/2012 #1

是的,R 中的子赋值使用 (or or ) 创建整个对象的副本。您可以使用 和 来跟踪它,如下所示。特征并通过引用分配给它们传递的任何对象。因此,如果该对象之前被复制(通过子赋值或显式),那么它是通过引用修改的副本。<-=->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..

请注意,即使没有更改,向量也是如何被复制的(不同的十六进制值表示向量的新副本)。甚至整个都被复制了,而不仅仅是更改需要更改的元素。对于大数据来说,这很重要,因为要避免这一点,以及为什么和被引入 .aab:=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..

这些十六进制值与我们在上面看到的原始值相同。键入更多示例,使用 并与之进行比较。DTexample(copy)tracememdata.frame

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

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

评论

3赞 colin 2/21/2019
这种行为在什么时候是可取的?
0赞 lmo 7/29/2019
有趣的是,data.frame 对象不会发生复制整个对象的行为。在复制的 data.frame 中,只有通过赋值直接更改的向量会更改内存位置。不变的向量保持原始 data.frame 向量的内存位置。此处描述的 s 行为是截至 1.12.2 的当前行为。->data.table
120赞 statquant 1/12/2013 #2

只是一个快速总结。

<-with 就像 base;即,在之后使用 (例如更改列名或更改元素(例如 ) 完成子赋值之前,不会进行任何复制。然后,它像 base 一样获取整个对象的副本。这就是所谓的写时复制。我认为最好称为 copy-on-subassign!当您使用特殊运算符或 提供的函数时,它不会复制。如果您有大量数据,您可能希望改用它们。 并且不会复制 ,即使在函数中也是如此。data.table<-DT[i,j]<-v:=set*data.table:=set*data.table

给定此示例数据:

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

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

DT2 <- DT

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

这也非常适合。不是为了这样做。因此,以下是一个故意的错误,因为不仅仅是绑定对象名称: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

这因参考而改变。假设您通过引用数据对象添加一个新列,则无需这样做:DTnew

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

因为 RHS 已经通过引用进行了更改。额外的是误解了什么是。你可以把它写在那里,但它是多余的。DTDT <-:=

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.tabledata.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() 

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

像这样写允许你按组的引用进行细分。您可以按组引用添加新列。所以这就是为什么在里面这样做的原因::=j:=[...]

DT[, newcol:=mean(x), by=group]