I. Première partie – Préliminaires▲
I-A. Chapitre 1 - Introduction▲
I-A-1. Qu'est-ce que « S4 »?▲
S4 est la quatrième version de S. S est un langage qui a deux implémentations :
S-plus est commerciale, R est gratuite. La particularité de S4, par rapport au S3, est l'apparition de fonctions qui permettent de considérer S-plus comme un langage objet(1).
Par extension, S4 désigne la programmation orientée objet sous S. Et donc sous R et sous S-plus.
I-A-2. Qu'est-ce que la programmation objet ?▲
Un objet est un ensemble de variables et de fonctions qui concernent toutes le même thème : l'objet lui-même. Pas très clair ? Prenons un exemple : un objet image contiendra :
- les variables qui permettent de définir une image (comme la taille de l'image, son mode de compression, l'image proprement dite) ;
- les fonctions utilisées pour la manipulation de l'image (comme noirEtBlanc() ou redimentionner()).
Si vous êtes complètement débutant en objet et que tout ça n'est pas limpide, ne vous inquiétez pas, de nombreux exemples vont suivre.
I-A-3. Pourquoi faire de l'objet ?▲
Pour le néophyte, la programmation objet est quelque chose de lourd et les avantages ne semblent pas évidents : il faut penser son programme à l'avance, modéliser le problème, choisir ses types, penser aux liens qui lieront les objets entre eux… Que des inconvénients.
D'où la question légitime : pourquoi faire de l'objet ?
I-A-3-a. Programmation classique▲
Le plus simple est de prendre un exemple et de comparer la programmation classique à la programmation objet. L'IMC, l'Indice de Masse Corporelle est une mesure de maigreur ou d'obésité. On le calcule en divisant le poids (en kilo) par la taille au carré. Ensuite, on conclut :
- 20 < IMC < 25 : tout roule pour vous ;
- 25 < IMC < 30 : Nounours ;
- 30 < IMC < 40 : Nounours confortable ;
- 40 < IMC : Méga nounours, avec effet double douceur, mais qui devrait tout de même aller voir un médecin très vite…
- 18 < IMC < 20 : Barbie ;
- 16 < IMC < 18 : Barbie mannequin ;
- IMC < 16 : Barbie squelette, même diagnostic que le Méga nounours, attention danger…
On veut donc calculer l'IMC. En programmation classique, rien de plus simple :
>
### Programmation classique, IMC
>
poids <-
85
>
taille <-
1.84
>
(IMC <-
poids /
taille^
2
)
[
1
]
25.10633
Jusque là, rien de très mystérieux. Si vous voulez calculer pour deux personnes Moi et Elle, vous aurez :
>
### Programmation classique, mon IMC
>
poidsMoi <-
85
>
tailleMoi <-
1.84
>
(IMCmoi <-
poidsMoi /
tailleMoi^
2
)
[
1
]
25.10633
>
### Programmation classique, son IMC
>
poidsElle <-
62
>
tailleElle <-
1.60
>
(IMCelle <-
poidsMoi /
tailleElle^
2
)
[
1
]
33.20312
Ça marche… sauf qu'Elle est qualifiée de « Nounours confortable » (ou « Nounoursette » dans son cas) alors que son poids ne paraît pas spécialement excessif. Un petit coup d'œil au code révèle assez vite une erreur : le calcul de IMCelle est faux, nous avons divisé poidsMoi par tailleElle au lieu de poidsElle par tailleElle. Naturellement, R n'a pas détecté d'erreur : de son point de vue, il a juste effectué une division entre deux numeric.
I-A-3-b. Programmation objet▲
En langage objet, la démarche est toute autre. Il faut commencer par définir un objet IMC qui contiendra deux valeurs, poids et taille. Ensuite, il faut définir la fonction print qui affichera l'IMC(2) :
>
### Définition d'un objet IMC
>
setClass
("IMC"
, representation
(poids =
"numeric"
, taille =
"numeric"
))
[
1
]
"IMC"
>
setMethod
("show"
, "IMC"
,
+
function
(object) {
+
cat
("IMC ="
, object@
poids /
(object@
taille^
2
) , "\n"
)
+
}
+
)
[
1
]
"show"
Le code équivalent à ce qui a été fait dans la section I.A.3.aProgrammation classique est alors :
>
### Création d'un objet pour moi, et affichage de mon IMC
>
(monIMC <-
new
("IMC"
, poids =
85
, taille =
1.84
))
IMC =
25.10633
>
### Création d'un objet pour elle, et affichage de son IMC
>
(sonIMC <-
new
("IMC"
,poids =
62
, taille =
1.60
))
IMC =
24.21875
À partir du moment où l'initialisation est correcte (problème qui se posait également dans la programmation classique), il n'y a plus d'erreur possible. Là est toute la force de l'objet : la conception même du programme interdit un certain nombre de bogues.
Typage : l'objet protège également des erreurs de typage, c'est-à-dire les erreurs qui consistent à utiliser un type là où il faudrait en utiliser un autre. Une erreur de typage, c'est un peu comme additionner des pommes et des kilomètres au lieu d'additionner des pommes et des pommes.
Concrètement, une variable nommée poids est conçue pour contenir un poids, c'est-à-dire un nombre et non une chaîne de caractères. En programmation classique (pas de typage), poids peut contenir n'importe quoi :
>
### Programmation classique, pas de typage
>
(poids <-
"Bonjour"
)
[
1
]
"Bonjour"
En programmation objet, affecter "bonjour" à une variable qui devrait contenir un nombre provoquera une erreur :
>
new
("IMC"
, poids =
"Bonjour"
, taille =
1.84
)
Error in
validObject
(.Object) :
invalid class
"IMC"
object :
invalid object for
slot
"poids"
in
class
"IMC"
:
got class
"character"
, should be or extend class
"numeric"
Vérificateurs : l'objet permet de construire des vérificateurs de cohérence pour, par exemple, interdire certaines interdire certaines valeurs. En programmation classique, une taille peut être négative :
>
### Programmation classique, pas de contrôle
>
(TailleMoi <-
-
1.84
)
[
1
]
-
1.84
En programmation objet, on peut préciser que les tailles négatives n'existent pas :
>
### Programmation objet, contrôle
>
setValidity
("IMC"
,
+
function
(object){
+
if
(object@
taille<
0
){
+
return
("Taille négative"
)
+
}
else
{
+
return
(TRUE
)
+
}
+
}
+
)
Class "IMC"
[
in
".GlobalEnv"
]
Slots :
Name :
poids taille
Class :
numeric
numeric
>
new
("IMC"
, poids =
85
, taille =
-
1.84
)
Error in
validObject
(.Object) :
invalid class
"IMC"
object :
Taille négative
Héritage : la programmation objet permet de définir un objet comme héritier des propriétés d'un autre objet, devenant ainsi son fils. L'objet fils bénéficie ainsi de tout ce qui existe pour l'objet. Par exemple, on souhaite affiner un peu nos diagnostics en fonction du sexe de la personne. Nous allons donc définir un nouvel objet, IMCplus, qui contiendra trois valeurs : poids, taille et sexe. Les deux premières variables sont les mêmes que celles de l'objet IMC. Nous allons donc définir l'objet IMCplus comme héritier de l'objet IMC. Il pourra ainsi bénéficier de la fonction show telle que nous l'avons définie pour IMC et cela, sans aucun travail supplémentaire, puisque IMCplus hérite de IMC :
>
### Définition de l'héritier
>
setClass
("IMCplus"
,
+
representation
(sexe =
"character"
),
+
contains =
"IMC"
+
)
[
1
]
"IMCplus"
>
### Création d'un objet
>
lui <-
new
("IMCplus"
, taille =
1.76
, poids =
84
, sexe =
"Homme"
)
>
### Affichage qui utilise ce qui a été défini pour 'IMC'
>
lui
IMC =
27.11777
La puissance de cette caractéristique apparaîtra plus clairement au chapitre 9Chapitre 9 - Héritage.
Encapsulation : enfin, la programmation objet permet de définir tous les outils composant un objet et de les enfermer dans une boîte, puis de ne plus avoir à s'en occuper. Cela s'appelle l'encapsulation. Les voitures fournissent un bon exemple d'encapsulation : une fois le capot refermé, on n'a plus besoin de connaître les détails de la mécanique pour rouler. De même, une fois l'objet terminé et refermé, un utilisateur n'a pas à s'inquiéter de son fonctionnement interne. Mieux, concernant les voitures, il n'est pas possible de se tromper et de mettre de l'essence dans le radiateur puisque le radiateur n'est pas accessible. De même, l'encapsulation permet de protéger ce qui doit l'être en ne laissant accessible que ce qui ne risque rien.
I-A-4. Pour résumer▲
La programmation objet « force » le programmeur à avoir une réflexion préliminaire. On a moins la possibilité de programmer « à la va-vite » ; un plan de programme est quasi indispensable. En particulier :
- les objets doivent être déclarés et typés ;
- des mécanismes de contrôle permettent de vérifier la cohérence interne des objets ;
- un objet peut hériter des propriétés qui ont été définies pour un autre objet ;
- enfin, l'objet permet une encapsulation du programme : une fois que l'on a défini un objet et les fonctions qui s'y rattachent, on n'a plus besoin de s'occuper de la cuisine interne de l'objet.
I-A-5. Devez-vous vous mettre à l'objet ?▲
Nous venons de le voir, la programmation objet a des avantages, mais elle a aussi des inconvénients. En premier lieu, elle n'est pas facile à apprendre et on trouve assez peu de tutoriels disponibles (c'est d'ailleurs ce vide qui a donné naissance au présent ouvrage). Ensuite, la mise en route d'un projet est passablement plus lourde à gérer qu'en programmation classique où tout peut se faire petit à petit. Se pose donc la légitime question : doit-on ou ne doit-on pas faire de l'objet ?
- Pour la petite programmation de tous les jours, un nettoyage des données, une analyse univariée/bivariée, une régression, tous les outils existent déjà, pas besoin de S4.
- Pour les projets un peu plus importants, quand vous travaillez sur un nouveau concept, sur des données complexes dépassant le simple cadre du numeric et factor, alors l'objet devient une force : plus difficile au départ, mais bien moins bogué et plus facilement manipulable à l'arrivée.
- Enfin, la construction d'un package devrait toujours être réfléchie et structurée.
Dans ce cadre, la programmation objet est conseillée.
I-A-6. Le côté obscur de la programmation▲
Pour terminer avec cette longue introduction, une petite digression.
I-A-6-a. D'accord, pas d'accord▲
Vous êtes en train de lire un manuel de programmation objet et donc, d'apprendre une nouvelle méthode. Sachez qu'il n'existe pas « une » mais « des » manières de programmer : à ce sujet, les informaticiens ne sont pas toujours d'accord entre eux.
Ce livre suit une certaine vision, celle de son auteur. Des gens très compétents sont d'accord, d'autres le sont moins… Y a-t-il une vérité et si oui, où se trouve-t-elle ? Mystère. Néanmoins, pour essayer de donner une vision la plus large possible, il arrivera que ce livre vous présente plusieurs points de vue, celui de l'auteur, mais aussi celui de lecteurs qui ont réagi à ce qu'ils lisaient et qui ont fait valoir une autre vision des choses.
Ainsi, lecteur avisé, vous aurez tous les éléments en main, vous pourrez peser le pour et le contre et vous choisirez votre voie en toute liberté, à la lumière de la connaissance… |
I-A-6-b. Les fusées, la bourse, les bogues… et les coupables !▲
Quand les hommes ont commencé à envoyer des fusées dans l'espace(3) et qu'elles ont explosé en plein vol, ils ont écrasé une petite larme et ont cherché les causes de l'échec. Comme il fallait bien brûler quelqu'un, ils ont cherché un coupable. Et ils ont trouvé… les informaticiens. « C'est pas d'not' faute, ont déclaré les informaticiens tout marris, c'est un fait avéré intrinsèque aux ordinateurs : tous les programmes sont bogués ! » Sauf que dans le cas présent, la facture du bogue était plutôt salée…
Des gens très forts et très intelligents ont donc cherché des moyens de rendre la programmation moins boguée. Ils ont fabriqué de nouveaux langages et défini des règles de programmation. On appelle ça la programmation propre ou les bonnes pratiques. Certaines opérations, certaines pratiques, certaines manières de programmer sont donc qualifiées de bonnes, belles ou propres ; d'autres, au rebours, sont qualifiées de mauvaises, dangereuses, impropres voire sales (le terme exact consacré par les informaticiens est crade…). Il ne s'agit pas là de jugements de valeur, ce sont simplement des qualificatifs qui indiquent que ce type de programmation favorise l'apparition d'erreurs dans le code. Donc à éviter d'urgence.
I-A-6-c. R, langage propre ?▲
R n'est pas un langage très propre. Quand un informaticien veut ajouter un package, il est libre de faire à peu près ce qu'il veut (ce qui est une grande force) y compris la définition d'instructions « surprenantes » (ce qui est une faiblesse). Une instruction « surprenante » est une instruction dont le fonctionnement est contraire à l'intuition. Par exemple, sachant que numeric() désigne un numeric vide et que integer() est en entier vide, comment désigner une matrice vide ? Toute définition autre que matrix() serait surprenante. De nombreuses surprises existent dans R… (voir la section II.A.6L'objet vide pour plus de détails sur les objets vides). R permet donc de faire tout un tas d'opérations dangereuses. Il sera néanmoins parfois possible de le rendre propre en nous auto-interdisant des manipulations. Mais ça ne sera pas toujours le cas, nous serons amenés à utiliser des outils impropres. En tout état de cause, les passages dangereux seront signalés par le petit logo qui orne le début de ce paragraphe.
Mais tout le monde ne partage pas ce point de vue : « On a néanmoins besoin des langages ultraflexibles et peu contraints, précise un relecteur, parce qu'ils rendent certaines applications faciles à programmer alors que les langages propres ne les gèrent pas ou mal. Dans ce genre de cas, utiliser un langage propre revient un peu à prendre un marteau-piqueur pour écraser une noix. En particulier, écrire le cœur d'un programme en C (au lieu de R) peut accroître la rapidité d'exécution par un facteur 10 ou 20. »
C'est vrai. Mais tout dépend du niveau du programmeur : à haut niveau, pour ceux qui ne font pas d'erreur, l'efficacité peut primer sur la propreté. À notre niveau, il me paraît surtout important de programmer proprement pour résoudre le problème principal : programmer sans bogue.
En vous souhaitant de ne jamais tomber du côté obscur de la programmation… |
I-B. Chapitre 2 - Généralités sur les objets▲
I-B-1. Définition formelle▲
Un objet est un ensemble cohérent de variables et de fonctions qui tournent autour d'un concept central. Formellement, un objet est défini par trois éléments :
- la classe est le nom de l'objet. C'est aussi son architecture, la liste de variables et de fonctions qui le composent ;
- les variables de l'objet sont appelées attributs ;
- les fonctions de l'objet sont appelées méthodes.
Dans l'introduction, nous avions défini un objet de classe IMC. Il avait pour attributs poids et taille, pour méthode show.
I-B-1-a. Les attributs▲
Les attributs sont simplement des variables typées. Une variable typée est une variable dont on a fixé la nature une bonne fois pour toutes. Dans R, on peut écrire poids <- 62 (à ce stade, poids est un numeric) puis poids <- "bonjour" (poids est devenu un character). Le type de la variable poids peut changer.
En programmation objet, il ne sera pas possible de changer le type des attributs en cours de programmation(4). Cela semble être une contrainte, c'est en fait une sécurité. En effet, si une variable a été créée pour contenir un poids, elle n'a aucune raison de recevoir une chaîne de caractères… sauf erreur de programmation.
I-B-1-b. Les méthodes▲
On distingue quatre types d'opérations à faire sur les objets :
- méthodes de création : entrent dans cette catégorie toutes les méthodes permettant de créer un objet. La plus importante s'appelle le constructeur. Mais son utilisation est un peu rugueuse pour l'utilisateur. Le programmeur écrit donc des méthodes faciles d'accès pour créer un objet à partir de données, d'un fichier ou à partir d'un autre objet (par exemple numeric ou read.csv2) ;
- validation : en programmation objet, il est possible de vérifier que les attributs respectent certaines contraintes. Il est également possible de créer des attributs à partir de calculs faits sur les autres attributs. Tout cela rentre dans la validation d'objet ;
- manipulation des attributs : modifier et lire les attributs n'est pas aussi anodin que dans la programmation classique. Aussi, dédie-t-on des méthodes à la manipulation des attributs (par exemple names et names<-) ;
- Autres : tout ce qui précède constitue une sorte de « minimum légal » à programmer pour chaque objet. Restent ensuite les méthodes spécifiques à l'objet, en particulier les méthodes d'affichage et les méthodes effectuant des calculs.
Les méthodes sont des fonctions typées. Le type d'une fonction est la juxtaposition de deux types : son type entrée et son type sortie.
- Le type-entrée est simplement l'ensemble des types des arguments de la fonction. Par exemple, la fonction trace prend pour argument une matrice, son type-entrée est donc matrix.
- Le type-sortie est le type de ce que la fonction retourne. Par exemple, la fonction trace retourne un nombre, son type-sortie est donc numeric.
Finalement, le type d'une fonction est (type-entree,type-sortie). On le note Type-Sortie <- fonction(TypeEntree).
Par exemple, le type de la fonction trace est numeric <- trace(matrix).
I-B-2. Dessiner, c'est gagner !▲
Il vaut mieux un bon dessin qu'un long discours. La maxime s'applique particulièrement bien à la programmation objet. Chaque classe est représentée par un rectangle dans lequel sont notés les attributs et les méthodes avec leur type.
L'héritage (voir chapitre 9Chapitre 9 - Héritage) entre deux classes est noté par une flèche, du fils vers le père.
I-C. Chapitre 3 - Exemple de travail▲
Plutôt que d'inventer un exemple avec des 'wiz', des 'spoun' ou des concepts mathématiques complexes truffés d'équations, voilà un cas réel (fortement simplifié, mais réel) : la doctoresse Tam travaille sur des patientes anorexiques. Semaine après semaine, elle mesure leur IMC (exemple : 17, puis 17.2, puis 17.1, puis 17.4). Pour une patiente, la suite obtenue forme une trajectoire (exemple : 17, 17.3, 17.2, 17.4). Graphiquement, on obtient :
L'IMC de la demoiselle augmente progressivement. Lorsqu'on dispose de plusieurs trajectoires, les graphes deviennent illisibles :
D'où la nécessité, pour y voir plus clair, de regrouper les trajectoires.
Elle veut donc classer ses patientes en groupes selon des critères bien précis : [AGrandi] (Oui)/(Non), [DemandeAVoirSesParents] (Oui)/(Non)/(Refuse), [RemangeRapidement] (Oui)/(Non). Finalemment, son but est de comparer différentes manières de regrouper les trajectoires.
I-C-1. Analyse du problème▲
Ce problème est découpable en trois objets.
- Le premier sera celui qui contiendra les trajectoires des patientes.
- Le deuxième représentera un découpage en groupe (que nous appellerons une partition).
- Le troisième sera un mélange des deux : les trajectoires partitionnées en groupes.
I-C-2. L'objet Trajectoires▲
Tam nous prévient que pour un groupe donné, les mesures sont faites soit toutes les semaines, soit tous les quinze jours. L'objet Trajectoires doit en tenir compte. Il sera donc défini par deux attributs :
- temps : numéro de la semaine où les mesures sont effectuées. Pour simplifier, la semaine où commence le suivi du groupe aura le numéro 1 ;
- traj : les trajectoires de poids des patientes.
Exemple :
Quelle(s) méthode(s) pour cet objet ? Tam nous dit que dans ce genre de recherche, il y a souvent des données manquantes. Il est important de savoir combien il y en a. Nous définirons donc une première méthode qui comptera le nombre de valeurs manquantes.
La deuxième méthode sera à cheval entre les objets Trajectoires et Partition(5) : une manière classique de construire une partition de groupes est de distinguer les patientes selon leur IMC initial : dans notre exemple, on pourrait considérer deux groupes, les « IMC initial faible » et « IMC initial haut ». Il faudra donc définir une méthode de découpage selon l'IMC initial et le nombre de groupes souhaité.
Enfin, la présence de valeurs manquantes peut interdire l'utilisation d'outils statistiques. Il sera donc intéressant de pouvoir les imputer(6). La troisième méthode imputera les manquantes.
I-C-3. L'objet Partition▲
Une partition est un ensemble de groupes. Par exemple, les groupes pourraient être A ={T1,T3} et B={T2,T4}. Il sera sans doute plus simple de les considérer comme un vecteur ayant pour longueur le nombre de patientes : {A,B,A,B}. Dans un certain nombre de cas, il faudra également connaître le nombre de groupes, en particulier quand un groupe est manquant : si Tam classe ses ados en trois groupes et qu'elle obtient la partition {A,C,C,A}, il est important de garder en mémoire que le groupe B existe, même s'il ne contient personne. D'où deux attributs pour l'objet Partition :
- nbGroupes : donne le nombre de groupes.
- part : la suite des groupes auxquels les trajectoires appartiennent.
Exemple :
Certains statisticiens (et même certains logiciels) transforment les variables nominales en chiffres. Nous pourrions définir part comme un vecteur d'entier. « C'est beaucoup plus pratique », s'entend-on dire régulièrement. Sauf que ce faisant, on trompe R sur la nature de nos variables : R est conçu pour savoir calculer la moyenne d'une variable numérique, mais refuser de calculer la moyenne d'une variable nominale (un factor). Si nous codons part avec des chiffres, R acceptera de calculer la moyenne d'une partition, ce qui n'a aucun sens. Ce serait un peu comme faire la moyenne de Tortue, Girafe et Tigre… Une variable nominale doit donc être codée par un factor, une numérique par un numeric. Oui, nous savons, dans certains cas, il serait un peu plus pratique de coder part par des nombres, mais c'est beaucoup plus dangereux. Donc à éviter d'urgence !
I-C-4. L'objet TrajDecoupees▲
TrajDecoupees sera l'objet regroupant un objet Trajectoires et plusieurs Partition. En effet, il est possible que, pour un ensemble de trajectoires, plusieurs partitionnements soient intéressants. Il sera donc héritier de Trajectoires. Par contre, il ne sera pas héritier de Partition parce que les deux classes ne partagent pas vraiment de propriétés. Pour plus de détails, voir chapitre 9Chapitre 9 - Héritage.
- temps : le temps auquel les mesures sont effectuées (comme dans 'trajectoires').
- traj : les trajectoires de poids des patientes (comme dans 'trajectoires').
- listePartitions : un ensemble de partitions.
Exemple :
I-C-5. Plan d'analyse▲
Pour résumer, nous avons donc trois classes :
La flèche pleine (du fils vers le père) indique l'héritage, la flèche en pointillé désigne l'inclusion d'un objet dans l'autre, sans héritage.
I-C-6. Application à R▲
Et R dans tout ça, seriez-vous en droit de réclamer ? C'est également une des caractéristiques des langages objet. Nous avons fait une analyse relativement poussée (poussée pour un problème aussi simple que le nôtre) et il n'y a toujours pas la moindre ligne de code R…. Théoriquement, on pourrait même choisir de coder dans un autre langage.
Mais bon, là n'est pas le sujet du jour.
Appliquées à R, les méthodes définies au I.B.1Les méthodes deviennent :
- méthodes de création : la méthode de création principale porte le nom de la classe ;
- validation : cela dépend de l'objet ;
- manipulation des attributs : pour chaque attribut, il faudra une méthode permettant d'accéder à sa valeur et une méthode permettant de la modifier ;
- autres : les méthodes « autres » dépendent des particularités de l'objet, à l'exception toutefois des méthodes d'affichage. Pour l'affichage, show permet un affichage sommaire de l'objet quand on tape son nom dans la console. print donne un affichage plus complet. plot est l'affichage graphique.
Pour finir avec les particularités de R, la majorité des langages objet forcent le programmeur à grouper tout ce qui concerne un objet dans un même endroit. Cela s'appelle l'encapsulation. R n'a pas cette propriété : vous pouvez très bien déclarer un objet, puis un autre, puis une méthode pour le premier objet, puis déclarer un troisième objet, et ainsi de suite. C'est fortement déconseillé et de surcroît facilement évitable en suivant une règle toute simple.
À chaque objet son fichier. |
Tout ce qui concerne un objet doit être dans un unique ficher, un fichier ne doit contenir que des informations relatives à un unique objet.