【问题标题】:Is R data.table documented to pass by reference as argument?R data.table 是否记录为通过引用作为参数传递?
【发布时间】:2020-10-25 15:29:30
【问题描述】:

检查这个玩具代码:

> x <- data.table(a = 1:2) 
> foo <- function(z) { z[, b:=3:4]  }
> y <- foo(x)
> x[]
   a b
1: 1 3
2: 2 4

似乎 data.table 是通过引用传递的。这是故意的吗?这有记录吗?我确实阅读了文档,但找不到有关此行为的提及。

不是询问 R 的文档化引用语义(在 :=set*** 和其他一些)。我在问是否应该将 data.table 完整对象作为函数参数通过引用传递。


编辑:按照@Oliver 的回答,这里有一些更奇怪的例子。

> dt<- data.table(a=1:2)
> attr(dt, ".internal.selfref")
<pointer: 0x564776a93e88>
> address(dt)
[1] "0x5647bc0f6c50"
> 
> ff<-function(x) { x[, b:=3:4]; print(address(x)); print(attr(dt, ".internal.selfref")) }
> ff(dt)
[1] "0x5647bc0f6c50"
<pointer: 0x564776a93e88>

所以不仅.internal.selfref 与调用者的dt 副本相同,地址也是如此。它确实是同一个对象。 (我认为)。

data.frames 并非完全如此:

> df<- data.frame(a=1:2)
> address(df)
[1] "0x5647b39d21e8"
> ff<-function(x) { print(address(x)); x$b=3:4; print(address(x)) }
> 
> ff(df)
[1] "0x5647b39d21e8"
[1] "0x5647ae24de78"

也许根本问题是常规 data.table 操作不会触发 R 的 copy-on-modify 语义?

【问题讨论】:

  • @Roland 谢谢 - 但这个小插图(以及我能找到的所有其他文档)没有将 data.table 语义作为函数参数讨论
  • 再读一遍。它确实准确地描述了您在问题中表现出的行为。
  • @Roland 你说得对,那里提到过,我错过了。没有真正解释,但明确提及。

标签: r data.table


【解决方案1】:

确实,在文档中很难找到这个问题的明确答案。

您所经历的似乎确实是data.table 的传递引用行为。但这并不完全像人们想象的那样。这是我们正在经历的set*:=[.data.table 的行为,这在?copy 中有记录(尽管不是以一种可能仍然有点不清楚的方式)。基本上(据我所知)data.table 有一个自引用指针,这些函数都使用这个指针来覆盖现有的data.table,而不是创建一个副本。使用非标准评估避免了浅拷贝。

我们可以用一系列的例子来说明发生了什么:

示例 1:使用 set 覆盖原始对象。

library(data.table)
dt <- data.table(a = 1:3)

## Example 1:
### add by reference. A shallow copy is taken by R-intervals
### But the self-referncing pointer still points to the old object (original table is overwritten) 
test1 <- function(x){
  # Add column to existing dt by reference (similar to using `set`)
  x[, b := seq(.N)]
}
test1(dt)  
dt
   a b
1: 1 1
2: 2 2
3: 3 3

这与问题中报告的结果相同。这里发生的情况似乎是,set 方法在内部使用指向对象的指针,该指针再次指向原始对象。
请注意,这里我使用[.data.table:=。使用set(x, j = 'b', value = seq(nrow(x))) 可以获得相同的结果。

从这个对象中,我们还可以看到位于属性中的自引用指针(注意示例 3 中的指针地址)

attributes(dt)
$names
[1] "a" "b"

$row.names
[1] 1 2 3

$class
[1] "data.table" "data.frame"

$.internal.selfref
<pointer: 0x0000017e19811ef0>

示例 2:使用 [[ 使用从 list 继承的方法并创建一个副本

test2 <- function(x){
  x[['c']] <- seq(nrow(x))
  x
}
dt2 <- test2(dt)
dt   
   a b
1: 1 1
2: 2 2
3: 3 3

在这个例子中,我们可以看到,尽管我创建了一个新列,但它并没有暴露给原始表。此方法不覆盖现有对象的原因似乎是,在 data.table 包中没有定义为使用 set[[.data.table&lt;- 方法。因此,它回退到[[.list&lt;-,它不知道表中的自引用指针,因此在原始表中不会生成新列。而是创建一个副本,该副本具有与原始表相同的属性,包括引用指针。

示例 3:在示例 2 的结果中添加新列

我们在示例 2 中看到的行为导致了一些有趣的行为。首先我们可以确认指针是相同的

attributes(dt2)
identical(attr(dt, '.internal.selfref'), attr(dt2, '.internal.selfref'))

但如果我们随后尝试向dt2 添加新信息,我们将收到警告

dt2[, d := 1:3]

警告信息:
在 `[.data.table`(dt2, , `:=`(d, 1:3)) 中: 通过获取 data.table 的(浅)副本检测和修复无效的 .internal.selfref ,以便 := 可以通过引用添加此新列。在较早的时候,这个 data.table 已被 R 复制(或使用 structure() 或类似方法手动创建)。避免 names

从中我们可以看到data.table 的开发人员非常清楚这种行为,或者至少知道这是一种可能的危险。所以从这里我们可以看出

  1. no data.tables 实际上不是通过引用传递的。驻留在属性中的自引用指针通过引用传递,然后用于覆盖原始表中的列
  2. 这可能是预期行为,data.table 的用户应该注意这一点。
  3. 如果希望在函数中将[.data.table:=set 一起使用,则应创建dt &lt;- copy(dt) 或在函数文档中明确声明它被引用覆盖。

有关 R 如何处理指针的更多信息,我相信手册 Writing R extensionsRcpp 小插曲都描述了它们的行为,尽管指针通常被认为是一个高级主题并且可能导致意外行为。

【讨论】:

  • 谢谢!很好的见解。我不完全确定这是它的底部 - 请查看问题的编辑(评论太长)
【解决方案2】:

我认为您感到惊讶的是实际上是 R 行为,这就是为什么它没有在 data.table 中专门记录的原因(也许无论如何都应该如此,因为其含义对 data.table 更重要)。

您很惊讶传递给函数的对象具有相同的地址,但base R 也是如此:

x = 1:10
address(x)
# [1] "0x7fb7d4b6c820"
(function(y) {print(address(y))})(x)
# [1] "0x7fb7d4b6c820"

函数环境中复制的是指向x指针。此外,对于base R,父 x 是不可变的:

foo = function(y) {
  print(address(y))
  y[1L] = 2L
  print(address(y))
}
foo(x)
# [1] "0x7fb7d4b6c820"
# [1] "0x7fb7d4e11d28"

也就是说,只要我们尝试编辑 y,就会生成一个副本。这与引用计数有关——你可以看到 Luke Tierney 在这方面所做的一些工作,e.g. this presentation

data.table 的不同之处在于data.table启用了父对象的编辑权限——我想你知道这是一把双刃剑。

【讨论】:

  • address sn-ps 不是导致我说 data.tables 是按值传递的原因,而是它们内容的变化。所以基本上是的,“data.table 操作以某种方式不会触发 R 的修改时复制语义”。谢谢!
猜你喜欢
  • 1970-01-01
  • 2018-08-13
  • 2017-07-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多