Отправка `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, но до сих пор я не нашел никаких реализаций этой идеи. Любая помощь или указатели?
Ответы
Ответ 1
Как вы сами упоминаете, использование 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 использует их для двух из своих потенциально многих аргументов рекурсивно.
Ответ 2
Я не думаю, что вы сможете придумать что-то вполне удовлетворяющее. Лучшее, что вы можете сделать, это экспортировать rbind.myclass
, чтобы пользователи могли вызывать его напрямую, не делая mypackage:::rbind.myclass
. Вы можете назвать это чем-то другим, если хотите (dplyr
вызывает свою версию bind_rows
), но если вы решите сделать это, я бы использовал имя, которое вызывает rbind
, например rbind_myclass
.
Даже если вы можете заставить r-core согласиться изменить поведение отправки, чтобы rbind
отправил свой первый аргумент, все еще будут случаи, когда пользователи захотят rbind
несколько объектов вместе с myclass
объект где-то, кроме первого. Как еще пользователи могут отправлять сообщения rbind.myclass(df, df, myclass)
?
Решение data.table
кажется опасным; Я не удивлюсь, если сопровождающие CRAN проведут проверку и не разрешают это в какой-то момент.
Ответ 3
Подход 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
.