<- function(x) {
ajoute2 <- x + 2
res return(res)
}
14 Écrire ses propres fonctions
14.1 Introduction et exemples
14.1.1 Structure d’une fonction
Nous avons vu lors de l’introduction à R que le langage repose sur deux grands concepts : les objets et les fonctions. Pour reprendre une citation de John Chambers, en R, tout ce qui existe est un objet, et tout ce qui se passe est une fonction.
Le principe d’une fonction est de prendre en entrée un ou plusieurs arguments (ou paramètres), d’effectuer un certain nombre d’actions et de renvoyer un résultat :
Nous avons déjà rencontré et utilisé un grand nombre de fonctions, certaines assez simples (mean
, max
…) et d’autres beaucoup plus complexes (summary
, mutate
…). R, comme tout langage de programmation, offre la possibilité de créer et d’utiliser ses propres fonctions.
Voici un exemple de fonction très simple, quoi que d’une utilité douteuse, puisqu’elle se contente d’ajouter 2 à un nombre :
En exécutant ce code, on crée une nouvelle fonction nommée ajoute2
, que l’on peut directement utiliser dans un script ou dans la console :
ajoute2(3)
#> [1] 5
On va décomposer pas à pas la structure de cette première fonction.
D’abord, une fonction est créée en utilisant l’instruction function
. Celle-ci est suivie d’une paire de parenthèses et d’une paire d’accolades.
function() {
}
Dans les parenthèses, on indique les arguments de la fonction, ceux qui devront lui être passés quand nous l’appellerons. Ici notre fonction ne prend qu’un seul argument, que nous avons décidé arbitrairement de nommer x
.
function(x) {
}
Les accolades comprennent une série d’instructions R qui constituent le corps de notre fonction. C’est ce code qui sera exécuté quand notre fonction est appelée. On peut utiliser dans le corps de la fonction les arguments qui lui sont passés. Ici, la première ligne utilise la valeur de l’argument x
, lui ajoute 2 et stocke le résultat dans un nouvel objet res
.
function(x) {
<- x + 2
res }
Pour qu’elle soit utile, notre fonction doit renvoyer le résultat qu’elle a calculé précédemment. Ceci se fait via l’instruction return
à qui on passe la valeur à retourner.
function(x) {
<- x + 2
res return(res)
}
Enfin, pour que notre fonction puisse être appelée et utilisée, nous devons lui donner un nom, ou plus précisément la stocker dans un objet. Ici on la stocke dans un objet nommé ajoute2
.
<- function(x) {
ajoute2 <- x + 2
res return(res)
}
Les fonctions étant des objets comme les autres, elles suivent les mêmes contraintes pour leur nom : on a donc droit aux lettres, chiffres, point et tiret bas.
Attention à ne pas donner à votre fonction le nom d’une fonction déjà existante : par exemple, si vous créez une fonction nommée table
, la fonction du même nom de R base ne sera plus disponible (sauf à la préfixer avec base::table
). Si vous “écrasez” par erreur une fonction existante, il vous suffit de relancer votre session R et de trouver un nouveau nom.
Avec le code précédent, on a donc créé un nouvel objet ajoute2
de type function
. Cette nouvelle fonction prend un seul argument x
, calcule la valeur x + 2
et retourne ce résultat. On l’utilise en tapant son nom suivi de la valeur de son argument entre parenthèses, par exemple :
ajoute2(41)
#> [1] 43
Ou encore :
<- 5
y <- ajoute2(y)
z
z#> [1] 7
À noter que comme x + 2
fonctionne si x
est un vecteur, on peut aussi appeler notre fonction en lui passant un vecteur en argument.
<- 1:5
vec ajoute2(vec)
#> [1] 3 4 5 6 7
Si on récapitule, une fonction se définit donc de la manière suivante :
Une fonction peut évidemment prendre plusieurs arguments. Dans ce cas on liste les arguments dans les parenthèses en les séparant par des virgules :
<- function(x, y) {
somme return(x + y)
}
somme(3, 5)
#> [1] 8
Une fonction peut aussi n’accepter aucun argument, dans ce cas on laisse les parenthèses vides.
<- function() {
miaule return("Miaou")
}
miaule()
#> [1] "Miaou"
À noter que si on appelle une fonction avec un nombre d’arguments incorrect, cela génère une erreur.
somme(1)
#> Error in somme(1): argument "y" is missing, with no default
miaule("ouaf")
#> Error in miaule("ouaf"): unused argument ("ouaf")
14.1.2 Exemple de fonction
Prenons un exemple un peu plus élaboré : la fonction table()
retourne le tri à plat en effectifs d’une variable qualitative. On souhaite créer une fonction qui calcule plutôt le tri à plat en pourcentages. Voici une manière de le faire :
<- function(v) {
prop_tab <- table(v)
tri <- length(v)
effectif_total <- tri / effectif_total * 100
tri return(tri)
}
Notre fonction prend en entrée un argument nommé v
, en l’occurrence un vecteur représentant une variable qualitative. On commence par faire le tri à plat de ce vecteur avec table
, puis on calcule la répartition en pourcentages en divisant ce tri à plat par l’effectif total et en multipliant par 100.
Testons avec un vecteur d’exemple :
<- c("rouge", "vert", "vert", "bleu", "rouge")
vec prop_tab(vec)
#> v
#> bleu rouge vert
#> 20 40 40
Testons sur une variable du jeu de données hdv2003
1 :
library(questionr)
data(hdv2003)
prop_tab(hdv2003$qualif)
#> v
#> Ouvrier specialise Ouvrier qualifie Technicien
#> 10.15 14.60 4.30
#> Profession intermediaire Cadre Employe
#> 8.00 13.00 29.70
#> Autre
#> 2.90
Ça fonctionne, mais avec une petite limite : par défaut table()
ignore les NA
. On peut modifier ce comportement en lui ajoutant un argument useNA = "ifany"
.
<- function(v) {
prop_tab <- table(v, useNA = "ifany")
tri <- length(v)
effectif_total <- tri / effectif_total * 100
tri return(tri)
}
prop_tab(hdv2003$qualif)
#> v
#> Ouvrier specialise Ouvrier qualifie Technicien
#> 10.15 14.60 4.30
#> Profession intermediaire Cadre Employe
#> 8.00 13.00 29.70
#> Autre <NA>
#> 2.90 17.35
Quand on modifie une fonction existante, il faut exécuter à nouveau le code correspondant à sa définition pour la “mettre à jour”. Ici, si on ne le fait pas l’objet prop_tab
contiendra toujours l’ancienne définition.
Pour “mettre à jour” une fonction après avoir modifié son code, on peut soit sélectionner le code qui la définit et l’exécuter de la manière habituelle, soit, dans RStudio, se positionner dans le corps de la fonction et utiliser le raccourci clavier Ctrl + Alt + F
.
Autre amélioration possible : on pourrait vouloir modifier le nombre de décimales affichées pour les pourcentages, par exemple en les limitant à 1. Pour cela on ajoute une instruction round()
.
<- function(v) {
prop_tab <- table(v, useNA = "ifany")
tri <- length(v)
effectif_total <- tri / effectif_total * 100
tri <- round(tri, 1)
tri return(tri)
}
prop_tab(hdv2003$qualif)
#> v
#> Ouvrier specialise Ouvrier qualifie Technicien
#> 10.2 14.6 4.3
#> Profession intermediaire Cadre Employe
#> 8.0 13.0 29.7
#> Autre <NA>
#> 2.9 17.3
Ça fonctionne ! Cela dit, limiter à un chiffre après la virgule ne convient pas forcément dans tous les cas. L’idéal serait d’offrir la possibilité à la personne qui appelle la fonction de choisir elle-même la précision de l’affichage. Comment ? Tout simplement en ajoutant un deuxième argument à notre fonction, que nous nommerons decimales
, et en utilisant cet argument à la place du 1 dans l’appel à round()
.
<- function(v, decimales) {
prop_tab <- table(v, useNA = "ifany")
tri <- length(v)
effectif_total <- tri / effectif_total * 100
tri <- round(tri, decimales)
tri return(tri)
}
Désormais, notre fonction s’utilise en lui indiquant deux arguments :
prop_tab(hdv2003$qualif, 1)
#> v
#> Ouvrier specialise Ouvrier qualifie Technicien
#> 10.2 14.6 4.3
#> Profession intermediaire Cadre Employe
#> 8.0 13.0 29.7
#> Autre <NA>
#> 2.9 17.3
De la même manière, on pourrait vouloir laisser le choix à l’utilisateur d’afficher ou non les NA
dans le tri à plat. C’est possible en ajoutant un troisième argument à notre fonction et en utilisant sa valeur dans le paramètre useNA
de table()
.
<- function(v, decimales, useNA) {
prop_tab <- table(v, useNA = useNA)
tri <- length(v)
effectif_total <- tri / effectif_total * 100
tri <- round(tri, decimales)
tri return(tri)
}
prop_tab(hdv2003$qualif, 1, "no")
#> v
#> Ouvrier specialise Ouvrier qualifie Technicien
#> 10.2 14.6 4.3
#> Profession intermediaire Cadre Employe
#> 8.0 13.0 29.7
#> Autre
#> 2.9
14.1.3 Effets de bord et affichage de messages
Parfois une fonction n’a pas pour objectif de renvoyer un résultat mais d’accomplir une action, comme générer un graphique, afficher un message, enregistrer un fichier… Dans ce cas la fonction peut ne pas inclure d’instruction return()
.
Les actions “visibles” dans notre session R accomplies par une fonction en-dehors du résultat renvoyé sont appelés des effets de bord.
Par exemple la fonction suivante prend en argument un vecteur et génère un diagramme en barres du tri à plat de cette variable (en modifiant un peu la présentation au passage).
<- function(var) {
my_barplot <- table(var)
tri barplot(tri, col = "skyblue", border = NA)
}
my_barplot(hdv2003$clso)
Un autre effet de bord très courant consiste à afficher des informations dans la console. Pour cela on peut utiliser print
, qui affiche de manière aussi lisible que possible l’objet qu’on lui passe en argument :
<- function(v) {
indicateurs print(mean(v))
print(sd(v))
}
indicateurs(hdv2003$age)
#> [1] 48.157
#> [1] 16.94181
Quand on souhaite seulement afficher une chaîne de caractère, on peut utiliser cat()
qui fournit une sortie plus lisible que print
:
<- function(nom) {
hello cat("Bonjour,", nom, "!")
}
hello("Pierre-Edmond")
#> Bonjour, Pierre-Edmond !
Enfin, on peut aussi utiliser message()
qui, comme son nom l’indique, affiche un message dans la console, avec une mise en forme spécifique. En général on l’utilise plutôt pour afficher des informations relatives au déroulement de la fonction.
Dans l’exemple suivant, on utilise la fonction runif()
pour générer aléatoirement n
nombres entre 0 et 1 et on affiche avec cat()
la valeur du plus petit nombre généré. Comme l’exécution du runif()
peut prendre du temps si n
est grand, on affiche un message avec message()
pour prévenir l’utilisateur.
<- function(n) {
min_alea message("Génération de ", n, " nombres aléatoires...")
<- runif(n)
v cat("Le plus petit nombre généré vaut", min(v))
}
min_alea(50000)
#> Génération de 50000 nombres aléatoires...
#> Le plus petit nombre généré vaut 0.000000834465
14.1.4 Utilité des fonctions
On peut se demander dans quels cas il est utile de créer une fonction.
Une règle courante considère que dès qu’on a répété le même code plus de deux fois, il est préférable d’en faire une fonction. Celles-ci ont en effet comme avantage d’éviter la duplication du code.
Imaginons que nous avons récupéré un jeu de données avec toute une série de variables ayant les modalités "1"
et "2"
qui correspondent aux réponses "Oui"
et "Non"
à des questions. On crée un data frame fictif comportant quatre variables de ce type :
<- data.frame(
df q1 = c("1", "1", "2", "1"),
q2 = c("1", "2", "2", "2"),
q3 = c("2", "2", "1", "1"),
q4 = c("1", "2", "1", "1")
)
df#> q1 q2 q3 q4
#> 1 1 1 2 1
#> 2 1 2 2 2
#> 3 2 2 1 1
#> 4 1 2 1 1
On a vu Section 9.3 qu’on peut recoder l’une de ces variables à l’aide de la fonction fct_recode()
de l’extension forcats
:
$q1 <- fct_recode(df$q1,
df"Oui" = "1",
"Non" = "2"
)
On peut donc être tenté de dupliquer ce code autant de fois qu’on a de questions à recoder :
$q1 <- fct_recode(df$q1,
df"Oui" = "1",
"Non" = "2"
)$q2 <- fct_recode(df$q2,
df"Oui" = "1",
"Non" = "2"
)$q3 <- fct_recode(df$q3,
df"Oui" = "1",
"Non" = "2"
)$q4 <- fct_recode(df$q4,
df"Oui" = "1",
"Non" = "2"
)
Mais il est plus judicieux dans ce cas de créer une fonction pour ce recodage :
<- function(var) {
recode_oui_non <- fct_recode(var,
var_recodee "Oui" = "1",
"Non" = "2"
)return(var_recodee)
}
En effet, il est alors très simple d’appliquer ce recodage à plusieurs variables :
$q1 <- recode_oui_non(df$q1)
df$q2 <- recode_oui_non(df$q2)
df$q3 <- recode_oui_non(df$q3)
df$q4 <- recode_oui_non(df$q4) df
Autre avantage, si on réalise qu’on a commis une erreur et qu’en fait le code "1"
correspondait à "Non"
et le code "2"
à "Oui"
, on n’a pas besoin de modifier tous les endroits où on a copié/collé notre recodage : on a juste à corriger la définition de la fonction.
Les avantages de procéder ainsi sont donc multiples :
- créer une fonction évite la répétition du code et le rend moins long et plus lisible, surtout si on donne à notre fonction un nom explicite permettant de comprendre facilement ce qu’elle fait.
- créer une fonction évite les erreurs de copier/coller du code.
- une fonction permet de mettre à jour plus facilement son code : si on se rend compte d’une erreur ou si on souhaite améliorer son fonctionnement, on n’a qu’un seul endroit à modifier.
- enfin, créer des fonctions permet potentiellement de rendre son code réutilisable d’un script à l’autre ou même d’un projet à l’autre. Voire, à terme, de les regrouper dans un package pour soi-même ou pour diffusion à d’autres utilisateurs et utilisatrices de R.
14.2 Arguments et résultat d’une fonction
14.2.1 Définition des arguments
Les arguments (ou paramètres) d’une fonction sont ce qu’on lui donne “en entrée”, et qui vont soit lui fournir des données, soit modifier son comportement. La liste des arguments acceptés par une fonction est indiquée entre les parenthèses de l’appel de function()
:
<- function(arg1, arg2, arg3) {
ma_fonction print(arg1)
print(arg2)
print(arg3)
}
Une fonction peut aussi ne pas accepter d’arguments, dans ce cas on la définit juste avec function()
.
Lors de l’appel de la fonction, on peut lui passer les arguments par position :
ma_fonction(x, 12, TRUE)
Dans ce cas, arg1
vaudra x
, arg2
vaudra 12
et arg3
vaudra TRUE
.
On peut aussi passer les arguments par nom :
ma_fonction(arg1 = x, arg2 = 12, arg3 = TRUE)
Quand on passe les arguments par nom, on peut les indiquer dans l’ordre que l’on souhaite :
ma_fonction(arg1 = x, arg3 = TRUE, arg2 = 12)
Et on peut évidemment mélanger passage par position et passage par nom :
ma_fonction(x, 12, arg3 = TRUE)
Le plus souvent, les premiers arguments acceptés par une fonction sont les données sur lesquelles elle va travailler, tandis que les arguments suivants sont des paramètres qui vont modifier son comportement. Par exemple, median
accepte comme premier argument x
, un vecteur, puis un argument na.rm
qui va changer sa manière de calculer la médiane des valeurs de x
.
En général on appelle la fonction en passant les paramètres correspondant aux données par position, et les autres en les nommant. C’est ainsi qu’on ne fait ni median(x = tailles, na.rm = TRUE)
ni median(tailles, TRUE)
, mais plutôt median(tailles, na.rm = TRUE)
.
En ce qui concerne le nom des arguments, en général ceux correspondant aux données transmises à une fonction peuvent avoir des noms relativement génériques (x
, y
, v
pour un vecteur, data
ou df
pour un data.frame…). Les autres doivent par contre avoir des noms à la fois courts et explicites : par exemple plutôt decimales
que nd
ou nombre_de_decimales
.
14.2.2 Valeurs par défaut
Au moment de la définition de la fonction, on peut indiquer une valeur par défaut qui sera prise par l’argument si la personne qui utilise la fonction n’en fournit pas.
Si on reprend la fonction prop_tab
déjà définie plus haut :
<- function(v, decimales, useNA) {
prop_tab <- table(v, useNA = useNA)
tri <- tri / length(v) * 100
tri <- round(tri, decimales)
tri return(tri)
}
On peut indiquer une valeur par défaut aux arguments decimales
et useNA
de la manière suivante :
<- function(v, decimales = 1, useNA = "ifany") {
prop_tab <- table(v, useNA = useNA)
tri <- tri / length(v) * 100
tri <- round(tri, decimales)
tri return(tri)
}
Si on appelle prop_tab
en lui passant uniquement le vecteur v
, on voit que decimales
vaut bien 1 et useNA
vaut bien `“ifany”:
prop_tab(hdv2003$qualif)
#> v
#> Ouvrier specialise Ouvrier qualifie Technicien
#> 10.2 14.6 4.3
#> Profession intermediaire Cadre Employe
#> 8.0 13.0 29.7
#> Autre <NA>
#> 2.9 17.3
14.2.3 Arguments obligatoires et arguments facultatifs
Si un argument n’a pas de valeur par défaut, il est obligatoire : si l’utilisateur essaye d’appeler la fonction sans définir cet argument, cela génère une erreur.
<- function(v, decimales, useNA) {
prop_tab <- table(v, useNA = useNA)
tri <- tri / length(v) * 100
tri <- round(tri, decimales)
tri return(tri)
}
prop_tab(hdv2003$sexe)
#> Error in prop_tab(hdv2003$sexe): argument "useNA" is missing, with no default
Pour être tout à fait précis, l’erreur est générée uniquement lorsque l’argument sans valeur par défaut est utilisé dans la fonction.
Si à l’inverse un argument a une valeur par défaut, il devient facultatif : on peut appeler la fonction sans le définir.
<- function(v, decimales = 1, useNA = "ifany") {
prop_tab <- table(v, useNA = useNA)
tri <- tri / length(v) * 100
tri <- round(tri, decimales)
tri return(tri)
}
prop_tab(hdv2003$sexe)
#> v
#> Homme Femme
#> 45 55
Parfois un argument est facultatif mais on n’a pas forcément de valeur par défaut à lui attribuer. Dans ce cas on lui attribue en général par défaut la valeur NULL
, et on utilise l’instruction if()
dans la fonction pour tester s’il a été défini ou pas. Ce cas de figure est détaillé Section 17.2.4.
14.2.4 L’argument ...
Une fonction peut prendre un argument spécial nommé ...
:
<- function(x, correct = TRUE, ...) {
ma_fonction
}
Cet argument spécial “capture” tous les arguments présents et qui n’ont pas été définis avec la fonction. Par exemple, si on appelle la fonction précédente avec :
ma_fonction(1:5, correct = FALSE, title = "Titre", size = 12)
Alors ...
contiendra les arguments title
et size
et leurs valeurs.
Si on veut accéder à la valeur de size
dans ...
, on utilise list(...)$size
.
En général ...
est utilisé pour passer ces arguments à d’autres fonctions. Reprenons notre fonction my_barplot
définie précédemment :
<- function(var) {
my_barplot <- table(var)
tri barplot(tri, col = "skyblue", border = NA)
}
On pourrait permettre de personnaliser les couleurs des barres et de leurs bordures en ajoutant des arguments supplémentaires :
<- function(var, col = "skyblue", border = NA) {
my_barplot <- table(var)
tri barplot(tri, col = col, border = border)
}
Mais si on veut aussi permettre de personnaliser d’autres arguments de barplot
comme main
, xlab
, xlim
… il faudrait rajouter autant d’arguments supplémentaires à notre fonction, ce qui deviendrait vite ingérable. Une solution est de “capturer” tous les arguments supplémentaires avec ...
et de les passer directement à barplot
, de cette manière :
<- function(var, ...) {
my_barplot <- table(var)
tri <- sort(tri)
tri barplot(tri, ...)
}
Ce qui permet d’appeler notre fonction avec tous les arguments possibles de barplot
, par exemple :
my_barplot(
$clso,
hdv2003col = "yellowgreen",
main = "Croyez-vous en l'existence des classes sociales ?"
)
14.2.5 Résultat d’une fonction
On l’a vu, l’objectif d’une fonction est en général de renvoyer un résultat. Lors de la définition d’une fonction, le résultat peut être retourné en utilisant la fonction return()
:
<- function(x) {
ajoute2 <- x + 2
res return(res)
}
En réalité, l’utilisation de return()
n’est pas obligatoire : une fonction retourne automatiquement le résultat de la dernière instruction qu’elle exécute. On aurait donc pu écrire :
<- function(x) {
ajoute2 <- x + 2
res
res }
Ou même, encore mieux et plus lisible :
<- function(x) {
ajoute2 + 2
x }
Dans la suite de ce document on utilisera, lorsque c’est possible, la syntaxe la plus “compacte” qui omet le return()
.
Un point important à noter : lorsque R rencontre une instruction return()
dans une fonction, il interrompt immédiatement son exécution et “sort” de la fonction en renvoyant le résultat.
Ainsi, dans la fonction suivante :
<- function(x) {
ajoute2 return(x + 2)
* 5
x }
L’instruction x * 5
ne sera jamais exécutée car R “sort” de la fonction dès qu’il évalue le return()
de la ligne précédente.
Conséquence de ce comportement, on ne peut pas utiliser plusieurs return()
pour renvoyer plusieurs résultats depuis une seule fonction. Est-ce à dire qu’une fonction R ne pourrait renvoyer qu’une seule valeur ? Non, car si elle ne peut retourner qu’un seul objet, celui-ci peut être complexe et comporter plusieurs valeurs.
Par exemple, on a vu précédemment une fonction rudimentaire nommée indicateurs()
qui affiche la moyenne et l’écart-type d’un vecteur numérique.
<- function(v) {
indicateurs print(mean(v))
print(sd(v))
}
Plutôt que de se contenter de les afficher dans la console, on pourrait vouloir retourner ces deux valeurs pour pouvoir les réutiliser par la suite. Pour cela, une première solution pourrait être de renvoyer un vecteur comportant ces deux valeurs.
<- function(v) {
indicateurs <- mean(v)
moyenne <- sd(v)
ecart_type c(moyenne, ecart_type)
}
indicateurs(hdv2003$age)
#> [1] 48.15700 16.94181
Mais dans ce cas de figure il est recommandé de retourner plutôt une liste nommée2, de cette manière :
<- function(v) {
indicateurs <- mean(v)
moyenne <- sd(v)
ecart_type list(moyenne = moyenne, ecart_type = ecart_type)
}
indicateurs(hdv2003$age)
#> $moyenne
#> [1] 48.157
#>
#> $ecart_type
#> [1] 16.94181
On a du coup un affichage un peu plus lisible, et on peut accéder aux éléments du résultat via leur nom :
<- indicateurs(hdv2003$age)
res $moyenne
res#> [1] 48.157
14.3 Portée des variables
Un point délicat mais important quand on commence à créer ses propres fonctions concerne la portée des variables, c’est-à-dire la façon dont les objets créés dans une fonction et ceux existant en-dehors “cohabitent”. C’est une question assez complexe, mais seules quatre grandes règles sont réellement utiles au départ.
14.3.1 Une fonction peut accéder à un objet extérieur
Si on fait appel dans une fonction à un objet qui n’existe pas et n’a pas été passé comme argument, on obtient une erreur.
<- function() {
f
obj
}
f()
#> Error in f(): object 'obj' not found
Si on crée cet objet dans notre fonction avant de l’utiliser, on supprime évidemment l’erreur.
<- function() {
f <- 2
obj
obj
}
f()
#> [1] 2
Mais on peut aussi accéder depuis une fonction à un objet qui existe dans notre environnement au moment où la fonction a été appelée.
<- function() {
f
obj
}
<- 3
obj f()
#> [1] 3
Dans cet exemple, au moment de l’exécution de f()
, comme obj
n’existe pas au sein de la fonction (il n’a pas été passé comme argument ni défini dans le corps de la fonction), R va chercher dans l’environnement global, celui depuis lequel la fonction a été appelée. Comme il trouve un objet obj
, il utilise sa valeur au moment de l’appel de la fonction.
14.3.2 Les arguments et les objets créés dans la fonction sont prioritaires
Que se passe-t-il si un objet avec le même nom existe à la fois dans la fonction et dans notre environnement global ? Dans ce cas R privilégie l’objet créé dans la fonction.
<- function() {
f <- 10
obj
obj
}
<- 3
obj f()
#> [1] 10
Cette règle s’applique également pour les arguments passés à la fonction.
<- function(obj) {
f
obj
}
<- 3
obj f(20)
#> [1] 20
14.3.3 Un objet créé dans une fonction n’existe que dans cette fonction
Autre règle importante : un objet créé à l’intérieur d’une fonction n’est pas accessible à l’extérieur de celle-ci.
<- function() {
f <- 15
nouvel_objet
nouvel_objet
}
f()
#> [1] 15
nouvel_objet#> Error in eval(expr, envir, enclos): object 'nouvel_objet' not found
Ici, nouvel_objet
existe tant qu’on est dans la fonction, mais il est détruit dès qu’on en sort et donc inaccessible dans notre environnement global.
Les objets créés dans notre session et qui existent dans notre environnement (tel que visible dans l’onglet Environment de RStudio) sont appelés des objets globaux : ils existent et sont accessibles pour les fonctions appelées depuis cet environnement. Les objets créés lors de l’exécution d’une fonction sont à l’inverse des objets locaux : ils n’existent qu’à l’intérieur de la fonction et pour la durée de son exécution. Si deux objets du même nom coexistent, l’objet local est prioritaire par rapport à l’objet global.
14.3.4 On ne peut pas modifier un objet global dans une fonction
Une conséquence importante de la troisième règle est qu’il n’est pas possible de modifier un objet de notre environnement global depuis une fonction3 :
<- function() {
f <- 10
obj message("Valeur dans la fonction : ", obj)
}
<- 3
obj f()
#> Valeur dans la fonction : 10
obj#> [1] 3
Pour comprendre le résultat obtenu, on peut essayer de décomposer pas à pas :
- Au moment du
obj <- 3
, R crée un objet global nomméobj
avec la valeur 3. - Quand on exécute
f()
et qu’on rencontre l’instructionobj <- 10
, R crée un nouvel objet nomméobj
, local celui-ci, avec la valeur 10. À ce moment-là on a donc deux objets distincts portant le même nom, l’un global avec la valeur 3, l’autre local avec la valeur 10. Comme l’objet local est prioritaire, c’est lui qui est utilisé lors de l’affichage du message. - Lorsqu’on sort de
f()
, l’objet local contenant la valeur 10 est détruit. Il ne reste plus que l’objet global avec la valeur 3. C’est donc lui qui est affiché lors du dernier appel àobj
.
Pour les mêmes raisons, dans l’exemple suivant, le recodage appliqué à la variable taille
du tableau df
passé en argument à la fonction recode_taille()
n’est pas conservé en-dehors de la fonction. Ce recodage n’existe que dans un tableau d
local à la fonction, et détruit dès qu’on en est sorti.
<- data.frame(taille = c(155, 182), poids = c(65, 71))
df
<- function(d) {
recode_taille $taille <- d$taille / 100
d
}
recode_taille(df)
# Le recodage n'est pas conservé
df#> taille poids
#> 1 155 65
#> 2 182 71
Si on souhaite modifier un objet global, on doit le passer comme argument en entrée de notre fonction, et le renvoyer comme résultat en sortie. Pour que le recodage précédent soit bien répercuté dans notre tableau df
, on doit faire :
<- function(d) {
recode_taille $taille <- d$taille / 100
d
d
}
<- recode_taille(df)
df
# Le recodage est bien conservé
df#> taille poids
#> 1 1.55 65
#> 2 1.82 71
14.4 Les fonctions comme objets
Quand on crée une fonction, on la “nomme” en la stockant dans un objet. Cet objet peut être utilisé comme n’importe quel autre objet dans R. On peut ainsi copier une fonction en l’attribuant à un nouvel objet :
<- function(x) {
f + 2
x
}
<- f
g g(10)
#> [1] 12
On a déjà vu à de nombreuses reprises que quand on fournit juste un nom d’objet à R, celui-ci affiche son contenu dans la console. C’est aussi le cas pour les fonctions : dans ce cas c’est le code source de la fonction qui est affiché.
f#> function(x) {
#> x + 2
#> }
14.4.1 Passer des fonctions comme argument
Certaines fonctions sont prévues pour s’appliquer elles-mêmes à des fonctions. Par exemple, formals
et body
permettent d’afficher respectivement les arguments et le corps d’une fonction passée en argument.
formals(f)
#> $x
body(f)
#> {
#> x + 2
#> }
Il est donc possible de passer une fonction comme argument d’une autre fonction, comme dans body(f)
. On a déjà vu un exemple de ce type de fonctionnement avec la fonction tapply
Section 4.2.2. Celle-ci prend trois arguments : un vecteur de valeurs, un facteur, et une fonction. Elle applique ensuite la fonction aux valeurs pour chaque niveau du facteur.
Par exemple, si on a un data frame avec une liste de fruits et leur poids :
<- data.frame(
df fruit = c("Pomme", "Pomme", "Citron", "Citron"),
poids = c(147, 189, 76, 91)
)
df#> fruit poids
#> 1 Pomme 147
#> 2 Pomme 189
#> 3 Citron 76
#> 4 Citron 91
On peut utiliser tapply
pour calculer le poids moyen par type de fruit.
tapply(df$poids, df$fruit, mean)
#> Citron Pomme
#> 83.5 168.0
Si on souhaite plutôt calculer le poids maximal, il suffit de passer à tapply
la fonction max
plutôt que la fonction mean
.
tapply(df$poids, df$fruit, max)
#> Citron Pomme
#> 91 189
Cette manière de transmettre une fonction à une autre fonction peut être un peu déroutante de prime abord, mais c’est une mécanique qu’on va retrouver très souvent dans les chapitres suivants.
Si f
est une fonction, il est important de bien faire la différence entre f
et f()
:
f
est la fonction en elle-mêmef()
est le résultat de la fonction quand on l’exécute sans lui passer d’argument
Quand on passe une fonction comme argument à une autre fonction, on utilise donc toujours la notation sans les parenthèses.
14.4.2 Fonctions anonymes
Dans le cas où on souhaite calculer quelque chose pour lequel une fonction n’existe pas déjà, on peut créer une nouvelle fonction :
<- function(poids) {
poids_moyen_kg mean(poids / 1000)
}
Et la passer en argument à tapply()
:
tapply(df$poids, df$fruit, poids_moyen_kg)
#> Citron Pomme
#> 0.0835 0.1680
Si on ne souhaite pas réutiliser cette fonction par la suite, on peut aussi définir cette fonction directement comme argument de tapply
:
tapply(df$poids, df$fruit, function(poids) {
mean(poids/1000)
})#> Citron Pomme
#> 0.0835 0.1680
Dans ce cas on a créé ce qu’on appelle une fonction anonyme, qui n’a pas de nom (elle n’a pas été stockée dans un objet), et qui n’existe que le temps de l’appel à tapply
.
14.5 Ressources
L’ouvrage R for Data Science (en anglais), accessible en ligne, contient un chapitre complet d’introduction sur les fonctions.
L’ouvrage Advanced R (également en anglais) aborde de manière très approfondie les fonctions ainsi que la programmation fonctionnelle.
Le manuel officiel Introduction to R (toujours en anglais) contient une partie sur l’écriture de ses propres fonctions.
14.6 Exercices
14.6.1 Introduction et exemples
Exercice 1.1
Écrire une fonction nommée perimetre
qui prend en entrée un argument nommé r
et retourne le périmètre d’un cercle de rayon r
, c’est-à-dire 2 * pi * r
(pi
est un objet R qui contient la valeur de \(\pi\)).
Vérifier avec l’appel suivant :
perimetre(4)
#> [1] 25.13274
<- function(r) {
perimetre <- 2 * pi * r
resultat return(resultat)
}
Exercice 1.2
Écrire une fonction etendue
qui prend en entrée un vecteur numérique et retourne la différence entre la valeur maximale et la valeur minimale de ce vecteur.
Vérifier avec l’appel suivant :
etendue(c(18, 35, 21, 40))
#> [1] 22
<- function(v) {
etendue <- max(v)
vmax <- min(v)
vmin return(vmax - vmin)
}
Exercice 1.3
Écrire une fonction nommée alea
qui accepte un argument n
, génère un vecteur de n
valeurs aléatoires entre 0 et 1 avec la fonction runif(n)
et retourne ce vecteur comme résultat.
<- function(n) {
alea <- runif(n)
v return(v)
}
Modifier la fonction pour qu’elle accepte deux arguments supplémentaires min
et max
et qu’elle retourne un vecteur de n
valeurs aléatoires comprises entre min
et max
avec la fonction runif(n, min, max)
.
<- function(n, min, max) {
alea <- runif(n, min, max)
v return(v)
}
Modifier à nouveau la fonction pour qu’elle retourne un vecteur de n
nombres entiers aléatoires compris entre min
et max
en appliquant la fonction trunc()
au vecteur généré par runif()
.
Vérifier le résultat avec :
<- alea(10000, 1, 6)
v table(v)
#> v
#> 1 2 3 4 5 6
#> 1684 1579 1682 1715 1677 1663
<- function(n, min, max) {
alea <- runif(n, min, max + 1)
v <- trunc(v)
v return(v)
}
Exercice 1.4
Écrire une fonction nommée meteo
qui prend un argument nommé ville
avec le corps suivant :
<- readLines(paste0("https://v2.wttr.in/", ville, "?A"))
out cat(out, sep = "\n")
Tester la fonction avec par exemple meteo("Lyon")
(il est possible que l’affichage dans la console ne soit pas lisible si vous travaillez sous Windows).
<- function(ville) {
meteo <- readLines(paste0("https://v2.wttr.in/", ville, "?A"))
out cat(out, sep = "\n")
}
Exercice 1.5
Soit le code suivant, qui recode une variable du jeu de données hdv2003
en utilisant str_to_lower()
puis fct_recode()
:
library(questionr)
library(tidyverse)
data(hdv2003)
$hard.rock <- str_to_lower(hdv2003$hard.rock)
hdv2003$hard.rock <- fct_recode(hdv2003$hard.rock, "o" = "oui", "n" = "non") hdv2003
Transformer ce code en une fonction nommée recode_oui_non
, et appliquer cette fonction à hard.rock
, lecture.bd
et cuisine
.
<- function(var) {
recode_oui_non <- str_to_lower(var)
var_rec <- fct_recode(var_rec, "o" = "oui", "n" = "non")
var_rec return(var_rec)
}
$hard.rock <- recode_oui_non(hdv2003$hard.rock)
hdv2003$lecture.bd <- recode_oui_non(hdv2003$lecture.bd)
hdv2003$cuisine <- recode_oui_non(hdv2003$cuisine) hdv2003
14.6.2 Arguments et résultat
Exercice 2.1
Observer le code de la fonction suivante pour comprendre à quoi correspondent chacun de ses trois arguments, puis réordonner et renommer ces arguments de manière plus pertinente :
<- function(d, vecteur_contenant_les_donnees, supprimer_les_na) {
moyenne_arrondie <- mean(vecteur_contenant_les_donnees, na.rm = supprimer_les_na)
res <- round(res, d)
res return(res)
}
<- function(v, decimales, na.rm) {
moyenne_arrondie <- mean(v, na.rm = na.rm)
res <- round(res, decimales)
res return(res)
}
Donner aux arguments de la fonction une valeur par défaut.
<- function(v, decimales = 2, na.rm = TRUE) {
moyenne_arrondie <- mean(v, na.rm = na.rm)
res <- round(res, decimales)
res return(res)
}
Simplifier la fonction en utilisant la syntaxe plus compacte qui ne fait pas appel à return()
.
<- function(v, decimales = 2, na.rm = TRUE) {
moyenne_arrondie <- mean(v, na.rm = na.rm)
res round(res, decimales)
}
Exercice 2.2
Simplifier la fonction suivante pour que son corps ne fasse plus qu’une seule ligne :
<- function(x) {
centrer_reduire <- x - mean(x)
res <- res / sd(x)
res return(res)
}
<- function(x) {
centrer_reduire - mean(x)) / sd(x)
(x }
Exercice 2.3
Le code suivant permet de déterminer la lettre initiale et la longueur d’un mot.
<- str_sub(mot, 1, 1)
initiale <- nchar(mot) longueur
Utiliser ce code pour créer une fonction caracteristiques_mot()
qui prend un argument mot
et retourne à la fois son initiale et sa longueur.
caracteristiques_mot("Bidonnage")
#> $initiale
#> [1] "B"
#>
#> $longueur
#> [1] 9
<- function(mot) {
caracteristiques_mot <- str_sub(mot, 1, 1)
initiale <- nchar(mot)
longueur list(initiale = initiale, longueur = longueur)
}
Facultatif : modifier la fonction pour qu’elle retourne un vecteur plutôt qu’une liste, et l’appliquer sur un mot de votre choix. Que constatez-vous ?
Comme les vecteurs atomiques ne peuvent contenir que des données du même type, le nombre correspondant à longueur
a été converti en chaîne de caractères.
14.6.3 Portée des variables
Exercice 3.1
En lisant les codes suivants, essayer de prévoir quelle va être la valeur affichée par la dernière ligne. Vérifier en exécutant le code :
<- function() {
f <- 3
x
x
}
f()
<- function() {
f
x
}
<- 5
x f()
<- function(x) {
f
x
}
<- 5
x f(30)
<- function(x = 100) {
f
x
}
<- 5
x f()
<- function(x = 100) {
f <- 150
x
x
}
<- 5
x f(30)
<- function() {
f <- 5
x
}
<- 1000
x f()
x
Exercice 3.2
Dans le code suivant, on a essayé de créer une fonction qui modifie un tableau de données passé en argument pour ne conserver que les lignes correspondant aux pommes. Est-ce que ça fonctionne ?
<- data.frame(
df fruit = c("Pomme", "Pomme", "Citron", "Citron"),
poids = c(147, 189, 76, 91)
)
<- function(d) {
filtre_pommes <- dplyr::filter(d, fruit == "Pomme")
d
}
filtre_pommes(df)
df
Modifier le code pour obtenir le résultat souhaité.
<- function(d) {
filtre_pommes ::filter(d, fruit == "Pomme")
dplyr
}
<- filtre_pommes(df)
df df
14.6.4 Les fonctions comme objets
Exercice 4.1
Écrire une fonction nommée bonjour
qui ne prend aucun argument et affiche juste le texte “Bonjour !” dans la console.
<- function() {
bonjour cat("Bonjour !")
}
Exécuter dans la console les deux commandes suivantes tour à tour :
bonjour()
bonjour
Comprenez-vous la différence entre les deux ?
Copier la fonction dans un nouvel objet nommé salut
. Exécuter la nouvelle fonction ainsi créée.
<- bonjour
salut salut()
Exercice 4.2
Construire une fonction etendue()
qui prend en entrée un vecteur numérique et retourne la différence entre la valeur maximale et la valeur minimale de ce vecteur (vous pouvez récupérer le code de l’exercice 1.2).
<- function(v) {
etendue max(v) - min(v)
}
À l’aide de tapply()
, appliquez la fonction etendue()
à la variable age
pour chaque valeur de qualif
dans le jeu de données hdv2003
.
#> Ouvrier specialise Ouvrier qualifie Technicien
#> 74 68 62
#> Profession intermediaire Cadre Employe
#> 62 63 72
#> Autre
#> 78
library(questionr)
data(hdv2003)
tapply(hdv2003$age, hdv2003$qualif, etendue)
Réécrire le code précédent en utilisant une fonction anonyme (ie en définissant la fonction directement dans le tapply
).
tapply(hdv2003$age, hdv2003$qualif, function(v) {
max(v) - min(v)
})
Exercice 4.3
Exécutez le code suivant. Comprenez-vous les résultats obtenus ?
<- function(y) {
f * 4
y
}
body(f)
f(5)
body(f) <- quote(y + 2)
body(f)
f(5)
Intuitivement, comprenez-vous à quoi sert la fonction quote
?
Le jeu de données
hdv2003
fait partie de l’extensionquestionr
, il est décrit Section A.3.2.2.↩︎Les listes seront abordées un peu plus en détail Section 16.2.↩︎
En réalité c’est possible avec l’opérateur
<<-
, mais c’est fortement déconseillé dans la très grande majorité des cas.↩︎