web-dev-qa-db-fra.com

Manière "correcte" de spécifier des arguments optionnels dans les fonctions R

Je suis intéressé par la manière "correcte" d'écrire des fonctions avec des arguments optionnels dans R. Au fil du temps, je suis tombé sur quelques morceaux de code qui empruntent un chemin différent, et je ne pouvais pas trouver une position (officielle) appropriée. sur ce sujet.

Jusqu'à présent, j'ai écrit des arguments optionnels comme ceci:

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

La fonction renvoie simplement son argument si seul x est fourni. Il utilise une valeur par défaut NULL pour le deuxième argument et si cet argument n'est pas NULL, la fonction ajoute les deux nombres.

Alternativement, on pourrait écrire la fonction comme ceci (où le second argument doit être spécifié par nom, mais on pourrait aussi unlist(z) ou définir z <- sum(...) à la place):

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

Personnellement je préfère la première version. Cependant, je peux voir le bien et le mal avec les deux. La première version est un peu moins sujette aux erreurs, mais la seconde pourrait être utilisée pour incorporer un nombre arbitraire d’options.

Existe-t-il un moyen "correct" de spécifier des arguments optionnels dans R? Jusqu'ici, j'ai opté pour la première approche, mais les deux peuvent parfois se sentir un peu "hacky".

142
SimonG

Vous pouvez également utiliser missing() pour vérifier si l'argument y a été fourni:

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

fooBar(3,1.5)
# [1] 4.5
fooBar(3)
# [1] 3
108
Josh O'Brien

Pour être honnête, j'aime bien le premier moyen utilisé par le PO pour le démarrer avec une valeur NULL, puis le vérifier avec is.null (principalement parce qu'il est très simple et facile à comprendre). Cela dépend peut-être de la façon dont les gens sont habitués à coder, mais le Hadley semble également supporter la méthode is.null:

Extrait du livre "Advanced-R" de Hadley, Chapitre 6, Fonctions, p.84 (pour la vérification de la version en ligne ici ):

Vous pouvez déterminer si un argument a été fourni ou non avec la fonction 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

Parfois, vous souhaitez ajouter une valeur par défaut non triviale, qui peut prendre plusieurs lignes de code pour être calculée. Au lieu d'insérer ce code dans la définition de la fonction, vous pouvez utiliser missing () pour le calculer conditionnellement si nécessaire. Cependant, il est difficile de savoir quels arguments sont requis et lesquels sont facultatifs sans lire attentivement la documentation. Au lieu de cela, je règle généralement la valeur par défaut sur NULL et utilise is.null () pour vérifier si l'argument a été fourni.

47
LyzandeR

Ce sont mes règles de base:

Si les valeurs par défaut peuvent être calculées à partir d'autres paramètres, utilisez les expressions par défaut comme suit:

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

si vous utilisez autrement manquant

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

Dans le cas rare où vous pensez qu'un utilisateur peut vouloir spécifier une valeur par défaut qui dure toute une session R, utilisez getOption

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

Si certains paramètres s'appliquent en fonction de la classe du premier argument, utilisez un générique 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 
}

Utilisez ... uniquement lorsque vous transmettez des paramètres supplémentaires à une autre fonction

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

Enfin, si vous choisissez l’utilisation ... sans transmettre les points à une autre fonction, avertissez l’utilisateur que votre fonction ignore les paramètres inutilisés puisqu'elle peut être très déroutant autrement:

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

Il y a plusieurs options et aucune d'entre elles n'est la méthode officielle correcte, aucune n'est vraiment incorrecte, bien qu'elles puissent transmettre des informations différentes à l'ordinateur et aux autres lecteurs de votre code.

Pour l'exemple donné, je pense que l'option la plus claire serait de fournir une valeur par défaut d'identité, dans ce cas, procédez comme suit:

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

C'est la plus courte des options montrées jusqu'à présent et la brièveté peut aider à la lisibilité (et parfois même à la vitesse d'exécution). Il est clair que ce qui est renvoyé est la somme de x et y et vous pouvez voir que la valeur y n'est pas définie comme étant 0, ce qui, une fois ajouté à x, ne donnera que x. De toute évidence, si quelque chose de plus compliqué que l'addition est utilisé, une valeur d'identité différente sera nécessaire (le cas échéant).

Une chose que j’aime vraiment dans cette approche est qu’il est clair quelle est la valeur par défaut lorsqu’on utilise la fonction args, ou même si on regarde le fichier d’aide (il n’est pas nécessaire de faire défiler les détails, c’est juste là dans l'utilisation).

L'inconvénient de cette méthode est que lorsque la valeur par défaut est complexe (nécessitant plusieurs lignes de code), il serait probablement moins lisible d'essayer de placer tout cela dans la valeur par défaut et les approches missing ou NULL devenir beaucoup plus raisonnable.

Certaines des autres différences entre les méthodes apparaissent lorsque le paramètre est transmis à une autre fonction ou lorsque vous utilisez les fonctions match.call ou sys.call.

Donc, je suppose que la méthode "correcte" dépend de ce que vous envisagez de faire avec cet argument particulier et des informations que vous souhaitez transmettre aux lecteurs de votre code.

8
Greg Snow

J'aurais tendance à préférer utiliser NULL pour clarifier ce qui est requis et ce qui est optionnel. Un mot d'avertissement sur l'utilisation de valeurs par défaut qui dépendent d'autres arguments, comme suggéré par Jthorpe. La valeur n'est pas définie lorsque la fonction est appelée, mais lorsque l'argument est référencé pour la première fois! Par exemple:

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

Par contre, si vous référencez y avant de changer x:

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

C'est un peu dangereux, car il est difficile de garder trace de ce que "y" est en train d'être initialisé comme s'il n'était pas appelé tôt dans la fonction.

6
Michael Grosskopf

Je voulais juste souligner que la fonction intégrée sink contient de bons exemples de différentes manières de définir des arguments dans une fonction:

> 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))
    }
}
6
user5359531

que dis-tu de ça?

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

Alors essaye:

> fun(1,y=4)
[1] FALSE
> fun(1)
[1] TRUE
1
Keyu Nie