Понимание того, когда data.table является ссылкой на (против копии) другого data.table

155

У меня возникли проблемы с пониманием свойств pass-by-reference data.table. Некоторые операции, похоже, "ломают" ссылку, и я хотел бы точно понять, что происходит.

При создании data.table из другого data.table (через <-, а затем обновив новую таблицу на :=, также изменится исходная таблица. Ожидается, что:

?data.table::copy и https://stackoverflow.com/questions/8030452/pass-by-reference-operator-in-the-data-table-package-modifies-another-data

Вот пример:

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

Итак, кажется, что строка newDT$b[2] <- 200 как-то "ломает" ссылку. Я бы предположил, что это каким-то образом вызывает копию, но я хотел бы полностью понять, как R обрабатывает эти операции, чтобы я не вводил потенциальные ошибки в свой код.

Я был бы очень признателен, если бы кто-нибудь мог мне это объяснить.

  • 1
    Я только что обнаружил эту «особенность», и она ужасна. В интернете широко рекомендуется использовать <- вместо = для базового назначения в R (например, Google: google.github.io/styleguide/Rguide.xml#assignment ). Но это означает, что манипулирование данными.таблицы не будет функционировать так же, как манипулирование фреймами данных, и, следовательно, это далеко не полная замена фрейма данных.
Теги:
data.table
copy
reference
assignment-operator

2 ответа

121
Лучший ответ

Да, это переназначение в R с помощью <- (или = или ->), которое делает копию всего объекта. Вы можете проследить это с помощью 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..

Обратите внимание, что даже скопирован вектор a (другое шестнадцатеричное значение указывает на новую копию вектора), хотя a не был изменен. Было скопировано даже все b, а не просто изменение элементов, которые необходимо изменить. Это важно избегать для больших данных и почему := и 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..

Эти шестнадцатеричные значения совпадают с исходными значениями, которые мы видели для DT выше. Введите example(copy) для получения дополнительных примеров с использованием tracemem и сравнения с data.frame.

Btw, если вы tracemem(DT), затем DT[2,b:=600], вы увидите один экземпляр. Это копия первых 10 строк, которые выполняет метод print. При завершении invisible() или при вызове внутри функции или script метод print не вызывается.

Все это относится и к внутренним функциям; т.е. := и set() не копируются при записи даже внутри функций. Если вам нужно изменить локальную копию, тогда вызовите x=copy(x) в начале функции. Но помните, что data.table - для больших данных (а также более быстрые преимущества программирования для небольших данных). Мы намеренно не хотим копировать большие объекты (когда-либо). В результате нам не нужно учитывать обычное правило 3 * рабочей памяти. Мы стараемся, чтобы рабочая память имела только одну колонку (т.е. Коэффициент рабочей памяти 1/ncol, а не 3).

  • 0
    Когда это поведение желательно?
85

Просто быстро подведите итоги.

<- с data.table - это как основание; то есть копирование не выполняется до тех пор, пока субсайт не будет выполнен после <- (например, изменение имен столбцов или изменение элемента, такого как DT[i,j]<-v). Затем он берет копию всего объекта, как базу. Это называется копированием на запись. Думаю, это будет лучше известно как копирование на субассигнат! Он НЕ копируется при использовании специального оператора := или функций set*, предоставляемых data.table. Если у вас большие данные, вы, вероятно, захотите их использовать. := и set* НЕ КОПИРУЕТСЯ data.table, ДАЖЕ НЕ РАБОТАЕТ.

Учитывая этот пример данных:

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

Следующее просто "связывает" другое имя DT2 с той же привязкой объекта данных, привязанной в настоящее время к имени DT:

DT2 <- DT

Это никогда не копирует и никогда не копирует в базе. Он просто отмечает объект данных, так что R знает, что два разных имени (DT2 и DT) указывают на один и тот же объект. И поэтому R необходимо будет скопировать объект, если он впоследствии назначен.

Это тоже идеально подходит для 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

Это изменило DT по ссылке. Скажем, вы добавляете новый столбец new по ссылке на объект данных, нет необходимости делать это:

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

потому что RHS уже изменил DT по ссылке. Дополнительный DT <- заключается в неправильном понимании того, что делает :=. Вы можете написать его там, но это лишнее.

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.table в памяти, вам нужен способ сделать это. Это очень продуманное дизайнерское решение data.table.

Конечно, можно сделать копии. Вам просто нужно сообщить data.table, что вы уверены, что хотите скопировать 20-гигабайтный набор данных, используя функцию copy():

DT3 <- copy(DT)   # rather than DT3 <- DT
DT3[,new3:=3L]     # now, this just changes DT3 because it a copy, not DT too.

Чтобы избежать копирования, не используйте назначение базового типа или обновление:

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, как это позволяет вам переназначить по ссылке группой. Вы можете добавить новый столбец по ссылке группы. Итак, почему := выполняется таким образом внутри [...]:

DT[, newcol:=mean(x), by=group]
  • 0
    это должно быть DT3 вместо DT в DT[,new3:=3L] . Невозможно изменить только один символ в вашем ответе.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню