Отправка `rbind` и`cbind` для `data.frame`
Фон
Механизм диспетчеризации R
функции rbind()
а также cbind()
это нестандартно. Я исследовал некоторые возможности написания rbind.myclass()
или же cbind.myclass()
функции, когда один из аргументов является data.frame
, но пока у меня нет удовлетворительного подхода. Этот пост концентрируется на rbind
, но то же самое относится и к cbind
,
проблема
Давайте создадим rbind.myclass()
функция, которая просто повторяет, когда это было вызвано.
rbind.myclass <- function(...) "hello from rbind.myclass"
Мы создаем объект класса myclass
и следующие звонки rbind
все правильно отправить rbind.myclass()
a <- "abc"
class(a) <- "myclass"
rbind(a, a)
rbind(a, "d")
rbind(a, 1)
rbind(a, list())
rbind(a, matrix())
Однако, когда один из аргументов (это не обязательно должен быть первым), rbind()
позвоню base::rbind.data.frame()
вместо:
rbind(a, data.frame())
Такое поведение немного удивительно, но оно на самом деле задокументировано в dispatch
раздел rbind()
, Совет, данный там:
Если вы хотите объединить другие объекты с фреймами данных, может потребоваться сначала привести их к фреймам данных.
На практике этот совет может быть трудным для реализации. Преобразование во фрейм данных может удалить важную информацию о классе. Кроме того, пользователь, который может не знать о совете, может застрять с ошибкой или неожиданным результатом после выполнения команды. rbind(a, x)
,
подходы
Предупредить пользователя
Первая возможность состоит в том, чтобы предупредить пользователя, что вызов rbind(a, x)
не должно быть сделано, когда x
это фрейм данных Вместо этого пользователь пакета mypackage
должен сделать явный вызов скрытой функции:
mypackage:::rbind.myclass(a, x)
Это может быть сделано, но пользователь должен помнить, чтобы сделать явный вызов при необходимости. Вызов скрытой функции является последним средством и не должен быть обычной политикой.
перехват rbind
В качестве альтернативы я пытался защитить пользователя, перехватывая отправку. Моей первой попыткой было дать местное определение base::rbind.data.frame()
:
rbind.data.frame <- function(...) "hello from my rbind.data.frame"
rbind(a, data.frame())
rm(rbind.data.frame)
Это не так, как rbind()
не одурачить в вызове rbind.data.frame
от .GlobalEnv
и вызывает base
версия как обычно.
Другая стратегия состоит в том, чтобы переопределить rbind()
с помощью локальной функции, которая была предложена в S3 диспетчеризации `rbind` и` cbind`.
rbind <- function (...) {
if (attr(list(...)[[1]], "class") == "myclass") return(rbind.myclass(...))
else return(base::rbind(...))
}
Это прекрасно работает для отправки rbind.myclass()
так что пользователь теперь может набрать rbind(a, x)
для любого типа объекта x
,
rbind(a, data.frame())
Недостатком является то, что после library(mypackage)
мы получаем сообщение The following objects are masked from ‘package:base’: rbind
,
Хотя технически все работает, как и ожидалось, должны быть лучшие способы, чем base
переопределение функции.
Заключение
Ни одна из вышеперечисленных альтернатив не является удовлетворительной. Я читал об альтернативах, использующих диспетчеризацию S4, но до сих пор я не нашел ни одной реализации этой идеи. Любая помощь или указатели?
3 ответа
Как вы упоминаете, использование S4 было бы хорошим решением, которое хорошо работает. Я недавно не исследовал фреймы данных, так как меня гораздо больше интересуют другие обобщенные матрицы, как в моих давних пакетах CRAN "Матрица" (="рекомендуется", т.е. часть каждого R-распределения), так и в "Rmpfr".
На самом деле даже два разных способа:
1) Rmpfr
использует новый способ определения методов для '...' в rbind () / cbind (). это хорошо задокументировано в ?dotsMethods
(мнемоника: '...' = точки) и реализована в Rmpfr/R/array.R строка 511 ff (например, https://r-forge.r-project.org/scm/viewvc.php/pkg/R/array.R?view=annotate&root=rmpfr)
2) Matrix
использует более старый подход путем определения (S4) методов для rbind2() и cbind2(): если вы читаете ?rbind
он упоминает, что и когда используются rbind2/cbind2. Идея там: "2" означает, что вы определяете методы S4 с сигнатурой для двух ("2") матричных объектов, а rbind / cbind использует их для двух из своих потенциально многих аргументов рекурсивно.
dotsMethod
подход был предложен Мартином Мачлером и реализован в Rmpfr
пакет. Нам нужно определить новый шаблон, класс и метод, используя S4.
setGeneric("rbind", signature = "...")
mychar <- setClass("myclass", slots = c(x = "character"))
b <- mychar(x = "b")
rbind.myclass <- function(...) "hello from rbind.myclass"
setMethod("rbind", "myclass",
function(..., deparse.level = 1) {
args <- list(...)
if(all(vapply(args, is.atomic, NA)))
return( base::cbind(..., deparse.level = deparse.level) )
else
return( rbind.myclass(..., deparse.level = deparse.level))
})
# these work as expected
rbind(b, "d")
rbind(b, b)
rbind(b, matrix())
# this fails in R 3.4.3
rbind(b, data.frame())
Error in rbind2(..1, r) :
no method for coercing this S4 class to a vector
Я не смог устранить ошибку. См. R: Разве универсальные методы не должны работать внутри пакета без его подключения? для связанной проблемы.
Как этот подход переопределяет rbind()
мы получаем предупреждение The following objects are masked from 'package:base': rbind
,
Я не думаю, что вы сможете придумать что-то полностью удовлетворяющее. Лучшее, что вы можете сделать, это экспорт rbind.myclass
так что пользователи могут вызывать его напрямую, не делая mypackage:::rbind.myclass
, Вы можете назвать это как-нибудь еще, если хотите (dplyr
называет свою версию bind_rows
), но если вы решите сделать это, я бы использовал имя, которое вызывает rbind
, лайк rbind_myclass
,
Даже если вы можете заставить r-core согласиться изменить поведение диспетчера, чтобы rbind
рассылки по первому аргументу, все еще будут случаи, когда пользователи захотят rbind
несколько объектов вместе с myclass
Объект где-то, кроме первого. Как еще пользователи могут отправлять rbind.myclass(df, df, myclass)
?
data.table
решение кажется опасным; Я не удивлюсь, если сопровождающие CRAN поставят чек и запретят это в какой-то момент.