structures de contrôle et fonctions
Transcription
structures de contrôle et fonctions
Programmation en R - structures de contrôle et fonctions Sophie Baillargeon, Université Laval 8 mars 2016 Contents Structures de contrôle 2 Alternatives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 Boucles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 Fonctions 9 Syntaxe générale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 Fonction sans nom . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 Arguments en entrée . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 Valeurs par défaut des arguments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 Appel d’une fonction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 Argument ... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 Sortie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 Exécution d’une fonction et environnements associés . . . . . . . . . . . . . . . . . . . . . . . . . . 18 Portée lexicale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 Chemin de recherche complet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 Bonnes pratiques concernant les objets utilisables dans le corps d’une fonction . . . . . . . . . 20 Création de méthodes S3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 Utilisation de classes S4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 Références 26 La matière vue jusqu’à maintenant dans le cours concernait surtout l’utilisation du logiciel R, notamment dans le but de faire de l’analyse de données. Nous allons maintenant aborder le sujet de la programmation dans le langage informatique R. Trois outils de base en programmation sont présentés ici : les alternatives, les boucles et les fonctions. 1 Structures de contrôle Définition Structures de contrôle (aussi appelées séquencements) : commandes qui contrôlent l’ordre dans lequel les différentes instructions d’un programme informatique sont exécutées. • alternatives (énoncés conditionnels) : if . . . else, • boucles (énoncés itératifs) : for, while, etc. Alternatives Les alternatives ont pour but d’exécuter des instructions seulement si une certaine condition est satisfaite. Écriture générale : if (condition) { instructions } else { instructions } En fait, on peut avoir un if sans else. On peut aussi imbriquer plusieurs if. . . else. La fonction switch est parfois utile pour remplacer plusieurs if...else imbriqués. Elle ne sera pas illustrée ici. Voici un exemple : Programme qui calcule des statistiques descriptives simples selon le type des éléments du vecteur sur lequel le calcul est fait x <- cars$speed if (is.numeric(x)) { min <- min(x) moy <- mean(x) max <- max(x) stats <- c(min = min, moy = moy, max = max) } else if (is.character(x) || is.factor(x)) { stats <- table(x) } else { stats <- NA } stats ## ## min moy max 4.0 15.4 25.0 On peut faire rouler les instructions de nouveau après avoir redéfini le vecteur x. 2 x <- Orange$Tree if (is.numeric(x)) { min <- min(x) moy <- mean(x) max <- max(x) stats <- c(min = min, moy = moy, max = max) } else if (is.character(x) || is.factor(x)) { stats <- table(x) } else { stats <- NA } stats ## x ## 3 1 5 2 4 ## 7 7 7 7 7 Il serait pratique de créer une fonction à partir de ce bout de code. Écriture condensée : Parfois on utilise une écriture condensée : x <- if (condition) instruction else instruction Cette écriture est recommandée seulement si elle rend le code plus lisible pour des alternatives très simples. Exemple : x <- 1:5 plusGrand <- if(x > 3) ">3" else x ## Warning in if (x > 3) ">3" else x: la condition a une longueur > 1 et seul ## le premier élément est utilisé plusGrand ## [1] 1 2 3 4 5 Attention : La condition dans un if doit être une expression retournant un seul TRUE ou un seul FALSE, pas un vecteur logique de longueur supérieure à 1. Si la condition est un vecteur logique de longueur supérieure à 1, comme dans l’exemple précédent, seul le premier élément est utilisé et un avertissement est affiché. Ici on souhaitait plutôt faire ceci : plusGrand <- ifelse(x > 3, ">3", x) plusGrand ## [1] "1" "2" "3" ">3" ">3" 3 ifelse est une fonction qui agit de façon vectorielle. Elle teste une condition et retourne une valeur en fonction du résultat. Ce n’est pas une structure de contrôle. Autre exemple : x <- cars$speed idStat <- "moyenne" stat <- if(idStat == "moyenne") mean(x) else median(x) On pourrait imaginer ici être en train de rédiger le corps d’une fonction qui aurait un argument nommé idStat servant à identifier la statistique à calculer sur les valeurs fournies dans le premier argument nommé x. Boucles Les boucles ont pour but de répéter des instructions à plusieurs reprises. Écriture générale Quand on sait d’avance le nombre d’itérations à effectuer : for(i in ensemble ) { instructions } Quand on ne connaît pas d’avance le nombre d’itérations à effectuer : while(condition) { instructions } ou encore (intérêt = tester la condition après avoir exécuté les instructions et non avant) repeat { instructions if (condition) break } Mots-clés utiles pour contrôler l’exécution des instructions à l’intérieur d’une boucle : • next : pour terminer immédiatement une itération (sans exécuter les instructions après le mot-clé next) et reprendre l’exécution de la boucle à la prochaine itération. • break : pour terminer complètement l’exécution de la boucle (les itérations restantes ne sont pas effectuées). Exemple de boucle for Dans les notes sur la manipulation de données en R, on a vu des fonctions de la famille des apply. Ces fonctions cachent en fait des boucles. On va reprendre ici un exemple de ces notes et utiliser des boucles pour effectuer les mêmes calculs. Il faut d’abord importer de nouveau le jeu de données boxoffice. 4 library(XML) url <- "http://pro.boxoffice.com/statistics/alltime_numbers/domestic/data" tables <- readHTMLTable(url, header = TRUE, stringsAsFactors = FALSE) boxoffice <- tables[[1]] str(boxoffice) ## 'data.frame': 856 obs. of ## $ # : chr ## $ Movie (Distributor): chr ## $ Release Date : chr ## $ Gross : chr 4 variables: "1" "2" "3" "4" ... "Star Wars: The Force Awakens (Disney)" "Avatar (Fox)" "Titanic (Paramou "Dec 18, 2015" "Dec 18, 2009" "Dec 19, 1997" "Jun 12, 2015" ... "$928,995,339" "$760,507,625" "$658,672,302" "$652,270,625" ... Revenons sur la tâche d’isoler le nom du distributeur dans la colonne Movie (Distributor). On avait d’abord coupé les chaînes de caractères lors de la rencontre d’une parenthèse ouvrante. resSplit <- strsplit(x = boxoffice$"Movie (Distributor)", split = "(", fixed = TRUE) resSplit[1:2] ## ## ## ## ## [[1]] [1] "Star Wars: The Force Awakens " "Disney)" [[2]] [1] "Avatar " "Fox)" Ensuite, on avait utilisé des fonctions de la famille des apply pour extraire le distributeur comme suit. distributeur <- mapply("[", resSplit, sapply(resSplit, length)) distributeur[1:2] ## [1] "Disney)" "Fox)" Cette commande est en fait l’équivalent d’une boucle sur tous les éléments de la liste retournée en sortie de strsplit. Ici, resSplit est une liste contenant uniquement des vecteurs, mais de longueurs différentes. La tâche à effectuer est l’extraction du dernier élément de tous les vecteurs. On pourrait faire ça avec une boucle for comme suit. distributeur <- vector(mode = "character", length = length(resSplit)) for (i in 1:length(resSplit)) { element <- resSplit[[i]] posDernier <- length(element) distributeur[i] <- element[posDernier] } distributeur[1:2] ## [1] "Disney)" "Fox)" Plutôt que de créer les objets intermédiaires element et posDernier, on aurait pû combiner d’un coup toutes les instructions : 5 for (i in 1:length(resSplit)) { distributeur[i] <- resSplit[[i]][length(resSplit[[i]])] } Le code est un peu plus difficile à comprendre, mais il est plus succinct. Note concernant l’enregistrement des résultats dans une boucle Une affectation de valeur à un endroit précis d’un objet (ex.: objet[1] <- 1) nécessite que l’objet existe préalablement. Ainsi, une boucle est souvent précédée par l’initialisation d’un objet dédié à contenir les résultats calculés dans la boucle. Dans l’exemple précédent, on devait initialiser le vecteur distributeur avant la boucle. Note concernant l’affichage de résultats dans une boucle for (i in 1:5) { i } Cette boucle n’affiche rien. Il faut utiliser les fonctions print ou cat pour qu’un résultat soit affiché dans la console. for (i in 1:5) { print(i) } ## ## ## ## ## [1] [1] [1] [1] [1] 1 2 3 4 5 for (i in 1:5) { cat(i) } ## 12345 cat est utile pour faire afficher une trace des itérations. for (i in 1:5) { cat("itération", i, "terminée\n") } ## ## ## ## ## itération itération itération itération itération 1 2 3 4 5 terminée terminée terminée terminée terminée Rappel : Le caractère \n représente un retour de chariot. Exemples de boucle while ou repeat avec le mot-clé break 6 Parfois, on ne sait pas d’avance combien d’itérations il y a à effectuer. Par exemple, si on souhaite simuler le lancer d’un dé jusqu’à l’obtention d’un 6 et compter le nombre de lancers. resultat <- 1 # initialisation à un résultat quelconque, différent de 6 nbreLancers <- 0 while (resultat != 6){ resultat <- sample(1:6, size = 1) nbreLancers <- nbreLancers + 1 } nbreLancers ## [1] 3 Note : la boucle while peut être remplacée par une boucle repeat avec un énoncé break comme suit. nbreLancers <- 0 repeat { resultat <- sample(1:6, size = 1) nbreLancers <- nbreLancers + 1 if (resultat == 6) break } nbreLancers ## [1] 4 Ici, on n’a pas besoin d’initialiser resultat car la condition est évaluée à la fin de la boucle, après avoir calculé resultat. En fait, il serait intéressant de faire cette opération un grand nombre de fois et de calculer le nombre moyen de lancers requis pour obtenir un 6. nbreRep <- 10000 nbreLancers <- rep(0, nbreRep) for (i in 1:nbreRep) { resultat <- 1 # initialisation à un résultat quelconque, différent de 6 while (resultat !=6){ resultat <- sample(1:6, size = 1) nbreLancers[i] <- nbreLancers[i] + 1 } } mean(nbreLancers) ## [1] 5.9707 C’est une façon empirique d’estimer l’espérance d’une variable aléatoire géométrique de paramètre p = 1/6. Plus grand est le nombre de répétitions, plus l’estimation est précise (convergence). En théorie, cette espérance vaut 1/p = 6. Voici un autre bout de code effectuant la même tâche que les bouts précédents, cette fois en utilisant le mot-clé next. 7 nbreRep <- 100 nbreLancers <- 0 j <- 1 for (i in 1:nbreRep) { resultat <- sample(1:6, size = 1) nbreLancers[j] <- nbreLancers[j] + 1 if (resultat != 6) next nbreLancers <- c(nbreLancers, 0) j <- j + 1 } mean(nbreLancers) ## [1] 4.761905 Dans ce programme, si la condition resultat != 6 est rencontrée, on passe à l’itération suivante de la boucle, sans soumettre les instructions nbreLancers <- c(nbreLancers, 0) et j <- j + 1. Le mot-clé next permet donc d’omettre l’exécution de instructions. Il est pratiquement toujours utilisé dans un énoncé conditionnel. En fait, le dernier programme fait la même chose que le programme suivant. nbreRep <- 100 nbreLancers <- 0 j <- 1 for (i in 1:nbreRep) { resultat <- sample(1:6, size = 1) nbreLancers[j] <- nbreLancers[j] + 1 if (resultat == 6) { nbreLancers <- c(nbreLancers, 0) j <- j + 1 } } mean(nbreLancers) ## [1] 6.666667 Le programme sans le next est probablement plus simple à comprendre! Interuption de l’exécution d’une boucle : Il peut arriver que, par erreur, on soumette en R une boucle vraiment longue à rouler, possiblement infinie. Si on travaille en RStudio, on peut interrompre l’exécution de n’importe quelle commande, incluant une boucle, d’une des façons suivantes : • la touche Esc, • le bouton STOP en entête de la fenêtre pour la console R, • le menu Session > Interrupt R. 8 Fonctions Lorsque l’on est susceptible d’avoir besoin d’un bout de code R à répétition (par exemple pour faire un même calcul sur des données différentes), il est préférable d’en faire une fonction R. Avantages des fonctions : • sauver du temps, • diminuer les risques de faire des erreurs, • rédiger du code plus clair et plus court, donc plus facile à comprendre et à partager. Bref, faire des fonctions est une bonne pratique de programmation. Syntaxe générale Pour créer une fonction en R, on utilise la fonction nommée function en respectant la syntaxe suivante : nomFonction <- function(arg1, arg2, arg3){ # corps de la fonction } arg1, arg2 et arg3 représentent les arguments de la fonction, soit les objets que l’on peut fournir en entrée à la fonction. Par exemple : statDesc <- function(x){ if (is.numeric(x)) { min <- min(x) moy <- mean(x) max <- max(x) stats <- c(min = min, moy = moy, max = max) } else if (is.character(x) || is.factor(x)) { stats <- table(x) } else { stats <- NA } stats } Cette fonction reprend un exemple présenté au début de ces notes. Elle calcule des statistiques descriptives simples selon le type des éléments du vecteur donné en entrée. Après avoir soumis le code de cette fonction dans la console, la fonction se retrouve dans l’environnement de travail. On peut alors l’appeler comme on l’a déjà appris. statDesc(x = cars$speed) ## ## min moy max 4.0 15.4 25.0 On pourrait ajouter un argument à cette fonction. Par exemple, on pourrait offrir l’option d’une sortie présentée sous la forme d’une matrice plutôt que d’un vecteur. 9 statDesc <- function(x, sortieMatrice){ # Calcul if (is.numeric(x)) { stats <- c(min = min(x), moy = mean(x), max = max(x)) } else if (is.character(x) || is.factor(x)) { stats <- table(x) } else { stats <- NA } # Production de la sortie if (sortieMatrice){ stats <- as.matrix(stats) colnames(stats) <- if (is.character(x) || is.factor(x)) "frequence" else "stat" } stats } Le code de la fonction a aussi été un peu reformaté. On peut appeler la fonction comme suit. statDesc(x = iris$Species, sortieMatrice = TRUE) ## frequence ## setosa 50 ## versicolor 50 ## virginica 50 Les composantes d’une fonction R sont : • la liste de ses arguments, possiblement avec des valeurs par défaut (on va y revenir); args(statDesc) ## function (x, sortieMatrice) ## NULL • le corps de la fonction, soit le code qui la constitue. body(statDesc) ## { ## ## ## ## ## ## ## ## ## ## if (is.numeric(x)) { stats <- c(min = min(x), moy = mean(x), max = max(x)) } else if (is.character(x) || is.factor(x)) { stats <- table(x) } else { stats <- NA } if (sortieMatrice) { 10 ## ## ## ## ## ## ## } stats <- as.matrix(stats) colnames(stats) <- if (is.character(x) || is.factor(x)) "frequence" else "stat" } stats • l’environnement englobant de la fonction (défini plus loin). environment(statDesc) ## <environment: R_GlobalEnv> Fonction sans nom Notons qu’une fonction n’a même pas besoin de porter de nom. La grande majorité du temps, une fonction est conçue pour être appelée à plusieurs reprises et il est alors nécessaire qu’elle ait un nom. Cependant, on crée parfois des fonctions à usage unique. Par exemple, les fonctions de la famille des apply prennent en entrée une fonction. Il est parfois utile de se créer une fonction pour personnaliser le calcul effectué par une fonction de la famille des apply. Par exemple, si on veut calculer le minimum, la moyenne et le maximum (comme le fait notre fonction statDesc) de toutes les variables numériques du jeu de données iris, mais selon le niveau de la variable Species, on peut utiliser trois appels à la fonction aggregate comme suit. aggregate(x = iris[, -5], by = list(iris$Species), FUN = min) ## Group.1 Sepal.Length Sepal.Width Petal.Length Petal.Width ## 1 setosa 4.3 2.3 1.0 0.1 ## 2 versicolor 4.9 2.0 3.0 1.0 ## 3 virginica 4.9 2.2 4.5 1.4 aggregate(x = iris[, -5], by = list(iris$Species), FUN = mean) ## Group.1 Sepal.Length Sepal.Width Petal.Length Petal.Width ## 1 setosa 5.006 3.428 1.462 0.246 ## 2 versicolor 5.936 2.770 4.260 1.326 ## 3 virginica 6.588 2.974 5.552 2.026 aggregate(x = iris[, -5], by = list(iris$Species), FUN = max) ## Group.1 Sepal.Length Sepal.Width Petal.Length Petal.Width ## 1 setosa 5.8 4.4 1.9 0.6 ## 2 versicolor 7.0 3.4 5.1 1.8 ## 3 virginica 7.9 3.8 6.9 2.5 On pourrait aussi créer une fonction qui calcule les trois statistiques et donner cette fonction en entrée à aggregate comme valeur à l’argument FUN. 11 aggregate(x = iris[, -5], by = list(iris$Species), FUN = function(x) c(min = min(x), moy = mean(x), max = max(x))) ## ## ## ## ## ## ## ## ## ## ## ## Group.1 Sepal.Length.min Sepal.Length.moy Sepal.Length.max Sepal.Width.min 1 setosa 4.300 5.006 5.800 2.300 2 versicolor 4.900 5.936 7.000 2.000 3 virginica 4.900 6.588 7.900 2.200 Sepal.Width.moy Sepal.Width.max Petal.Length.min Petal.Length.moy Petal.Length.max 1 3.428 4.400 1.000 1.462 1.900 2 2.770 3.400 3.000 4.260 5.100 3 2.974 3.800 4.500 5.552 6.900 Petal.Width.min Petal.Width.moy Petal.Width.max 1 0.100 0.246 0.600 2 1.000 1.326 1.800 3 1.400 2.026 2.500 On n’a jamais donné de nom à la fonction et ça ne cause aucun problème ici. Arguments en entrée Les arguments sont définis en énumérant leurs noms dans l’appel à la fonction function. nomFonction <- function(arg1, arg2, arg3) { # corps de la fonction } Il est aussi possible qu’une fonction ne possède aucun argument. HelloWorld <- function() cat("Hello World !") Pour appeler une fonction sans fournir d’arguments, il faut tout de même utiliser les parenthèses. HelloWorld() ## Hello World ! Omettre les parenthèses retourne le code source de la fonction. HelloWorld ## function() cat("Hello World !") Valeurs par défaut des arguments Afin de définir une valeur par défaut pour un argument, il faut accompagner son nom dans l’énumération des arguments d’un opérateur = et d’une instruction R retournant la valeur par défaut. Par exemple, dans la fonction statDesc, il serait préférable de définir un format par défaut pour la sortie. 12 statDesc <- function (x, sortieMatrice = FALSE) { # Calcul if (is.numeric(x)) { stats <- c(min = min(x), moy = mean(x), max = max(x)) } else if (is.character(x) || is.factor(x)) { stats <- table(x) } else { stats <- NA } # Production de la sortie if (sortieMatrice){ stats <- as.matrix(stats) colnames(stats) <- if (is.character(x) || is.factor(x)) "frequence" else "stat" } stats } Les arguments n’ayant pas de valeur par défaut sont obligatoires. Si on appelle une fonction sans donner de valeur en entrée à un paramètre obligatoire, une erreur est produite. Les arguments ayant une valeur par défaut peuvent, pour leur part, ne pas être fournis en entrée. statDesc(sortieMatrice = FALSE) ## Error in statDesc(sortieMatrice = FALSE): l'argument "x" est manquant, avec aucune valeur par défaut statDesc(x = cars$speed) ## ## min moy max 4.0 15.4 25.0 Attardons-nous maintenant à un cas particulier de valeur par défaut en R. Supposons qu’une fonction possède un argument qui prend en entrée une seule chaîne de caractères et que seulement un petit nombre de chaînes de caractères distinctes sont acceptées comme valeur de cet argument. C’est le cas par exemple pour l’argument useNA de la fonction table. La fonction accepte seulement les valeurs "no", "ifany" ou "always" pour l’argument useNA. Donner une valeur autre à l’argument produira une erreur. table(iris$Species, useNA = "test") ## Error in match.arg(useNA): 'arg' should be one of "no", "ifany", "always" Une pratique courante en R pour un argument de ce type est de lui donner comme valeur dans l’énumération des arguments le vecteur de toutes ses valeurs possibles. C’est ce qui est fait dans la fonction table. args(table) ## function (..., exclude = if (useNA == "no") c(NA, NaN), useNA = c("no", ## "ifany", "always"), dnn = list.names(...), deparse.level = 1) ## NULL 13 La valeur par défaut de l’argument n’est en réalité pas le vecteur complet c("no", "ifany", "always"), mais plutôt le premier élément de ce vecteur, soit "no". Il en est ainsi, car le corps de la fonction contient l’instruction suivante. useNA <- match.arg(useNA) On devrait reproduire cette façon de faire dans nos propres fonctions qui possèdent un argument du même type que l’argument useNA de la fonction table. La fonction match.arg vérifie que la valeur donnée en entrée à un argument est bien une valeur acceptée ou retourne le premier élément du vecteur de valeurs possibles si aucune valeur n’a été donnée en entrée à l’argument. Par exemple, remplaçons l’argument sortieMatrice de notre fonction statDesc par l’argument formatSortie comme suit. statDesc <- function (x, formatSortie = c("vecteur", "matrice", "liste")) { # Calcul if (is.numeric(x)) { stats <- c(min = min(x), moy = mean(x), max = max(x)) } else if (is.character(x) || is.factor(x)) { stats <- table(x) } else { stats <- NA } # Production de la sortie formatSortie <- match.arg(formatSortie) if (formatSortie == "matrice"){ stats <- as.matrix(stats) colnames(stats) <- if (is.character(x) || is.factor(x)) "frequence" else "stat" } else if (formatSortie == "liste") { stats <- as.list(stats) } stats } La valeur par défaut de l’argument formatSortie est bel et bien "vecteur". statDesc(x = cars$speed) ## ## min moy max 4.0 15.4 25.0 statDesc(x = cars$speed, formatSortie = "vecteur") ## ## min moy max 4.0 15.4 25.0 La présence du vecteur des valeurs possibles dans la définition des arguments est très informative. Appel d’une fonction Les appels à nos propres fonctions respectent les mêmes règles que les appels à n’importe quelle fonction R. En plus du fonctionnement des valeurs par défaut décrit ci-dessus, rappelons que les arguments peuvent être fournis à une fonction R par position, par nom complet ou même par nom partiel. L’association des arguments à leurs valeurs se fait en respectant les règles de préséances suivantes : 14 1. d’abord les arguments fournis avec un nom exact se voient attribuer une valeur, 2. puis les arguments fournis avec un nom partiel, 3. et finalement les arguments non nommés, selon leurs positions. Voici quelques exemples. testAppel <- function(x, option, param, parametre) { cat("l'argument x prend la valeur", x, "\n") cat("l'argument option prend la valeur", option, "\n") cat("l'argument param prend la valeur", param, "\n") cat("l'argument parametre prend la valeur", parametre, "\n") } testAppel(1, 2, 3, 4) ## ## ## ## l'argument l'argument l'argument l'argument x prend la valeur 1 option prend la valeur 2 param prend la valeur 3 parametre prend la valeur 4 testAppel(1, param = 2, opt = 3, 4) ## ## ## ## l'argument l'argument l'argument l'argument x prend la valeur 1 option prend la valeur 3 param prend la valeur 2 parametre prend la valeur 4 testAppel(1, par = 2, option = 3, 4) ## Error in testAppel(1, par = 2, option = 3, 4): l'argument 2 correspond à plusieurs arguments formels Une bonne pratique de programmation en R est d’utiliser l’association par positionnement seulement pour les premiers arguments, ceux les plus souvent utiliser. Les arguments moins communs devraient être nommés afin de conserver un code facile à comprendre. Argument ... On a déjà mentionné les deux utilités de l’argument .... On peut utiliser cet argument dans nos propres fonctions, en exploitant l’une ou l’autre de ses utilités. L’argument ... peut permettre de prendre un nombre indéterminé d’objets en entrée, comme dans cet exemple. statDescMulti <- function(...){ args <- list(...) lapply(args, statDesc) } 15 statDescMulti(cars$speed, iris$Species) ## [[1]] ## min moy max ## 4.0 15.4 25.0 ## ## [[2]] ## x ## setosa versicolor ## 50 50 virginica 50 Ici, l’instruction list(...) est la clé. C’est de cette façon que l’on récupère tout ce qui a été donné en entrée dans l’argument .... L’argument ... permet aussi de passer des arguments à une fonction appelée dans le corps de la fonction. Par exemple, l’argument ... serait utile à notre fonction statDesc pour contrôler le traitement des valeurs manquantes. statDesc <- function (x, formatSortie = c("vecteur", "matrice", "liste"), ...) { # Calcul if (is.numeric(x)) { stats <- c(min = min(x, ...), moy = mean(x, ...), max = max(x, ...)) } else if (is.character(x) || is.factor(x)) { stats <- table(x, ...) } else { stats <- NA } # Production de la sortie formatSortie <- match.arg(formatSortie) if (formatSortie == "matrice"){ stats <- as.matrix(stats) colnames(stats) <- if (is.character(x) || is.factor(x)) "frequence" else "stat" } else if (formatSortie == "liste") { stats <- as.list(stats) } stats } statDesc(x = c(cars$speed, NA)) ## min moy max ## NA NA NA statDesc(x = c(cars$speed, NA), na.rm = TRUE) ## ## min moy max 4.0 15.4 25.0 Sortie Une fonction retourne : 16 • l’objet donné en argument à la fonction return dans le corps de la fonction, • ou, en l’absence d’appel à la fonction return, la dernière expression évaluée dans le corps de la fonction. Par exemple, la version suivante de la fonction statDescMulti retourne la liste des arguments fournis en entrée plutôt que le résultat de l’appel à lapply à cause de la présence de la fonction return. statDescMulti <- function(...){ args <- list(...) return(args) lapply(args, statDesc) } statDescMulti(speed = cars$speed, dist = cars$dist) ## ## ## ## ## ## ## ## ## $speed [1] 4 4 7 7 8 9 10 10 10 11 11 12 12 12 12 13 13 13 13 14 14 14 14 [24] 15 15 15 16 16 17 17 17 18 18 18 18 19 19 19 20 20 20 20 20 22 23 24 [47] 24 24 24 25 $dist [1] 2 [18] 34 [35] 84 10 46 36 4 26 46 22 36 68 16 60 32 10 80 48 18 20 52 26 26 56 34 54 64 17 32 66 28 40 54 14 32 70 20 40 92 24 28 50 42 93 120 26 56 85 34 76 Une fonction ne peut retourner qu’un seul objet. Si on veut retourner plusieurs objets, il faut les combiner dans un seul objet (typiquement dans une liste), comme dans l’exemple suivant. statDescMulti <- function(...){ call <- match.call() args <- list(...) stats <- lapply(args, statDesc) list(stats = stats, call = call) } statDescMulti(speed = cars$speed, dist = cars$dist) ## ## ## ## ## ## ## ## ## ## ## ## $stats $stats$speed min moy max 4.0 15.4 25.0 $stats$dist min moy max 2.00 42.98 120.00 $call statDescMulti(speed = cars$speed, dist = cars$dist) Pour faciliter la réutilisation des résultats, il est souhaitable de toujours nommer les éléments d’une liste retournée en sortie. 17 Fonction match.call L’exemple précédent fait intervenir la fonction match.call. Il est commun pour des fonctions d’ajustement de modèle telles que lm de retourner dans la sortie une copie de l’appel de la fonction. exemple <- lm(dist ~ speed, data = cars) exemple$call ## lm(formula = dist ~ speed, data = cars) C’est la fonction match.call qui permet de créer cet élément de la sortie. Les fonctions match.call et return sont des exemples de fonctions seulement utiles dans le corps d’une fonction. Les appeler directement dans la console retourne une erreur ou une sortie sans intérêt. Exécution d’une fonction et environnements associés Lorsqu’on appelle une fonction R, un environnement est créé spécifiquement pour l’évaluation du corps de la fonction, puis détruit lorsque l’exécution est terminée. Rappelons que l’évaluation est simplement la façon dont R s’y prend pour comprendre ce qu’une commande R signifie. Attardons-nous à comprendre comment R fait pour trouver la valeur d’un objet lorsqu’il évalue les instructions dans le corps d’une fonction. Au départ, l’environnement créé lors de l’appel d’une fonction contient seulement des promesses d’évaluation, car R utilise une évaluation d’arguments dite paresseuse. Il évalue les arguments seulement lorsque le corps de la fonction les utilise pour la première fois. Ainsi, au fur et à mesure que les lignes de code du corps de la fonction sont évaluées, les arguments de la fonction deviennent des objets dans l’environnement créé spécifiquement pour l’évaluation de la fonction. Un objet associé à un argument donné en entrée lors de l’appel de la fonction est créé en évaluant la valeur qui lui a été attribuée. Pour créer les objets associés aux arguments auxquels aucune valeur n’a été fournie dans l’appel de la fonction, R évalue l’instruction fournie comme valeur par défaut dans la définition de la fonction. Au cours de l’évaluation des instructions formant le corps de la fonction, on crée parfois aussi de nouveaux objets dans cet environnement, que l’on réutilise plus tard. En informatique, on appelle ces objets variables locales. Portée lexicale Trouver la valeur des arguments et des variables locales en cours d’exécution d’une fonction est simple pour R. Ces objets se trouvent directement dans l’environnement d’évaluation de la fonction. On dit en informatique qu’ils ont une portée locale. La question que l’on peut maintenant se poser est la suivante : Comment R trouve-t-il la valeur des objets appelés à l’intérieur d’une fonction, qui ne sont ni des arguments ni des variables locales? Chaque langage de programmation suit une certaine règle pour résoudre ce problème. Les deux règles les plus courantes sont l’utilisation d’une portée lexicale (en anglais lexical scoping) ou encore d’une portée dynamique (en anglais dynamic scoping). Avec une portée lexicale, si un objet appelé n’est pas trouvé dans l’environnement d’évaluation d’une fonction, le programme va le chercher dans l’environnement d’où la fonction a été créée, nommé environnement englobant (en anglais enclosing environment). Avec une portée dynamique, le programme va plutôt le chercher dans l’environnement d’où la fonction a été appelée, nommé environnement d’appel (en anglais calling environment). 18 R utilise par défaut la portée lexicale. Voici un petit exemple pour illustrer la portée lexicale. a <- 1 b <- 2 f <- function(x) { a*x + b } Quelle valeur sera retournée par f(2)? Est-ce 1*2 + 2 = 4? Oui! f(2) ## [1] 4 Les objets nommés a et b ne se retrouvaient pas dans l’environnement d’exécution de la fonction. Alors R a cherché leurs valeurs dans l’environnement englobant de la fonction f, qui est ici l’environnement de travail. environment(f) ## <environment: R_GlobalEnv> Il a trouvé a = 1 et b = 2. La fonction environment retourne l’environnement englobant d’une fonction. Modifions maintenant l’exemple comme suit. g <- function(x) { a <- 2 b <- 1 f(x) } Quelle valeur sera retournée par g(2)? Est-ce 2*2 + 1 = 5? Non! g(2) ## [1] 4 La fonction g est appelée dans l’environnement de travail. Elle appelle elle-même f. L’environnement d’appel de f est donc l’environnement d’exécution de g. Par contre, l’environnement englobant de f n’a pas changé. Il est encore l’environnement de travail, car c’est dans cet environnement que la fonction a été définie. environment(f) ## <environment: R_GlobalEnv> La portée lexicale permet de s’assurer que le fonctionnement de l’évaluation d’une fonction ne dépende pas du contexte dans lequel la fonction est appelée. Il dépend seulement de l’environnement d’où la fonction a été créée. Si la portée en R était par défaut dynamique, g(2) aurait retourné la valeur 5. Et si f était créée à l’intérieur de la fonction g? 19 g<-function(x) { f<-function(x) { a*x + b } a <- 2 b <- 1 f(x) } Que retourne g(2) maintenant? g(2) ## [1] 5 L’environnement englobant de f est maintenant l’environnement d’exécution de g, car f a été défini dans le corps de la fonction g. Chemin de recherche complet Le chemin de recherche de valeurs des objets lors de l’évaluation d’une fonction en R ne s’arrête pas à l’environnement d’exécution de la fonction suivi de l’environnement englobant de la fonction. Il remonte toujours jusqu’à l’environnement de travail. Parfois, l’environnement englobant est directement l’environnement de travail. Si l’environnement englobant est plutôt l’environnement d’exécution d’une autre fonction, alors la recherche se poursuit dans l’environnement englobant de cette fonction. En remontant ainsi le chemin des environnements englobants, on finit toujours par retomber sur l’environnement de travail. Et de là, le chemin de recherche se poursuit par les environnements de tous les packages chargés, comme on l’a vu dans le cours Calculs statistiques et mathématiques en R. On peut donc utiliser dans nos propres fonctions des fonctions provenant d’autres packages. Il faut seulement s’assurer que ces packages sont chargés pour que nos fonctions roulent sans erreur. Bonnes pratiques concernant les objets utilisables dans le corps d’une fonction Il est recommandé d’utiliser dans une fonction uniquement des objets que l’on est certain de pouvoir atteindre. L’idéal est de se limiter aux arguments de la fonction, aux objets créés dans la fonction (variables locales) ainsi qu’aux objets se trouvant dans des packages chargés. Si on comprend bien le concept de portée lexical, on peut aussi s’amuser à utiliser des objets dans l’environnement englobant d’une fonction. Cependant, il est risqué d’utiliser les objets de l’environnement de travail, même si cet environnement se retrouve toujours dans le chemin de recherche de valeurs des objets lors de l’évaluation d’une fonction. Le contenu de l’environnement de travail est constamment modifié au fil de nos sessions. Aussi, si l’on partage nos fonctions avec une autre personne, on ne contrôle pas le contenu de l’environnement de travail pendant la session R de cette personne. Ces recommandations s’appliquent au code dans le corps d’une fonction, mais aussi aux instructions définissant les valeurs par défaut des arguments. On a appris que ces instructions sont évaluées dans le corps de la fonction. Elles peuvent donc contenir sans problème d’autres arguments de la fonction. Cependant, on devrait éviter d’utiliser des objets provenant de l’environnement de travail dans ces instructions. 20 Création de méthodes S3 Il est facile de créer pour nos fonctions de nouvelles méthodes pour des fonctions génériques existantes (ex. print, summary, plot, coef, etc.). On a déjà parlé de ces fonctions dans le cours Calculs statistiques et mathématiques en R. On ne va pas voir ici comment créer de nouvelles fonctions génériques, mais plutôt comment créer de nouvelles méthodes (versions) de ces fonctions. Ce système de programmation orienté objet en R porte un nom. On l’appelle le système S3. Pour créer des méthodes qui traitent de façon particulière la sortie d’une de nos fonctions, il suffit de compléter les deux étapes suivantes. 1. D’abord, il est préférable que l’objet retourné en sortie par cette fonction soit une liste. Ensuite, il faut attribuer une nouvelle classe (par exemple le nom de la fonction) avec la fonction class à cet objet en sortie. 2. On doit ensuite créer une fonction nommée : nomFonctionGenerique.nomClasse. Cette fonction doit comporter au minimum les deux arguments suivants : • x qui représente ici par convention un objet de la classe nomClasse, • l’argument . . . , même si on ne l’utilise pas. Pour illustrer la création d’une méthode S3, créons une méthode print pour notre fonction statDesc. Tout d’abord, transformons la sortie de statDesc en liste, et attribuons-lui une nouvelle classe. statDesc <- function (x, formatSortie = c("vecteur", "matrice", "liste"), ...) { # Calcul if (is.numeric(x)) { stats <- c(min = min(x, ...), moy = mean(x, ...), max = max(x, ...)) } else if (is.character(x) || is.factor(x)) { stats <- table(x, ...) } else { stats <- NA } # Production de la sortie formatSortie <- match.arg(formatSortie) if (formatSortie == "matrice"){ stats <- as.matrix(stats) colnames(stats) <- if (is.character(x) || is.factor(x)) "frequence" else "stat" } else if (formatSortie == "liste") { stats <- as.list(stats) } out <- list(stats = stats) class(out) <- "statDesc" out } Maintenant, écrivons le code de notre nouvelle méthode print, pour un objet de classe statDesc. print.statDesc <- function(x, ...){ cat("Statistiques descriptives sur les observations de la variable `x` :\n") print(x$stats, ...) } Le résultat de la fonction statDesc sera maintenant toujours affiché en utilisant la méthode print.statDesc. 21 statDesc(x = iris$Species, formatSortie = "matrice") ## ## ## ## ## Statistiques descriptives sur les observations de la variable `x` : frequence setosa 50 versicolor 50 virginica 50 Si on affecte le résultat de l’appel à la fonction à un objet, cet objet prendra la valeur retournée par la fonction et rien ne sera affiché. copie <- statDesc(x = iris$Species, formatSortie = "matrice") Cette affectation permet de pouvoir accéder directement à la sortie en allant chercher des éléments dans la liste retournée. str(copie) ## List of 1 ## $ stats: int [1:3, 1] 50 50 50 ## ..- attr(*, "dimnames")=List of 2 ## .. ..$ : chr [1:3] "setosa" "versicolor" "virginica" ## .. ..$ : chr "frequence" ## - attr(*, "class")= chr "statDesc" On peut aussi demander l’affichage formaté de l’objet. copie # équivalent à print(copie) ## ## ## ## ## Statistiques descriptives sur les observations de la variable `x` : frequence setosa 50 versicolor 50 virginica 50 Ainsi, à cause de la fonction générique print qui est responsable de tout affichage (ou impression) dans la console, ce qu’une fonction retourne et ce qu’elle affiche à la fin de son exécution sont deux choses distinctes. Dans l’exemple précédent, la fonction retourne une liste, mais elle affiche un petit tableau 3 × 1 comportant un titre. La méthode print sert à contrôler la façon dont la sortie de la fonction est affichée dans la console. Contrôler cet affichage est pratique, voire essentiel, pour une fonction qui retourne une très longue liste, comme presque toutes les fonctions d’ajustement de modèle. Par défaut, toute la liste serait affichée par la méthode print, ce qui serait illisible dans la console. Le nom complet de la méthode par défaut pour la fonction générique print est print.default. Toutes les fonctions génériques ont une méthode par défaut. Le fonctionnement de R lors de l’appel à une fonction générique est : 1. il extrait le nom de la classe, disons nomClasse, du premier argument fourni à la fonction avec la fonction class; 2. il vérifie si le nom de cette classe concorde avec une des méthodes définies pour la fonction générique 22 • s’il trouve une méthode correspondante, celle nommée nomFonctionGenerique.nomClasse, il l’appelle; • sinon, il appelle la fonction nomFonctionGenerique.default. Notons que la fonction class retourne un nom de classe pour tout objet R, même s’il ne possède pas d’attribut nommé class. Par exemple, l’objet copie que l’on vient de créer, possède un attribut classe. attributes(copie) ## ## ## ## ## $names [1] "stats" $class [1] "statDesc" Ainsi, la fonction class retourne cet attribut. class(copie) ## [1] "statDesc" Cependant, le petit vecteur suivant ne possède aucun attribut class. x <- 1:5 x ## [1] 1 2 3 4 5 attributes(x) ## NULL La fonction class ne retourne pas NULL pour autant. class(x) ## [1] "integer" On vient de voir comment faire de la programmation orientée objet en R. Cependant, cette forme de programmation orientée objet est différente de la programmation orientée objet retrouvée dans les langages Java ou C++ par exemple. C’est une forme de programmation orientée objet moins formelle. En plus du système de programmation orienté objet vu ici, nommé S3, il existe d’autres systèmes de programmation orientée objet en R, notamment le système S4 et le système RC. On ne verra pas comment créer des fonctions exploitant ces autres systèmes (voir les références pour les intéressés), mais on va voir comment utiliser des fonctions retournant des objets de classes S4. Le système RC ne sera pas du tout abordé ici, car il est nettement moins couramment employé. 23 Utilisation de classes S4 S3 est le premier système de programmation orienté objet a avoir vu le jour en R. Le deuxième système est nommé S4. Même si on ne voit pas comment créer des classes S4, il est bon de savoir comment utiliser ce type de classes qui est assez courant, particulièrement dans les packages distribués sur Bioconductor. Pour illustrer les classes S4, installons le package sp. install.packages("sp") Voici un exemple d’utilisation d’une fonction de ce package, tiré d’une fiche d’aide du package. library(sp) x = c(1,2,3,4,5) y = c(3,2,5,1,4) S <- SpatialPoints(cbind(x,y)) S ## ## ## ## ## ## ## ## SpatialPoints: x y [1,] 1 3 [2,] 2 2 [3,] 3 5 [4,] 4 1 [5,] 5 4 Coordinate Reference System (CRS) arguments: NA str(S) ## Formal class 'SpatialPoints' [package "sp"] with 3 slots ## ..@ coords : num [1:5, 1:2] 1 2 3 4 5 3 2 5 1 4 ## .. ..- attr(*, "dimnames")=List of 2 ## .. .. ..$ : NULL ## .. .. ..$ : chr [1:2] "x" "y" ## ..@ bbox : num [1:2, 1:2] 1 1 5 5 ## .. ..- attr(*, "dimnames")=List of 2 ## .. .. ..$ : chr [1:2] "x" "y" ## .. .. ..$ : chr [1:2] "min" "max" ## ..@ proj4string:Formal class 'CRS' [package "sp"] with 1 slot ## .. .. ..@ projargs: chr NA L’objet retourné par la fonction SpatialPoints n’est pas une liste. C’est un objet d’un nouveau type, défini dans le package sp. Pour atteindre les éléments dans l’objet, il est préférable d’utiliser une méthode conçue à cet effet. Par exemple, la fiche d’aide ouverte par la commande help("SpatialPoints-class") nous informe qu’une méthode coordinates est définie pour les objets de la classe "SpatialPoints". coordinates(S) ## ## ## ## ## ## [1,] [2,] [3,] [4,] [5,] x 1 2 3 4 5 y 3 2 5 1 4 24 Il est aussi possible d’utiliser l’opérateur @ (et non $ puisqu’il ne s’agit pas d’une liste) pour extraire des éléments de l’objet. S@coords ## ## ## ## ## ## [1,] [2,] [3,] [4,] [5,] x 1 2 3 4 5 y 3 2 5 1 4 Notons qu’un des intérêts du package sp est la production facilitée de graphiques représentant des données spatiales, par exemple des coordonnées géographiques, en s’assurant d’utiliser des axes sur la même échelle. plot(S, main = "Axes sur la même échelle", axes=TRUE) 1 2 3 4 5 Axes sur la même échelle −2 0 2 4 plot(x, y, main = "Axes non contrôlés") 25 6 8 3 1 2 y 4 5 Axes non contrôlés 1 2 3 4 5 x Références • http://adv-r.had.co.nz/Functions.html • http://adv-r.had.co.nz/Environments.html Programmation orientée objet en R : • Matloff, N. (2011). The Art of R Programming : A Tour of Statistical Software Design. No Starch Press, chapitre 9. • http://adv-r.had.co.nz/OO-essentials.html 26