将引用分配到已加载的包数据集中

19
我正在创建一个使用 data.table 作为数据集并具有一些使用 := 进行引用分配的函数的包。
我已经构建了一个简单的包来演示我的问题。
 library(devtools)
 install_github('foo','mnel')

它包含两个函数

foo <- function(x){
  x[, a := 1]
}
fooCall <- function(x){
  eval(substitute(x[, a :=1]),parent.frame(1))
} 

以及一个数据集(非懒加载)DT,使用以下方式创建

DT <- data.table(b = 1:5)
save(DT, file = 'data/DT.rda')

当我安装这个包时,我的理解是foo(DT)应该在DT内赋值。
 library(foo)
 data(DT)
 foo(DT)
   b a
1: 1 1
2: 2 1
3: 3 1
4: 4 1
5: 5 1

# However this has not assigned by reference within `DT`

DT
   b
1: 1
2: 2
3: 3
4: 4
5: 5

如果我使用更加 正确 的方式
tracmem(DT)
DT <- foo(DT)
# This works without copying
DT 
 b a
1: 1 1
2: 2 1
3: 3 1
4: 4 1
5: 5 1
untracemem(DT)

如果我在函数内使用evalsubstitute
fooCall(DT)
   b a
1: 1 1
2: 2 1
3: 3 1
4: 4 1
5: 5 1
# it does assign by reference 
DT
   b a
1: 1 1
2: 2 1
3: 3 1
4: 4 1
5: 5 1

我应该坚持使用

  1. DT <- foo(DT) 或者 eval/substitute 路线,或者
  2. 我是否对 data 如何加载数据集的方式有所误解,即使不是懒惰加载?

从未尝试过更新包中的引用数据!但是,鉴于它们已经 "sealed",包中的数据不是只读的吗?在这里输入 DT 并不意味着它通过引用被赋值了,对吗?DT 可能已经被复制到 .GlobalEnv 中,并且那可能是更新了它的地方。 - Matt Dowle
顺便说一下,tracemem会报告R本身的重复。这个函数很难捕捉到data.table的复制操作,例如当第一次进行过度分配时,因为从技术上讲,那不是完全的复制,而是一种过度分配(虽然只是浅层的复制而非深层的)。 - Matt Dowle
也许可以在包中的数据对象上尝试使用 alloc.col,看看会发生什么。 - Matt Dowle
@MatthewDowle 我认为 data(DT) 在全局环境中创建了一个副本,惰性加载可能意味着数据集被锁定。我不是要更新包中的副本,而是在示例/文档中使用数据集。 - mnel
@MatthewDowle,如下所述library(foo); data(DT); alloc.col(DT); foo(DT)可以按要求工作。 - mnel
显示剩余2条评论
2个回答

11
这与数据集或锁定无关——您可以简单地使用以下方法进行复制。
DT<-unserialize(serialize(data.table(b = 1:5),NULL))
foo(DT)
DT

我猜测这与data.table需要在第一次访问DT时重新创建对象内的extptr有关,但它是在副本上进行的,因此它无法与全局环境中的原始对象共享修改。

[来源 Matthew] 没错。

DT<-unserialize(serialize(data.table(b = 1:3),NULL))
DT
   b
1: 1
2: 2
3: 3
DT[,newcol:=42]
DT                 # Ok. DT rebound to new shallow copy (when direct)
   b newcol
1: 1     42
2: 2     42
3: 3     42

DT<-unserialize(serialize(data.table(b = 1:3),NULL))
foo(DT)
   b a
1: 1 1
2: 2 1
3: 3 1
DT                 # but not ok when via function foo()
   b
1: 1
2: 2
3: 3


DT<-unserialize(serialize(data.table(b = 1:3),NULL))
alloc.col(DT)      # alloc.col needed first
   b
1: 1
2: 2
3: 3
foo(DT)
   b a
1: 1 1
2: 2 1
3: 3 1
DT                 # now it's ok
   b a
1: 1 1
2: 2 1
3: 3 1

或者,不要将 DT 传递到函数中,直接引用它。像使用数据库一样使用 data.table:在 .GlobalEnv 中有几个固定名称的表格。
DT <- unserialize(serialize(data.table(b = 1:5),NULL))
foo <- function() {
   DT[, newcol := 7]
}
foo()
   b newcol
1: 1      7
2: 2      7
3: 3      7
4: 4      7
5: 5      7
DT              # Unserialized data.table now over-allocated and updated ok.
   b newcol
1: 1      7
2: 2      7
3: 3      7
4: 4      7
5: 5      7

2
@Matthew:但请注意,alloc.col() 在函数内部同样无法工作(与上述相同的原因) - 您确实需要一些不尝试伪造引用的东西 - 例如 DT <- DT[TRUE]。这是值得在 data.table 文档中提及的事情,因为反序列化 data.table 对象会创建一个难以追踪的问题(而且它经常发生-在工作区,软件包等)。 - Simon Urbanek
只有一个问题,我相信,如果需要通过引用向未序列化的数据表添加列,从函数内部,并且该表名事先不知道(即需要通过函数参数传递)。我想不出一个例子,在这种情况下调用alloc.col(DT)后立即反序列化是不可能的,但在实践中也是必需的。我倾向于像使用数据库一样使用data.table;即在.GlobalEnv中有几个大型固定名称的表格。请参见新编辑。 - Matt Dowle
@MatthewDowle(和Simon)-- 感谢您的指引,我可以看到您的第二个示例与我的构建适当调用并在正确的上级环境中评估它的想法有些相似。(在我的问题中是fooCall - mnel

5
另一种解决方案是使用inst/extdata保存rda文件(其中包含任意数量的data.table对象),并在data子目录下有一个名为DT.r的文件。
# get the environment from the call to `data()`
env <- get('envir', parent.frame(1))
# load the data
load(system.file('extdata','DT.rda', package= 'foo'), envir = env)
# overallocate (evaluating in correct environment)
if(require(data.table)){
# the contents of `DT.rda` are known, so write out in full
  evalq(alloc.col(DT), envir = env)

}
# clean up so `env` object not present in env environment after calling `data(DT)`
rm(list = c('env'), envir = env)



}

有趣。我在想是否应该增强alloc.col函数,使其也能接受字符向量作为参数?这样它就可以包装load()调用了。我认为你不需要使用data.table::前缀,因为alloc.col已经被导出并且旨在供用户使用。 - Matt Dowle
@MatthewDowle,关于data.table:: fixed的观点很好,已经修订为特定情况,其中load的结果是预先知道的。alloc.col可能还需要一个环境参数。 - mnel
好主意。现在已经提交了增强alloc.colFR#2595 - Matt Dowle

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接