Есть ли способ использовать два оператора «...» в функции в R? - PullRequest
36 голосов
/ 08 ноября 2010

Я хочу написать функцию, которая вызывает как 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 пришлось изменить двумя способами. Во-первых, часть what (то есть вызываемые функции) должна быть символом (то есть '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, что является в розыске).

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

Ответы [ 4 ]

22 голосов
/ 09 ноября 2010

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

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» работает только с функциями в кавычках, как в обновлениях Хенрика. Я отредактировал его здесь, чтобы избежать путаницы в будущем.

18 голосов
/ 08 ноября 2010

Эти вещи становятся сложными, и нет простых решений без указания дополнительных аргументов в вашей функции.Если бы у вас было ... в обоих вызовах 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(), вы будете i) использовать разные символы в легенде / сюжете, и вы получите предупреждение или ошибку о '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() содержал все аргументы легенды со значениями по умолчанию.

6 голосов
/ 08 ноября 2010

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


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

2 голосов
/ 19 сентября 2018

Мы могли бы создать функцию 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")

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

...