Есть ли способ использовать два оператора "..." в функции в R?

Я хочу написать функцию, которая вызывает оба plot() а также legend() и было бы идеально, если бы пользователь мог указать ряд дополнительных аргументов, которые затем передаются либо plot() или же legend(), Я знаю, что могу добиться этого для одной из двух функций, используя ...:

foo.plot <- function(x,y,...) {
    plot(x,y,...)
    legend("bottomleft", "bar", pch=1)
}

foo.plot(1,1, xaxt = "n")

Это проходит xaxt = "n" построить. Но есть ли способ, например, чтобы передать, например, title = "legend" к legend() вызывать без указания аргументов в заголовке функции?


Обновление от принятого ответа: я думал, что путь ВитошКа был самым элегантным, чтобы выполнить то, что я хотел. Однако были некоторые незначительные проблемы, с которыми мне приходилось обходиться, пока все не заработало, как я хотел.

Сначала я проверил, какой из параметров я хочу передать legend и какой plot, Первым шагом в этом направлении было выяснить, какие аргументы legend уникальны для legend а не часть сюжета и / или номинала:

legend.args <- names(formals(legend))
plot.args <- c(names(formals(plot.default)), names(par()))
dput(legend.args[!(legend.args %in% plot.args)])

я использую dput() здесь, потому что линия plot.args <- c(names(formals(plot.default)), names(par())) всегда вызывает новый пустой сюжет, который я не хотел. Итак, я использовал вывод dput в следующей функции.

Далее мне пришлось иметь дело с перекрывающимися аргументами (получить их через dput(largs.all[(largs.all %in% pargs.all)])). Для некоторых это было тривиально (например, x, y) другие передаются обеим функциям (например, pch). Но в моем реальном приложении я даже использую другие стратегии (например, разные имена переменных для adj, но не реализовано в этом примере).

Наконец, do.call Функция должна была быть изменена двумя способами. Во-первых, какая часть (то есть вызываемые функции) должна быть символом (т.е. 'plot' вместо plot). И список аргументов должен быть построен немного иначе.

foo.plot <- function(x,y,...) {
    leg.args.unique <- c("legend", "fill", "border", "angle", "density", "box.lwd", "box.lty", "box.col", "pt.bg", "pt.cex", "pt.lwd", "xjust", "yjust", "x.intersp", "y.intersp", "text.width", "text.col", "merge", "trace", "plot", "ncol", "horiz", "title", "inset", "title.col", "title.adj")
    leg.args.all <- c(leg.args.unique, "col", "lty", "lwd", "pch", "bty", "bg", "cex", "adj", "xpd")
    dots <- list(...)
    do.call('plot', c(list(x = x, y = x), dots[!(names(dots) %in% leg.args.unique)]))
    do.call('legend', c(list("bottomleft", "bar"), dots[names(dots) %in% leg.args.all]))
}


foo.plot(1,1,pch = 4, title = "legendary", ylim = c(0, 5))

В этом примере pch передается обоим plot а также legend, title передается только legend, а также ylim только для того, чтобы plot,


Обновление 2 основано на комментарии Гэвина Симпсона (см. Также комментарии в ответе Витошки):
(Я) Это правильно.
(ii) Это всегда может быть персонаж. Но если у вас есть переменная с тем же именем, что и у функции, то вам нужно заключить имя функции в кавычки do.call:

min.plot <- function(x,y,plot=TRUE) if(plot == TRUE) do.call(plot, list(x = x, y = y))
min.plot(1,1)
Error in do.call(plot, list(x = x, y = y)) : 
  'what' must be a character string or a function

(iii) Вы можете использовать c(x = 1, y = 1, list()) и работает нормально. Однако то, что я действительно сделал (не в приведенном мной примере, а в своей реальной функции), заключается в следующем: c(x = 1, y = 1, xlim = c(0, 2), list(bla='foo'))
Пожалуйста, сравните это с: c(list(x = 1, y = 1, xlim = c(0, 2)), list(bla='foo'))
В первом случае список содержит два элемента xlim, xlim1 а также xlim2 (каждый скаляр), в последнем случае список имеет только xlim (который является вектором длины 2, что я и хотел).

Итак, вы правы во всех ваших пунктах для моего примера. Но для моей реальной функции (с гораздо большим количеством переменных) я столкнулся с этими проблемами и хотел документировать их здесь. Извините за неточность.

6 ответов

Решение

Автоматический способ:

foo.plot <- function(x,y,...) {
    lnames <- names(formals(legend))
    pnames <- c(names(formals(plot.default)), names(par()))
    dots <- list(...)
    do.call('plot', c(list(x = x, y = x), dots[names(dots) %in% pnames]))
    do.call('legend', c("bottomleft", "bar", pch = 1, dots[names(dots) %in% lnames]))
}

pch должен быть отфильтрован от lnames, чтобы избежать дублирования в legend Позвоните на случай, если пользователь предоставит "pch", но вы поняли. Отредактировано в январе 2012 года Карлом В. "do.call" работает только с функциями в кавычках, как в обновлениях Хенрика. Я отредактировал это здесь, чтобы избежать путаницы в будущем.

Эти вещи становятся хитрыми, и нет простых решений без указания дополнительных аргументов в вашей функции. Если у тебя есть ... в обоих plot а также legend звонки, которые вы в конечном итоге получите предупреждения при переходе в legend-специфичные аргументы. Например, с:

foo.plot <- function(x,y,...) {
    plot(x,y,...)
    legend("bottomleft", "bar", pch = 1, ...)
}

Вы получаете следующие предупреждения:

> foo.plot(1, 1, xjust = 0.5)
Warning messages:
1: In plot.window(...) : "xjust" is not a graphical parameter
2: In plot.xy(xy, type, ...) : "xjust" is not a graphical parameter
3: In axis(side = side, at = at, labels = labels, ...) :
  "xjust" is not a graphical parameter
4: In axis(side = side, at = at, labels = labels, ...) :
  "xjust" is not a graphical parameter
5: In box(...) : "xjust" is not a graphical parameter
6: In title(...) : "xjust" is not a graphical parameter

Есть способы обойти эту проблему, смотрите plot.default и его локальные функции, определенные как обертки вокруг таких функций, как axis, box и т.д., где у вас будет что-то вроде localPlot() обертка, встроенная функция и вызовите это, а не plot() непосредственно.

bar.plot <- function(x, y, pch = 1, ...) {
    localPlot <- function(..., legend, fill, border, angle, density,
                          xjust, yjust, x.intersp, y.intersp,
                          text.width, text.col, merge, trace, plot = TRUE, ncol,
                          horiz, title, inset, title.col, box.lwd,
                          box.lty, box.col, pt.bg, pt.cex, pt.lwd) plot(...)
    localPlot(x, y, pch = pch, ...)
    legend(x = "bottomleft", legend = "bar", pch = pch, ...)
}

(Довольно почему 'plot' Аргумент требует, что значение по умолчанию выше моего понимания, но оно не будет работать, если не указать его по умолчанию. TRUE.)

Теперь это работает без предупреждений:

bar.plot(1, 1, xjust = 0.5, title = "foobar", pch = 3)

Как вы обрабатываете графические параметры, такие как bty например будет до вас - bty будет влиять на тип окна графика и тип окна легенды. Обратите внимание, что я справился 'pch' иначе, потому что если кто-то использует этот аргумент в bar.plot() позвоните, вы будете я), используя разные символы в легенде / сюжете, и вы получите предупреждение или ошибку о 'pch' соответствие дважды.

Как вы можете видеть, это становится довольно сложно...


Ответ Joris предоставляет интересное решение, которое, как я прокомментировал, напомнило мне аргументы списков управления в таких функциях, как lme(), Вот моя версия Joris' Answer, реализующая идею в виде этого списка управления:

la.args <- function(x = "bottomleft", legend = "bar", pch = 1, ...)
    c(list(x = x, legend = legend, pch = pch), list(...))

foo.plot <- function(x,y, legend.args = la.args(), ...) {
    plot(x, y, ...)
    do.call(legend, legend.args)
}

Который работает следующим образом, используя второй пример вызова Джори, соответствующим образом модифицированный:

foo.plot(1,1, xaxt = "n", legend.args=la.args(bg = "yellow", title = "legend"))

Вы можете быть настолько полны, насколько вам нравится при настройке la.args() функция - здесь я только устанавливаю значения по умолчанию для аргументов, которые установил Джорис, и объединяю любые другие. Было бы проще, если la.args() содержал все аргументы легенды со значениями по умолчанию.

Одним из способов является использование списков аргументов в сочетании с do.call, Это не самое красивое решение, но оно работает.

foo.plot <- function(x,y,legend.args,...) {
    la <- list(
        x="bottomleft",
        legend="bar",
        pch=1
    )
    if (!missing(legend.args)) la <- c(la,legend.args)
    plot(x,y,...)
    do.call(legend,la)
}
foo.plot(1,1, xaxt = "n")    
foo.plot(1,1, xaxt = "n",legend.args=list(bg="yellow",title="legend"))

Недостатком является то, что вы не можете указать, например, pch=2, например, в списке legend.args. Вы можете обойти это с некоторыми пунктами if, я оставлю это вам, чтобы продолжить возиться с этим.


Изменить: см. Ответ Гэвина Симпсона для лучшей версии этой идеи.

Мы могли бы построить formalize функция, которая сделает любую функцию совместимой с точками:

formalize <- function(f){
  # add ... to formals
  formals(f) <- c(formals(f), alist(...=))
  # release the ... in the local environment
  body(f)    <- substitute({x;y},list(x = quote(list2env(list(...))),y = body(f)))
  f
}

foo.plot <- function(x,y,...) {
  legend <- formalize(legend) # definition is changed locally
  plot(x,y,...)
  legend("bottomleft", "bar", pch = 1, ...)
}    

foo.plot(1,1, xaxt = "n", title = "legend")

Он показывает предупреждения, потому что plot получает аргументы, которых он не ожидает.

Мы могли бы использовать suppressWarnings на вызове plot (но он, очевидно, будет подавлять все предупреждения), или мы могли бы разработать другую функцию для plot Функция (или любая функция) локально более терпима к точкам:

casualize <- function(f){
  f_name <- as.character(substitute(f))
  f_ns   <- getNamespaceName(environment(f))
  body(f) <- substitute({
      # extract all args
      args <- as.list(match.call()[-1]);
      # relevant args only
      args <- args[intersect(names(formals()),names(args))]
      # call initial fun with relevant args
      do.call(getExportedValue(f_ns, f_name),args)})
  f
}

foo.plot <- function(x,y,...) {
  legend <- formalize(legend)
  plot   <- casualize(plot)
  plot(x,y,...)
  legend("bottomleft", "bar", pch = 1, ...)
}

foo.plot(1,1, xaxt = "n", title = "legend")

Теперь все работает отлично!

Аналогичное решение для принятого ответа, но в этом используетсяlist2()и оператор сплайсинга!!!(плюс его вспомогательная функцияinject()) из rlangупаковка. Его преимущество заключается в том, что не нужно объединять именованные или жестко закодированные аргументы в список «точек».

      foo.plot <- function(x, y, leg.pos='bottomleft', legend='bar', ...) {
    #.Pars.readonly <- c("cin", "cra", "csi", "cxy", "din", "page")  # from par()
    eval(body(par)[[2]])  # .Pars.readonly <- c(...)

    this_params <- names(formals())
    par_params <- setdiff(graphics:::.Pars, .Pars.readonly)
    plot_params <- setdiff(union(formalArgs(plot.default), par_params), this_params)
    legend_params <- setdiff(formalArgs(graphics::legend), this_params)

    dots <- rlang::list2(...)
    plot_args <- dots[names(dots) %in% plot_params]
    legend_args <- dots[names(dots) %in% legend_params]

    rlang::inject(plot.default(x, y, !!!plot_args))
    rlang::inject(graphics::legend(leg.pos, legend=legend, !!!legend_args))
}

Просто хотел добавить общее решение, которое я реализовал. Я написал функцию dots_parser, которая принимает функцию, которую вы хотите вызвать, любые именованные аргументы и многоточие, а затем запускает функцию только с соответствующими аргументами.

      dots_parser <- function(FUN, ...) {
  argnames <- names(formals(FUN))
  dots <- list(...)
  return(do.call(FUN, dots[names(dots) %in% argnames]))
}

Так, например, если бы код ранее выглядел так:

      func1(a = myvaluea, b = myvalueb, ...)

Но это возвращает ошибку, потому что ... включает аргументы, не принятые func1, теперь это работает:

      dots_parser(func1, a = myvaluea, b = myvalueb, ...)
Другие вопросы по тегам