data.table setnames 在调用环境中修改对象

data.table setnames modifying object in calling environment

编辑可能的重复项:

proposed link 有助于理解问题的来源(我已经将其作为第一条评论发布)。 然而,我的问题是如何解决在函数中调用 setnames 的具体问题,而不仅仅是了解发生了什么,它没有直接在那里解决。使用 copy 可能是一个选项,可能还有其他选项。

这个问题到目前为止有 2 个反对票,没有评论,如果我可以改进,请说出来。


data.tables::setnames 通过引用修改值,在下面的情况下它似乎会导致意外行为(至少对我来说是意外的)。

我发现了一种处理它的丑陋方法,但可能有更好、更系统的方法来处理它,所以我希望得到你的建议。

奇怪的行为

df1 <- data.frame(a=1,b="x")
f1 <- function(df2){
  setnames(df2,"b","c")
  df2
}
f1(df1)
#   a c
# 1 1 x
df1
#   a c
# 1 1 x

df1 已修改

如果我复制一份呢?

df1 <- data.frame(a=1,b="x")
f1 <- function(df2){
 df3 <- df2
 setnames(df3,"b","c")
 df3
}
f1(df1)
#   a c
# 1 1 x
df1
#   a c
# 1 1 x

没有

如果我制作了一个副本并且 "pretend to change" 但不这样做怎么办

df1 <- data.frame(a=1,b="x")
f1 <- function(df2){
  df3 <- subset(df2)    # note: it doesn't work with `identity`, EDIT: we can also use `data.table::copy`
  setnames(df3,"b","c")
  df3
}
f1(df1)
#   a c
# 1 1 x
df1
#   a b
# 1 1 x

有效


我应该怎么做?这是一个错误吗?


编辑:我发现 data.table 有一个 copy 函数,它比我使用 subset

更通用,当然也更有效

我认为这是预期的行为,因为 df2 指向与 df1 相同的对象。您可以通过以下方式查看:

library(pryr)
library(data.table)

df1 <- data.frame(a=1,b="x")
address(df1)
#[1] "0000000002892318" (will be different for others)

f_address<-function(df2) print(address(df2))
f_address(df1)
#[1] "0000000002892318"

由于 setnames 通过引用更改输入,当它更改 df2 时,它正在更改 df1df2 指向的对象。

要更改此设置,您可以创建自己的函数来显式复制 df1 然后对其进行修改:

setnames_copy <- function(x, old, new){ 
  y <- copy(x)
  setnames(y, old, new) 
}

f2 <- function(df2){
  setnames_copy(df2, "b", "c")
}
df3 <- f2(df1)
df3
#  a c
#1 1 x

df1
#  a b
#1 1 x

如您所见,df1 未被修改。

受@mike-h 解决方案的启发,这里有一个函数,它的行为类似于 setnames,但如果您在定义输入的环境之外使用 setnames,它会创建一个副本。

library(pryr)
setnames2 <- function(`$$`,old,new,verbose = FALSE){
  var_name <- as.character(substitute(`$$`))
  calling_env_objects  <- sapply(ls(envir=parent.frame(n=2)),. %>% get(envir=parent.frame(n=2)) %>% address)
  copied_object <- calling_env_objects[calling_env_objects == address(`$$`)]
  if(length(copied_object) ==0){          # if the address isn't used in the calling environment's calling environment
    if(verbose){cat("use regular setnames\n")}
    setnames(`$$`,old,new)
  } else if (length(copied_object) ==1){  # if the address is already in use in the calling environment's calling environment
    if(verbose){cat("create copy then use setnames on it and assign to relevant environment\n")}
    y <- copy(`$$`);setnames(y, old, new);assign(var_name,y,envir=parent.frame())
  } else {print("you shouldn't go around naming objects `$$`...")}
}

df1 <- data.frame(a=1,b="x")
f3 <- function(df2){
  setnames2(df2,"b","c",verbose=TRUE)
  print(df2)
}
f3(df1)
# create copy then use setnames on it and assign to relevant environment
#   a c
# 1 1 x
df1
#   a b
# 1 1 x    

f4 <- function(df2){
  df2 <- copy(df2)
  setnames2(df2,"b","c",verbose=TRUE)
  print(df2)
}
f4(df1)
# use regular setnames
#   a c
# 1 1 x
df1
#   a b
# 1 1 x