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