首页
学习
活动
专区
工具
TVP
发布
社区首页 >问答首页 >准确地理解data.table何时是对另一个data.table的引用(而不是它的副本

准确地理解data.table何时是对另一个data.table的引用(而不是它的副本
EN

Stack Overflow用户
提问于 2012-04-19 17:19:31
回答 2查看 38.5K关注 0票数 212

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

在从另一个data.table创建data.table时(通过<-,然后通过:=更新新表),原始表也会被更改。这是预期的,根据以下内容:

?data.table::copystackoverflow: pass-by-reference-the-operator-in-the-data-table-package

下面是一个例子:

代码语言:javascript
复制
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现在不再被修改:

代码语言:javascript
复制
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是如何处理这些操作的,以确保我不会在我的代码中引入潜在的bug。

如果有人能给我解释一下,我将不胜感激。

EN

回答 2

Stack Overflow用户

回答已采纳

发布于 2012-04-19 18:49:25

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

代码语言:javascript
复制
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,而不仅仅是更改需要更改的元素。对于大数据来说,避免这种情况很重要,这也是为什么在data.table中引入了:=set()

现在,使用我们复制的newDT,我们可以通过引用来修改它:

代码语言:javascript
复制
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个十六进制值(列点的向量以及两列中的每一列)都保持不变。因此,它确实是通过引用进行了修改,根本没有副本。

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

代码语言:javascript
复制
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)。

票数 148
EN

Stack Overflow用户

发布于 2013-01-12 19:52:14

只是一个简短的总结。

带有data.table<-就像是base;也就是说,直到使用<-完成了一个子赋值(比如更改列名或更改一个元素,比如DT[i,j]<-v),才会进行复制。然后,它会像base一样获取整个对象的副本。这就是所谓的写时复制。我想,更为人所知的是分包复制!当您使用特殊的:=运算符或data.table提供的set*函数时,它不会复制。如果你有很大的数据,你可能想用它们来代替。:=set*不会复制data.table,即使在函数中也是如此。

给定此示例数据:

代码语言:javascript
复制
DT <- data.table(a=c(1,2), b=c(11,12))

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

代码语言:javascript
复制
DT2 <- DT

这不会复制,也不会在base中复制。它只是标记数据对象,以便R知道两个不同的名称(DT2DT)指向同一个对象。因此R将需要复制对象,如果之后将其中任何一个子赋值给。

这对data.table来说也是完美的。:=不是用来做这件事的。因此,以下是一个故意的错误,因为:=不仅仅用于绑定对象名称:

代码语言:javascript
复制
DT2 := DT    # not what := is for, not defined, gives a nice error

:=是引用赋值的子赋值。但是你不能像在base中那样使用它:

代码语言:javascript
复制
DT[3,"foo"] := newvalue    # not like this

您可以这样使用它:

代码语言:javascript
复制
DT[3,foo:=newvalue]    # like this

这通过引用改变了DT。假设您通过引用数据对象添加了一个新列new,则无需执行此操作:

代码语言:javascript
复制
DT <- DT[,new:=1L]

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

DT可以通过引用、:=甚至在函数内部进行更改:

代码语言:javascript
复制
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是用于大型数据集的。如果你的内存中有一个20 do的data.table,那么你需要一种方法来做到这一点。这是data.table经过深思熟虑的设计决定。

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

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

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

代码语言:javascript
复制
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中编写:=允许您按引用、按组进行子赋值。您可以通过参照按组添加新列。这就是为什么在[...]:=是这样做的:

代码语言:javascript
复制
DT[, newcol:=mean(x), by=group]
票数 111
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/10225098

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档