"Правильний" спосіб вказати необов'язкові аргументи у функціях R


165

Мене цікавить, що є "правильним" способом запису функцій з необов'язковими аргументами в Р. З часом я натрапив на кілька фрагментів коду, які приймають тут інший маршрут, і я не зміг знайти належне (офіційне) положення на цю тему.

До цих пір я писав такі необов'язкові аргументи:

fooBar <- function(x,y=NULL){
  if(!is.null(y)) x <- x+y
  return(x)
}
fooBar(3) # 3
fooBar(3,1.5) # 4.5

Функція просто повертає свій аргумент, якщо тільки xнадається. Він використовує NULLзначення за замовчуванням для другого аргументу, а якщо цього аргументу немає NULL, тоді функція додає два числа.

Крім того, можна написати таку функцію (де другий аргумент потрібно вказати іменем, але замість цього можна unlist(z)або визначити z <- sum(...)):

fooBar <- function(x,...){
  z <- list(...)
  if(!is.null(z$y)) x <- x+z$y
  return(x)
}
fooBar(3) # 3
fooBar(3,y=1.5) # 4.5

Особисто я віддаю перевагу першій версії. Однак я можу бачити добре і погане з обома. Перша версія трохи менш схильна до помилок, але друга може бути використана для включення довільної кількості необов'язкових.

Чи існує "правильний" спосіб вказати необов'язкові аргументи в R? Поки що я зупинився на першому підході, але обидва можуть час від часу відчувати себе трохи "хакітними".


Перегляньте вихідний код, xy.coordsщоб побачити загальновживаний підхід.
Карл Віттофт

5
Вихідний код для xy.coordsзгадуваного Карла Віттофта l можна знайти на xy.coords
RubenLaguna

Відповіді:


129

Ви також missing()можете перевірити, подано аргумент чи ні y:

fooBar <- function(x,y){
    if(missing(y)) {
        x
    } else {
        x + y
    }
}

fooBar(3,1.5)
# [1] 4.5
fooBar(3)
# [1] 3

5
Мені подобається відсутнє краще. особливо якщо у вас багато значень за замовчуванням NULL, у вашій
пакетній

5
@rawr missing()також виразніше в тому сенсі, що він "говорить, що це означає". Плюс це дозволяє користувачам передавати значення NULL у місцях, де це має сенс!
Josh O'Brien

31
Для мене є великий мінус використання пропущеного таким чином: під час скидування аргументів функції ви більше не можете бачити, які аргументи потрібні та які є варіанти.
хадлі

3
@param x numeric; something something; @param y numeric; **optional** something something; @param z logical; **optional** something something
rawr

4
missing()страшно, коли ти хочеш передавати аргументи з однієї функції в іншу.
Джон Сміт

55

Якщо чесно, мені сподобався перший спосіб ОП фактично почати його зі NULLзначення, а потім перевірити його is.null(насамперед тому, що це дуже просто і легко зрозуміти). Можливо, це залежить від способу кодування людей, але, схоже, і Хедлі підтримує такий is.nullспосіб:

З книги Хедлі "Advanced-R" Глава 6, Функції, стор.84 (для онлайн-версії перевірте тут ):

Ви можете визначити, поданий аргумент чи ні з функцією відсутності ().

i <- function(a, b) {
  c(missing(a), missing(b))
}
i()
#> [1] TRUE TRUE
i(a = 1)
#> [1] FALSE  TRUE
i(b = 2)
#> [1]  TRUE FALSE
i(1, 2)
#> [1] FALSE FALSE

Іноді потрібно додати нетривіальне значення за замовчуванням, яке може знадобитися для обчислення кількох рядків коду. Замість того, щоб вставляти цей код у визначення функції, ви можете використовувати пропущений () для умовного обчислення, якщо це необхідно. Однак, це важко зрозуміти, які аргументи потрібні, а які необов’язкові, не уважно читаючи документацію. Замість цього я зазвичай встановлюю значення за замовчуванням NULL і використовую is.null (), щоб перевірити, чи аргумент подано.


2
Цікаво. Це звучить розумно, але чи виникає у вас коли-небудь заплутаність, які аргументи функції потрібні, а які необов’язкові? Я не впевнений, що я коли- небудь насправді мав такий досвід ...
Josh O'Brien

2
@ JoshO'Brien Я думаю, що у мене не було такої проблеми з будь-яким стилем кодування, якщо чесно, принаймні, це ніколи не було головною проблемою, мабуть, через документацію чи читання вихідного коду. І саме тому я в першу чергу кажу, що це дійсно питання стилю кодування, до якого ви звикли. Я використовую цей NULLспосіб досить довгий час, і, мабуть, тому я більше звик до нього, коли бачу вихідні коди. Мені це здається більш природним. Однак, як ви кажете, база R використовує обидва підходи, тому вона дійсно зводиться до індивідуальних переваг.
LyzandeR

2
На сьогодні я дуже хочу, щоб я міг позначити дві відповіді як правильні, тому що для чого я дійсно дійшов, використовуючи і те, is.nullі missingзалежно від контексту та для чого використовується аргумент.
SimonG

5
Це добре @SimonG і спасибі :). Я погоджуюся, що обидва відповіді дуже хороші, і вони часом залежать від контексту. Це дуже гарне запитання, і я вважаю, що відповіді дають дуже хорошу інформацію та знання, які в будь-якому разі є основною метою.
LyzandeR

24

Це мої правила:

Якщо значення за замовчуванням можна обчислити з інших параметрів, використовуйте вирази за замовчуванням, як у:

fun <- function(x,levels=levels(x)){
    blah blah blah
}

якщо інше не використовується

fun <- function(x,levels){
    if(missing(levels)){
        [calculate levels here]
    }
    blah blah blah
}

У рідкісному випадку, якщо ви, звичайно, користувач може захотіти вказати значення за замовчуванням, яке триває цілий R сеанс, використовуйтеgetOption

fun <- function(x,y=getOption('fun.y','initialDefault')){# or getOption('pkg.fun.y',defaultValue)
    blah blah blah
}

Якщо деякі параметри застосовуються залежно від класу першого аргументу, використовуйте загальний S3:

fun <- function(...)
    UseMethod(...)


fun.character <- function(x,y,z){# y and z only apply when x is character
   blah blah blah 
}

fun.numeric <- function(x,a,b){# a and b only apply when x is numeric
   blah blah blah 
}

fun.default <- function(x,m,n){# otherwise arguments m and n apply
   blah blah blah 
}

Використовуйте ...лише тоді, коли ви передаєте додаткові параметри іншій функції

cat0 <- function(...)
    cat(...,sep = '')

Нарешті, якщо ви вибираєте використання, ...не передаючи крапки в іншу функцію, попередити користувача, що ваша функція ігнорує будь-які невикористані параметри, оскільки в іншому випадку це може бути дуже заплутаним:

fun <- (x,...){
    params <- list(...)
    optionalParamNames <- letters
    unusedParams <- setdiff(names(params),optionalParamNames)
    if(length(unusedParams))
        stop('unused parameters',paste(unusedParams,collapse = ', '))
   blah blah blah 
}

Варіант методу s3 був одним із перших, що мені теж прийшло в голову
rawr

2
З ретроспективою, я захопився методом ОП присвоєння NULLфункції підпису, оскільки це зручніше для створення функцій, які гарно ланцюжком .
Jthorpe

10

Існує кілька варіантів, і жоден з них не є офіційним правильним способом, і жоден з них насправді є невірним, хоча вони можуть передавати різну інформацію на комп’ютер та інших, хто читає ваш код.

Для даного прикладу, я думаю, що найяскравішим варіантом було б надати ідентифікаційне значення за замовчуванням, у цьому випадку зробіть щось на кшталт:

fooBar <- function(x, y=0) {
  x + y
}

Це найкоротший із показаних досі варіантів, і стислість може сприяти читанню (а іноді навіть швидкості виконання). Зрозуміло, що повертається сума x і y, і ви можете бачити, що y не задано значення, яке буде 0, яке при додаванні до x просто призведе до x. Очевидно, якщо використовується щось складніше, ніж додавання, тоді знадобиться інше значення ідентичності (якщо таке існує).

Одне, що мені дуже подобається в цьому підході, - це те, що зрозуміло, що таке значення за замовчуванням при використанні argsфункції або навіть при перегляді файлу довідки (не потрібно прокручувати донизу деталі, це саме там у використанні ).

Недолік цього методу полягає в тому, що, коли значення за замовчуванням є складним (вимагає декількох рядків коду), це, ймовірно, знизить читабельність, щоб спробувати вкласти все це у значення за замовчуванням, і / missingабо NULLпідходи стануть набагато розумнішими.

Деякі інші відмінності між методами з’являться при передачі параметра до іншої функції або при використанні match.callабо sys.callфункцій.

Тому я думаю, що "правильний" метод залежить від того, що ви плануєте зробити з цим конкретним аргументом і якою інформацією ви хочете передати читачам свого коду.


7

Я б вважав за краще використовувати NULL для ясності того, що потрібно, а що не є обов'язковим. Одне слово попередження про використання значень за замовчуванням, які залежать від інших аргументів, як запропонував Jthorpe. Значення не встановлюється під час виклику функції, а коли вперше посилається на аргумент! Наприклад:

foo <- function(x,y=length(x)){
    x <- x[1:10]
    print(y)
}
foo(1:20) 
#[1] 10

З іншого боку, якщо ви посилаєтесь y перед зміною x:

foo <- function(x,y=length(x)){
    print(y)
    x <- x[1:10]
}
foo(1:20) 
#[1] 20

Це трохи небезпечно, тому що важко відслідковувати те, що "y" ініціалізується так, ніби це не викликається на початку функції.


7

Просто хотілося зазначити, що вбудована sinkфункція має хороші приклади різних способів встановлення аргументів у функції:

> sink
function (file = NULL, append = FALSE, type = c("output", "message"),
    split = FALSE)
{
    type <- match.arg(type)
    if (type == "message") {
        if (is.null(file))
            file <- stderr()
        else if (!inherits(file, "connection") || !isOpen(file))
            stop("'file' must be NULL or an already open connection")
        if (split)
            stop("cannot split the message connection")
        .Internal(sink(file, FALSE, TRUE, FALSE))
    }
    else {
        closeOnExit <- FALSE
        if (is.null(file))
            file <- -1L
        else if (is.character(file)) {
            file <- file(file, ifelse(append, "a", "w"))
            closeOnExit <- TRUE
        }
        else if (!inherits(file, "connection"))
            stop("'file' must be NULL, a connection or a character string")
        .Internal(sink(file, closeOnExit, FALSE, split))
    }
}

1

як щодо цього?

fun <- function(x, ...){
  y=NULL
  parms=list(...)
  for (name in names(parms) ) {
    assign(name, parms[[name]])
  }
  print(is.null(y))
}

Потім спробуйте:

> fun(1,y=4)
[1] FALSE
> fun(1)
[1] TRUE
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.