Обновляет data.table при передаче в качестве аргумента функции

1

Когда я передаю data.table в качестве аргумента функции, я могу обновить эту таблицу «по ссылке» в вызываемой функции, и результаты будут применены к исходному объекту. Однако, если я делаю что-то, что требует «глубокой копии» (например, rbindlist для добавления строк), копия существует только в вызываемой функции. Исходный объект остается в вызывающем кадре без изменений.

library(data.table)
l1 <- function(a1, action='update'){
  b <- l2(a1, action)
  print('l1')
  print(a1)
}
l2 <- function(a2, action){
  c <- l3(a2, action)
  print('l2')
  print(a2)
}
l3 <- function(a3, action){
  if (action == 'update') a3[, col2 := col + 1]
  if (action == 'append') a3 <- rbindlist(list(a3, data.table(col = c(21, 22))), fill=TRUE)
  if (action == 'forceupdate') assign('DT', 
                                      rbindlist(list(a3, data.table(col = c(21, 22))), fill=TRUE),
                                      envir = parent.frame(3))
  print('l3')
  print(a3)
  a3
}
DT <- data.table(col = c(1, 2, 3))
print(DT)
#>    col
#> 1:   1
#> 2:   2
#> 3:   3
l1(DT, 'update')
#> [1] "l3"
#>    col col2
#> 1:   1    2
#> 2:   2    3
#> 3:   3    4
#> [1] "l2"
#>    col col2
#> 1:   1    2
#> 2:   2    3
#> 3:   3    4
#> [1] "l1"
#>    col col2
#> 1:   1    2
#> 2:   2    3
#> 3:   3    4
print(DT)
#>    col col2
#> 1:   1    2
#> 2:   2    3
#> 3:   3    4

l1(DT, 'append')
#> [1] "l3"
#>    col col2
#> 1:   1    2
#> 2:   2    3
#> 3:   3    4
#> 4:  21   NA
#> 5:  22   NA
#> [1] "l2"
#>    col col2
#> 1:   1    2
#> 2:   2    3
#> 3:   3    4
#> [1] "l1"
#>    col col2
#> 1:   1    2
#> 2:   2    3
#> 3:   3    4
print(DT)
#>    col col2
#> 1:   1    2
#> 2:   2    3
#> 3:   3    4

l1(DT, 'forceupdate')
#> [1] "l3"
#>    col col2
#> 1:   1    2
#> 2:   2    3
#> 3:   3    4
#> [1] "l2"
#>    col col2
#> 1:   1    2
#> 2:   2    3
#> 3:   3    4
#> [1] "l1"
#>    col col2
#> 1:   1    2
#> 2:   2    3
#> 3:   3    4
print(DT)
#>    col col2
#> 1:   1    2
#> 2:   2    3
#> 3:   3    4
#> 4:  21   NA
#> 5:  22   NA

Создано 22.04.2021 пакетом REPEX (v1.0.0)

В этом примере есть трехуровневый стек вызовов функций. Аргумент на первом уровне передается вниз по стеку и обновляется в функции l3.

Используя синтаксис обновления data.table, l3 добавляет к объекту новый столбец, в результате чего исходный объект изменяется «на месте», а результаты видны на каждом уровне в стеке вызовов.

Однако, если я добавляю строки с помощью rbindlist, копия создается внутри кадра l3, и это не влияет на исходный объект или любое его представление в родительских вызовах.

Если я назначу изменение «исходному кадру», оно будет там видно, но промежуточные вызовы не видят изменения.

Есть ли способ отразить результаты этой «глубокой копии» в вызывающем стеке?

Еслиassign это путь, я был бы признателен за пример того, как установить имя и среду базового объекта данных, чтобы это назначение можно было выполнить без жесткого кодирования.

  • 0
    Если вы всегда хотите использовать deep copy , просто используйте copy(dt) .
  • 0
    Спасибо, Мир. Однако проблема здесь заключается в эффекте глубокой копии и кадре / среде, в которой оказывается результирующая таблица. Если копирование выполняется локально, то результат не может быть виден вызывающей функцией (ами), а если он присваивается исходному кадру, результат не может быть виден промежуточными функциями. Я бы хотел, чтобы он вел себя так же, как обновление по ссылке.
  • 0
    Я также борюсь с этим именно в той ситуации, когда мне нужно добавить строки в data.table, поэтому мне интересно, появятся ли умные ответы. В настоящее время я иногда выбираю <<- в этой ситуации, которая постепенно выполняет поиск в родительских средах до тех пор, пока не достигнет глобального уровня, где он что-то создаст. Это несовершенно, но может удовлетворить ваши потребности.
Теги:
data.table pass-by-reference
CodeFix

2 ответа

2

(Пожалуйста, не используйте это случайно в производстве. :-)

Я думаю, что этот вопрос действительно дублирует Вставить строку в data.table , Добавить строку по ссылке в конце объекта data.table и Как удалить строку по ссылке в data.table? . Несколько раз было сказано, что возможность добавлять строки по ссылке «можно» сделать, но это нетривиально (и еще не было сделано). Итог, единорогdata.table::insert функция не существует (пока).

Хотя мне не нравится идея использования<<- иassign , вот * хак *, который достигает этого ... с огромными оговорками.

func <- function(.x) {
  nm <- deparse(substitute(.x))
  stopifnot(
    "'.x' must be a whole data.table" =
      nm == make.names(nm)
  )
  env <- parent.frame()
  while (!is.null(env) && !exists(nm, envir=env, inherits=FALSE)) {
    env <- parent.env(env)
  }
  stopifnot(!is.null(env))
  assign(nm, rbindlist(list(.x, .x[1,])), envir=env)
}

Здесь он работает по назначению:

DT <- data.table(col1=1:3,col2=11:13)
DT
#     col1  col2
#    <int> <int>
# 1:     1    11
# 2:     2    12
# 3:     3    13
func(DT)
DT
#     col1  col2
#    <int> <int>
# 1:     1    11
# 2:     2    12
# 3:     3    13
# 4:     1    11

Однако, если мы передаем отфильтрованную / разбитую на подмножество таблицу, мы должны ошибиться:

func(DT[1,])
# Error in func(DT[1, ]) : '.x' must be a whole data.table

Почему? Позвольте мне продемонстрировать. Во-первых, я собираюсь изменитьfunc так что мы найдем оригиналDT , а не только частичное:

func <- function(.x) {
  nm <- deparse(substitute(.x))
  nm <- gsub("[[({].*", "", nm) # turns 'DT[1,]' --> 'DT'
  env <- parent.frame()
  # ... everything else here
}

DT <- data.table(col1=1:3,col2=11:13)
func(DT[1,])
DT
#     col1  col2
#    <int> <int>
# 1:     1    11
# 2:     1    11

DT <- data.table(col1=1:3,col2=11:13)
func(DT[,1])
DT
#     col1
#    <int>
# 1:     1
# 2:     2
# 3:     3
# 4:     1

func(copy(DT))
# Error in assign(nm, rbindlist(list(.x, .x[1, ])), envir = env) : 
#   cannot change value of locked binding for 'copy'

Все три из этих сценариев правдоподобны в обычном коде R, где функции может потребоваться только подмножество данных. И все три демонстрируют опасность попыток сделать вывод об исходном объекте на основе данных, переданных в функцию. Я уверен, что кто-нибудь может добавить логики кfunc таким образом, что он уловил бы некоторые из этих условий, но это выходит за рамки моего уровня * взлома *, чем я уже достиг с вышеупомянутым.

Цель этого упражнения заключалась в том, чтобы продемонстрировать, что в идеальных обстоятельствах, когда вызывающий абонент знает о рисках и всегда передает все (не подмножество / фильтр)data.table , эта функция будет эффективно добавлять строки по ссылке . Но, как мы знаем, это не просто ссылка, а просто скрытие уловки и вздутия памяти за дымом и зеркалами. И теряя немногоdata.table Скрытность / потрясающая эффективность в процессе.

(Пожалуйста, не используйте это случайно в производстве. :-)

Поделиться
Источник
  • 0
    Очень полезный анализ @ r2evans. data.table::insert будет решить проблему, и ваш func адрес последней части моего вопроса. Однако еще одним ограничением этого присваивания является то, что оно обновляет базовую таблицу, но этого не видит промежуточная функция в стеке вызовов, которая передала таблицу «по ссылке».
  • 0
    Правда, с этим много проблем. Я не считаю, что этот ответ является предпочтительным или каноническим подходом, data.table -devs, это просто ** хак *. Альтернативный подход - передать data.table через среду и внести все изменения / дополнения в env. Это допускает семантику по ссылке, но требует немного больше усилий как для вызывающего, так и для вызываемого.
1

Хотя вы не можете добавлять строки по ссылке наdata.table объект, вы можете заранее выделить дополнительные строки для будущих обновлений по ссылке. В зависимости от вашего фактического варианта использования этот подход стоит рассмотреть.

В приведенном ниже примере я начинаю с выделения 1000 строк вDB стол, хотя стартовыйDT имеет только 3 значения. Затем я могу выполнить произвольное количество «вставок», если я не превысил исходное выделение.

## Create a "Data Base" table with an over-allocated number of rows
DB <- data.table(ID = seq_len(1e3),
                 InUse = FALSE,
                 col1 = as.integer(NA),
                 col2 = as.integer(NA), key = "ID")


## Starting Table DT
DT <- data.table(InUse = TRUE,
                 col1 = c(1, 2, 3),
                 col2 = as.integer(NA))

## Add an ID lookup key
DT[, ID := seq_len(.N)]


## Populate the "Data Base" from Starting DT
setkey(DT,ID)
DB[DT, c("InUse","col1","col2") := .(i.InUse,i.col1, i.col2)]


l3 <- function(x, action){
  if (action == 'update') x[, col2 := col1 + 1L]
  if (action == 'append') {
    ## Define rows to be appended
    NR <- data.table(InUse = TRUE,
                     col1 = sample.int(10L,sample.int(n = 3L, size = 1L)),
                     col2 = as.integer(NA))

    ## Allocate unused ID's for new rows
    NR[,ID := seq_len(.N) + x[InUse == TRUE,ID[.N]]]
    
    ## Allocate values for new rows by joining on ID
    x[NR, c("InUse","col1","col2") := .(i.InUse,i.col1, i.col2), on = .(ID)]
    
    }
  
}


set.seed(1L)

DB[InUse == TRUE]
#    ID InUse col1 col2
# 1:  1  TRUE    1   NA
# 2:  2  TRUE    2   NA
# 3:  3  TRUE    3   NA
l3(DB, "update")

DB[InUse == TRUE]
#    ID InUse col1 col2
# 1:  1  TRUE    1    2
# 2:  2  TRUE    2    3
# 3:  3  TRUE    3    4

l3(DB,"append")
l3(DB, "update")
DB[InUse == TRUE]
#    ID InUse col1 col2
# 1:  1  TRUE    1    2
# 2:  2  TRUE    2    3
# 3:  3  TRUE    3    4
# 4:  4  TRUE    4    5

l3(DB,"append")
l3(DB, "update")
DB[InUse == TRUE]
#    ID InUse col1 col2
# 1:  1  TRUE    1    2
# 2:  2  TRUE    2    3
# 3:  3  TRUE    3    4
# 4:  4  TRUE    4    5
# 5:  5  TRUE    1    2
# 6:  6  TRUE    2    3
# 7:  7  TRUE    5    6

На самом деле это достаточно производительно, поскольку создание новых строк, которые будут добавлены, является самой медленной частью процесса. В приведенном ниже примере я упростил функцию, сделав ее «только для вставки», используяdata.table::set за производительность, с двумя аргументами -DB для «базы данных» и таблицыnew со строками, которые нужно вставить.

insert <- function(x, new){
  
  ## Allocate unused ID's for new rows
  set(new, j = "InUse", value = TRUE)
  set(new, j = "ID", value = seq_len(nrow(new)) + x[InUse == FALSE,min(ID)] - 1L)
  setkey(new,ID)
  
  ## Allocate values for new rows
  x[NR, c("InUse","col","col2") := .(i.InUse,i.col, i.col2), on = .(ID)]
  
}

## Create a "Data Base" table with an over-allocated number of rows
DB <- data.table(ID = seq_len(1e5),
                 InUse = FALSE,
                 col1 = as.integer(NA),
                 col2 = as.integer(NA), key = "ID")

NR <- data.table(col = 1L,
                 col2 = 2L)

microbenchmark(insert(DB,NR),times = 1000)
# Unit: milliseconds
#           expr      min       lq     mean   median       uq      max neval
# insert(DB, NR) 4.081703 4.231624 4.645903 4.278256 4.362353 60.53741  1000

Существует множество показателей производительности для «скорости вставки» базы данных, и все они различаются в зависимости от размера / формы вставляемых данных, но 4,6 миллисекунды (217 вставок в секунду) кажутся достаточно быстрыми для многих случаев использования. могу представить (возможно, блестящее приложение) .

Поделиться
Источник
  • 0
    Очень полезно в хорошо контролируемых ситуациях, когда можно гарантировать, что предварительного распределения будет достаточно. Я видел код, который «расширяется» блоками, если предварительное выделение памяти вот-вот будет нарушено, но для этого снова требуется «глубокая копия».

Другие вопросы

CodeFix
Цитата дня

"Завидую тестировщикам: все хотят с ними дружить."

Эндрю Таненбаум