Modo "corretto" per specificare argomenti opzionali nelle funzioni R.


165

Sono interessato a qual è il modo "corretto" di scrivere funzioni con argomenti opzionali in R. Nel corso del tempo, mi sono imbattuto in alcuni pezzi di codice che prendono una strada diversa qui e non sono riuscito a trovare una posizione (ufficiale) corretta su questo argomento.

Fino ad ora, ho scritto argomenti opzionali come questo:

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

La funzione restituisce semplicemente il suo argomento se xviene fornita solo . Utilizza un NULLvalore predefinito per il secondo argomento e se l'argomento non lo è NULL, la funzione aggiunge i due numeri.

In alternativa, si potrebbe scrivere la funzione in questo modo (dove il secondo argomento deve essere specificato per nome, ma si potrebbe anche unlist(z)o definire z <- sum(...)invece):

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

Personalmente preferisco la prima versione. Tuttavia, posso vedere il bene e il male con entrambi. La prima versione è un po 'meno soggetta a errori, ma la seconda potrebbe essere utilizzata per incorporare un numero arbitrario di opzioni.

Esiste un modo "corretto" per specificare argomenti opzionali in R? Finora, ho optato per il primo approccio, ma a volte entrambi possono sentire un po '"confuso".


Controlla il codice sorgente per xy.coordsvedere un approccio comunemente usato.
Carl Witthoft,

5
Il codice sorgente xy.coordscitato da Carl Witthoft l si trova su xy.coords
RubenLaguna,

Risposte:


129

È inoltre possibile utilizzare missing()per verificare se l'argomento è ystato fornito o meno :

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

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

5
Mi piace perdere meglio. specialmente se hai molti valori NULL predefiniti, non avrai x = NULL, y = NULL, z = NULL nella documentazione del pacchetto
rawr

5
@rawr missing()è anche più espressivo, nel senso che "dice cosa significa". Inoltre, consente agli utenti di passare un valore NULL, in luoghi dove ciò ha senso!
Josh O'Brien,

31
Per me, c'è un grande svantaggio nell'usare il perdere in questo modo: quando si scremano gli argomenti della funzione non è più possibile vedere quali argomenti sono richiesti e quali sono le opzioni.
Hadley,

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

4
missing()è terribile quando si desidera passare argomenti da una funzione all'altra.
John Smith,

55

Ad essere sincero, mi piace il primo modo in cui l'OP di avviarlo effettivamente con un NULLvalore e poi verificarlo is.null(principalmente perché è molto semplice e facile da capire). Forse dipende dal modo in cui le persone sono abituate alla programmazione, ma l'Hadley sembra supportare anche il is.nullmodo in cui:

Dal libro di Hadley "Advanced-R", capitolo 6, Funzioni, p.84 (per la versione online controlla qui ):

È possibile determinare se un argomento è stato fornito o meno con la funzione missing ().

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

A volte si desidera aggiungere un valore predefinito non banale, che potrebbe richiedere diverse righe di codice per il calcolo. Invece di inserire quel codice nella definizione della funzione, è possibile utilizzare missing () per calcolarlo in modo condizionale, se necessario. Tuttavia, ciò rende difficile sapere quali argomenti sono richiesti e quali sono facoltativi senza leggere attentamente la documentazione. Invece, di solito imposto il valore predefinito su NULL e utilizzo is.null () per verificare se l'argomento è stato fornito.


2
Interessante. Sembra ragionevole, ma ti sei mai lasciato perplesso su quali argomenti sono necessari per una funzione e quali sono facoltativi? Non sono sicuro di aver mai avuto quell'esperienza ...
Josh O'Brien,

2
@ JoshO'Brien Penso di non aver avuto quel problema con nessuno dei due stili di programmazione, a dire il vero, almeno non è mai stato un grosso problema probabilmente a causa della documentazione o della lettura del codice sorgente. Ed è per questo che dico principalmente che si tratta davvero dello stile di codifica a cui sei abituato. Ho usato il NULLmodo per un bel po 'e probabilmente è per questo che sono più abituato quando vedo i codici sorgente. Mi sembra più naturale. Detto questo, come dici tu la base R adotta entrambi gli approcci, quindi si riduce davvero alle preferenze individuali.
LyzandeR,

2
Ormai, vorrei davvero poter contrassegnare due risposte come corrette perché quello che sono veramente arrivato a usare entrambi is.nulle in missingbase al contesto e a cosa serve l'argomento.
SimonG,

5
Va bene @SimonG e grazie :). Concordo sul fatto che entrambe le risposte sono molto buone e talvolta dipendono dal contesto. Questa è un'ottima domanda e credo che le risposte forniscano ottime informazioni e conoscenze che è comunque l'obiettivo principale qui.
LyzandeR,

24

Queste sono le mie regole pratiche:

Se i valori predefiniti possono essere calcolati da altri parametri, utilizzare le espressioni predefinite come in:

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

se in caso contrario utilizza mancante

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

Nel raro caso in cui un utente desideri specificare un valore predefinito che dura un'intera sessione R, utilizzaregetOption

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

Se alcuni parametri si applicano a seconda della classe del primo argomento, utilizzare un generico 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 
}

Utilizzare ...solo quando si passano parametri aggiuntivi a un'altra funzione

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

Infine, se scegli l'uso ...senza passare i punti su un'altra funzione, avvisa l'utente che la tua funzione sta ignorando tutti i parametri inutilizzati poiché altrimenti potrebbe essere molto confuso:

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

anche l'opzione del metodo s3 è stata una delle prime cose che mi sono venute in mente
rawr

2
Con il senno di poi, mi sono affezionato al metodo di assegnazione dell'OP NULLnella firma della funzione, in quanto è più conveniente per creare funzioni che si incatenano bene.
Jthorpe,

10

Esistono diverse opzioni e nessuna di esse è il modo corretto ufficiale e nessuna di esse è in realtà errata, sebbene possano trasmettere informazioni diverse al computer e ad altri che leggono il tuo codice.

Per l'esempio dato, penso che l'opzione più chiara sarebbe quella di fornire un valore predefinito di identità, in questo caso fare qualcosa del tipo:

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

Questa è la più breve delle opzioni mostrate finora e la brevità può aiutare la leggibilità (e talvolta anche la velocità nell'esecuzione). È chiaro che ciò che viene restituito è la somma di xey e puoi vedere che y non ha un valore che sarà 0 che quando aggiunto a x si tradurrà semplicemente in x. Ovviamente se si utilizza qualcosa di più complicato dell'aggiunta, sarà necessario un valore di identità diverso (se presente).

Una cosa che mi piace molto di questo approccio è che è chiaro quale sia il valore predefinito quando si utilizza la argsfunzione o anche guardando il file della guida (non è necessario scorrere fino ai dettagli, è proprio lì nell'uso ).

Lo svantaggio di questo metodo è quando il valore predefinito è complesso (richiede più righe di codice), quindi probabilmente ridurrebbe la leggibilità per provare a mettere tutto ciò nel valore predefinito e gli approcci missingo NULLdiventeranno molto più ragionevoli.

Alcune delle altre differenze tra i metodi appariranno quando il parametro viene passato ad un'altra funzione o quando si usano le funzioni match.callo sys.call.

Quindi immagino che il metodo "corretto" dipenda da cosa pensi di fare con quel particolare argomento e da quali informazioni vuoi trasmettere ai lettori del tuo codice.


7

Tenderei a preferire l'uso di NULL per la chiarezza di ciò che è richiesto e di ciò che è facoltativo. Un avvertimento sull'uso di valori predefiniti che dipendono da altri argomenti, come suggerito da Jthorpe. Il valore non viene impostato quando viene chiamata la funzione, ma quando si fa riferimento all'argomento per la prima volta! Per esempio:

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

D'altra parte, se fai riferimento a y prima di cambiare x:

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

Questo è un po 'pericoloso, perché rende difficile tenere traccia di ciò che "y" viene inizializzato come se non fosse chiamato all'inizio nella funzione.


7

Volevo solo sottolineare che la sinkfunzione integrata ha buoni esempi di diversi modi per impostare argomenti in una funzione:

> 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

cosa ne pensi di questo?

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

Quindi prova:

> fun(1,y=4)
[1] FALSE
> fun(1)
[1] TRUE
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.