IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Petit Manuel de S4

Programmation Orientée Objet sous R


précédentsommairesuivant

II. Deuxième partie - Les bases de l'objet

II-A. Chapitre 4 - Déclaration des classes

Nous l'avons évoqué dans les chapitres précédents, une classe est un ensemble d'attributs et de méthodes. Nous allons maintenant définir les uns et les autres.

Dans la majorité des langages objet, la définition de l'objet contient les attributs et les méthodes. En R, la définition ne contient que les attributs. Les méthodes sont précisées ensuite. C'est dommage, ça atténue la puissance de l'encapsulation, mais c'est comme ça. L'utilisateur averti (c'est-à-dire vous) peut compenser « manuellement », par exemple en se forçant à définir attributs et méthodes en bloc dans un même fichier, et en utilisant un fichier par objet.

II-A-1. Définition des attributs

La première étape est de définir les attributs de l'objet proprement dit. Cela se fait à l'aide de l'instruction setClass. setClass est une fonction qui prend deux arguments (et quelques autres ingrédients que nous verrons plus tard).

  • Class (avec une majuscule) est le nom de la classe que nous sommes en train de définir.
  • representation est la liste des attributs de la classe.

Comme nous l'avons vu dans l'exemple introductif, la programmation objet fait du contrôle de type, c'est-à-dire qu'elle ne permettra pas à une chaîne de caractères d'être rangée là où devrait se trouver un entier. Chaque attribut doit être déclaré avec son type.

 
Sélectionnez
> setClass (
+     Class="Trajectoires",
+     representation= representation(
+         temps = "numeric",
+         traj = "matrix"
+     )
+ )

[1] "Trajectoires"

Malheureusement, il est tout de même possible de ne pas typer (ou de mal typer) en utilisant les listes. Le non typage est fortement déconseillé, mais lors de l'utilisation de listes, il n'y a pas d'autre option.

II-A-2. Constructeur par défaut

On peut ensuite créer un objet trajectoire grâce au constructeur new :

 
Sélectionnez
> new(Class="Trajectoires")

An object of class "Trajectoires"
Slot "temps":
numeric(0)

Slot "traj":
<0 x 0 matrix>

Comme vous pouvez le constater, l'affichage n'est pas extraordinaire. Il sera important de définir une méthode pour l'améliorer.

En général, on définit un objet en spécifiant les valeurs de ses attributs. On doit, à chaque fois, préciser le nom de l'attribut en question :

 
Sélectionnez
> new(Class="Trajectoires", temps =c(1 ,3 ,4))

An object of class "Trajectoires"
Slot "temps":
[1]  1 3 4

Slot "traj":
<0 x 0 matrix>

> new(Class="Trajectoires",temps =c(1 ,3) , traj =matrix(1:4 , ncol =2))

An object of class "Trajectoires"
Slot "temps":
[1]  1 3

Slot "traj":
     [,1] [,2]
[1,]    1    3
[2,]    2    4

Naturellement, un objet se stocke dans une variable comme n'importe quelle autre valeur de R. Pour illustrer nos dires, nous allons constituer une petite base de travail.

Trois hôpitaux participent à l'étude. La Pitié Salpêtrière (qui n'a pas encore rendu son fichier de données, honte à eux), Cochin et Sainte-Anne :

 
Sélectionnez
> trajPitie <- new(Class="Trajectoires")
> trajCochin <- new(
+     Class="Trajectoires",
+     temps =c(1 ,3 ,4 ,5) ,
+     traj =rbind(
+         c(15,   15.1, 15.2, 15.2),
+         c(16,   15.9, 16,   16.4),
+         c(15.2, NA,   15.3, 15.3),
+         c(15.7, 15.6, 15.8, 16)
+     )
+ )
> trajStAnne <- new(
+     Class="Trajectoires",
+     temps =c(1:10, (6:12) *2),
+     traj =rbind(
+         matrix(seq(16, 19, length=17), ncol =17, nrow =50, byrow=TRUE),
+         matrix(seq(15.8, 18, length=17), ncol =17, nrow =30, byrow=TRUE)
+     ) + rnorm(17*80, 0, 0.2)
+ )

II-A-3. Accéder aux attributs

Toute cette section est placée sous le double signe du danger… et du danger…

L'accès aux attributs se fait grâce à l'opérateur @ :

 
Sélectionnez
> trajCochin@temps

[1] 1 3 4 5

> trajCochin@temps <- c(1, 2, 4, 5)
> trajCochin

An object of class "Trajectoires"
Slot "temps":
[1] 1 2 4 5

Slot "traj":
     [,1]  [,2]  [,3]  [,4]
[1,] 15.0  15.1  15.2  15.2
[2,] 16.0  15.9  16.0  16.4
[3,] 15.2    NA  15.3  15.3
[4,] 15.7  15.6  15.8  16.0

Comme nous le verrons par la suite, l'utilisation de @ est à éviter. En effet, il ne fait pas appel aux méthodes de vérification. L'utilisation que nous présentons ici (affichage d'un attribut et, pire encore, affectation d'une valeur à un attribut) est donc à proscrire dans la plupart des cas. En tout état de cause, l'utilisateur final ne devrait jamais avoir à l'utiliser.

Il est également possible d'utiliser les fonctions attr ou attributes, mais c'est largement pire : en effet, si on fait une simple erreur de typographie, on modifie la structure de l'objet. Et ça, c'est très très très mal !

II-A-4. Valeurs par défaut

On peut déclarer un objet en lui donnant des valeurs par défaut. À chaque création, si l'utilisateur ne spécifie pas les valeurs des attributs, ceux-ci en auront quand même une. Pour cela, on doit ajouter l'argument prototype à la définition de l'objet :

 
Sélectionnez
> setClass (
+     Class="TrajectoiresBis",
+     representation= representation(
+         temps = "numeric",
+         traj = "matrix"
+     ),
+     prototype=prototype(
+         temps = 1,
+         traj = matrix(0)
+     )
+ )

[1] "TrajectoiresBis"

L'initialisation par défaut était quelque chose de nécessaire à l'époque lointaine où, si on n'initialisait pas une variable, on risquait d'écrire dans la mémoire système (et donc de provoquer un blocage de l'ordinateur, la perte de notre programme et d'autres trucs encore plus terribles). Aujourd'hui, une telle chose n'est plus possible. Si on n'initialise pas un attribut, R lui donne pour valeur un objet vide du type adéquat.

Du point de vue philosophique, lorsqu'on crée un objet, soit on connaît sa valeur auquel cas on la lui affecte, soit on ne la connaît pas auquel cas il n'y a aucune raison de lui donner une valeur plutôt qu'une autre. L'utilisation d'une valeur par défaut semble donc être plus une réminiscence du passé qu'une réelle nécessité. Elle n'a plus lieu d'être.

De plus, dans le feu de la programmation, il peut arriver que l'on « oublie » de donner à un objet sa vraie valeur. S'il existe une valeur par défaut, elle sera utilisée. S'il n'y a pas de valeur par défaut, cela provoquera une erreur ce qui, dans ce cas précis, est préférable, cela attire notre attention et nous permet de corriger. Donc, les valeurs par défaut autres que les valeurs vides sont à éviter.

II-A-5. Supprimer un objet

Dans le cas précis de l'objet trajectoires, il n'y a pas vraiment de valeur par défaut qui s'impose. Il est donc préférable de conserver la classe comme elle a été initialement définie. La classe TrajectoiresBis n'a plus de raison d'être. On peut la supprimer grâce à : 

 
Sélectionnez
> removeClass("TrajectoiresBis")

[1] TRUE

> new(Class="TrajectoiresBis")

Error in getClass (Class, where = topenv(parent.frame())) :

"TrajectoiresBis" is not a defined class

Supprimer la définition d'une classe ne supprime pas les méthodes qui lui sont associées. Pour supprimer définitivement une classe dans le sens classique du terme, il faut supprimer la classe puis supprimer toutes ses méthodes…

En particulier, vous créez une classe et ses méthodes. Ça ne marche pas comme prévu. Vous décidez de tout reprendre à zéro. Vous effacez donc la classe. Si vous la recréez, toutes les anciennes méthodes seront à nouveau actives.

II-A-6. L'objet vide

Certaines fonctionnalités objet appellent la fonction new sans lui transmettre d'argument. Par exemple, nous utiliserons dans la section sur l'héritage l'instruction as(tdCochin, "Trajectoires") (voir section III.B.8is, as et as<-). Cette instruction fait appel à new("Trajectoires"). Il est donc indispensable, lors de la construction d'un objet, de toujours garder à l'esprit que new doit être utilisable sans argument. Comme les valeurs par défaut sont déconseillées, il faut prévoir la construction de l'objet vide. Cela sera important en particulier lors de la construction de la méthode show.

Un objet vide est un objet possédant tous les attributs normaux d'un objet, mais ceux-ci sont vides, c'est-à-dire de longueur zéro. Par contre, un objet vide a une classe. Ainsi, un numeric vide est différent d'un integer vide.

 
Sélectionnez
> identical(numeric(), integer())

[1] FALSE

Quelques règles à connaître concernant les objets vides :

  • numeric() et numeric(0) désignent un numeric vide ;
  • même chose pour character() et character(0) ;
  • même chose pour integer() et integer(0) ;
  • par contre, factor() désigne un factor vide, factor(0) désigne un factor de longueur 1 et contenant l'élément zéro ;
  • plus problématique, matrix() désigne une matrice à une ligne et une colonne contenant NA. En tout état de cause, ça n'est pas la matrice vide (son attribut length vaut 1). Pour définir une matrice vide, il faut utiliser matrix(nrow=0,ncol=0) ;
  • même chose pour les array() ;
  • NULL représente l'objet nul. Ainsi, NULL est de classe NULL alors que numeric() est de classe numeric.

Pour tester qu'un objet est l'objet vide, il faut tester son attribut length. De même, si nous décidons de définir la méthode length pour nos objets, il faudra prendre garde à ce que length(monObjet)=0 soit vrai si, et seulement si, l'objet est vide (pour assurer une certaine cohérence à R).

II-A-7. Voir l'objet

Vous venez de créer votre première classe. Félicitations ! Pour pouvoir vérifier ce que vous venez de faire, plusieurs instructions permettent de « voir » la classe et sa structure. Le terme savant désignant ce qui permet au programme de voir le contenu ou la structure des objets s'appelle l'introspection.

slotNames donne le nom des attributs. getSlots donne le nom des attributs et leur type. getClass donne les noms des attributs et leur type, mais aussi les héritiers et les ancêtres. Comme l'héritage est encore « terra incognita », ça ne fait pour l'instant aucune différence :

 
Sélectionnez
> slotNames("Trajectoires")

[1] "temps" "traj"

> getSlots("Trajectoires")

    temps      traj
"numeric"  "matrix"

> getClass("Trajectoires")

Class "Trajectoires" [in ".GlobalEnv"]

Slots :

Name:    temps    traj
Class: numeric  matrix

II-B. Chapitre 5 - Les méthodes

Un des intérêts de l'objet est de pouvoir définir des fonctions qui vont adapter leur comportement à l'objet. Exemple que vous connaissez déjà, la fonction plot réagit différemment selon la classe de ses arguments : 

 
Sélectionnez
> taille <- rnorm(10 ,1.70 ,10)
> poids <- rnorm(10 ,70 ,5)
> groupe <- as.factor(rep(c("A","B"), 5))
> plot(taille~poids)
> plot(taille~groupe)
Image non disponible

Le premier plot trace un nuage de points, le deuxième dessine des boîtes à moustaches.

De même, il va être possible de définir un comportement spécifique pour nos trajectoires.

II-B-1. setMethod

Pour cela, on utilise la fonction setMethod. Elle prend trois arguments :

  1. f est le nom de la fonction que nous sommes en train de redéfinir. Dans notre cas, plot ;
  2. signature est le nom de la classe à laquelle elle s'applique. Nous aurons l'occasion d'y revenir section III.A-2signature ;
  3. definition est la fonction à utiliser. Dans notre cas, nous allons simplement utiliser un matplot en prenant en compte les temps des mesures et en les affichant sur l'axe des abscisses
 
Sélectionnez
> setMethod(
+     f ="plot",
+     signature ="Trajectoires",
+     definition = function(x,y ,...){
+         matplot(x@temps, t(x@traj), xaxt ="n", type ="l",
+                 ylab ="", xlab ="", pch =1, col =1, lty =1)
+         axis (1, at= x@temps)
+     }
+ )

[1] "plot"

> plot(trajCochin)
> plot(trajStAnne)
Image non disponible

Petite remarque : R nous impose, lors de la redéfinition d'une fonction, d'utiliser les mêmes arguments que la fonction en question. Pour connaître les arguments de plot, on peut utiliser args

 
Sélectionnez
> args(plot)

function (x, y, ...)
NULL

Nous sommes donc obligés d'utiliser function(x,y,...) même si nous savons d'ores et déjà que l'argument y n'a pas de sens. De plus, les noms par défaut ne sont pas vraiment uniformisés, certaines fonctions appellent object, d'autres .Object, d'autres encore x. R-la-goélette vogue au gré de ses programmeurs…

II-B-2. show et print

show et print servent pour l'affichage. Nous allons les définir pour les trajectoires. args(print) nous indique que print prend pour argument (x,...). Donc :

 
Sélectionnez
> setMethod("print", "Trajectoires",
+     function(x,...) {
+         cat("*** Class Trajectoires, method Print ***\n")
+         cat("* Temps = ");  print(x@temps)
+         cat("* Traj = \n"); print(x@traj)
+         cat("******* Fin Print(trajectoires) *******\n")
+     }
+ )

[1] "print"

> print(trajCochin)

*** Class Trajectoires, method Print ***
* Temps = [1] 1 2 4 5
* Traj  =
     [,1]  [,2]  [,3]  [,4]
[1,] 15.0  15.1  15.2  15.2
[2,] 16.0  15.9  16.0  16.4
[3,] 15.2    NA  15.3  15.3
[4,] 15.7  15.6  15.8  16.0
******* Fin Print(trajectoires) *******

Pour Cochin, le résultat est satisfaisant. Pour Sainte-Anne (qui compte 80 lignes), on ne verrait pas grand-chose, d'où le besoin d'une deuxième méthode d'affichage.

show est la méthode utilisée quand on tape le nom d'un objet. Nous allons donc la redéfinir en prenant en compte la taille de l'objet : s'il y a trop de trajectoires, show n'en affichera qu'une partie.

 
Sélectionnez
> setMethod("show", "Trajectoires",
+     function(object){
+         cat("*** Class Trajectoires, method Show ***\n")
+         cat("* Temps = ");print(object@temps)
+         nrowShow <- min(10, nrow(object@traj))
+         ncolShow <- min(10, ncol(object@traj))
+         cat("* Traj (limité à une matrice 10x10) = \n")
+         print(formatC(object@traj[1:nrowShow, 1:ncolShow]),
+               quote =FALSE)
+         cat("* ... ...\ n")
+         cat("******* Fin Show(trajectoires) *******\n")
+     }
+ )

[1] "show"

> trajStAnne

*** Class Trajectoires, method Show ***
* Temps = [1] 1 2 3 4 5 6 7 8 9 10 12 14 16 18 20 22 24
* Traj (limité à une matrice 10x10) =
      [,1]   [,2]   [,3]   [,4]   [,5]   [,6]   [,7]   [,8]   [,9]   [,10]
 [1,] 16.25  16.1   16.16  16.53  16.67  17.2   17.55  17.3   17.47  17.55
 [2,] 16.23  16.36  16.45  16.4   17.14  17     16.84  17.25  17.98  17.43
 [3,] 15.89  16.02  16.15  16.69  16.77  16.62  17.52  17.26  17.46  17.6
 [4,] 15.63  16.25  16.4   16.6   16.65  16.96  17.02  17.39  17.5   17.67
 [5,] 16.16  15.85  15.97  16.32  16.73  16.94  16.73  16.98  17.64  17.72
 [6,] 15.96  16.2   16.53  16.4   16.47  16.95  16.73  17.36  17.33  17.55
 [7,] 16.03  16.33  16.23  16.67  16.79  17.14  17     17.35  17.58  17.99
 [8,] 15.69  16.06  16.63  16.72  16.81  17.16  16.98  17.41  17.51  17.43
 [9,] 15.82  16.17  16.75  16.76  16.78  16.51  17.19  17.21  17.84  17.95
[10,] 15.98  15.76  16.1   16.54  16.78  16.89  17.22  17.18  16.94  17.36
* ... ...
******* Fin Show(trajectoires) *******

Reste un problème à régler. Nous avons vu section II.A.6L'objet vide que new devait être utilisable sans argument. Or, il ne l'est plus :

 
Sélectionnez
> new("Trajectoires")

*** Class Trajectoires, method Show ***
* Temps = numeric(0)
* Traj (limité à une matrice 10x10) =
Error in print(formatC(object@traj[1:nrowShow, 1:ncolShow]), quote
= FALSE ) :
    erreur lors de l'évaluation de l'argument 'x' lors de
la sélection d'une méthode pour la fonction 'print'

En effet, new crée un objet, puis l'affiche en utilisant show. Dans le cas de new sans argument, l'objet vide est passé à show. Or, show, tel que nous l'avons conçu, ne peut pas traiter l'objet vide.

D'une manière plus générale, toutes nos méthodes doivent prendre en compte le fait qu'elles auront peut-être à traiter l'objet vide :

 
Sélectionnez
> setMethod("show", "Trajectoires",
+     function(object){
+         cat("*** Class Trajectoires, method Show ***\n")
+         cat("* Temps = ");print(object@temps)
+         nrowShow <- min(10, nrow(object@traj))
+         ncolShow <- min(10, ncol(object@traj))
+         cat("* Traj (limité à une matrice 10x10) = \n")
+         if (length( object@traj)!=0){
+             print(formatC(object@traj[1:nrowShow, 1:ncolShow]),
+                   quote =FALSE)
+         } else {}
+         cat("* ... ...\ n")
+         cat("******* Fin Show(trajectoires) *******\n")
+     }
+ )

[1] "show"

> new("Trajectoires")

*** Class Trajectoires, method Show ***
* Temps = numeric(0)
* Traj (limité à une matrice 10x10) =
* ... ...
******* Fin Show(trajectoires) *******

Ça marche !

Nous disposons donc de deux méthodes d'affichage. Par défaut, show montre l'objet ou une partie de l'objet si celui-ci est trop grand ; print permet d'afficher l'intégralité de l'objet.

II-B-3. setGeneric

Jusqu'à présent, nous n'avons fait que définir pour l'objet Trajectoires des méthodes qui existaient déjà par ailleurs (print existait pour les numeric, pour les character…). Nous allons maintenant définir une méthode nouvelle. Pour cela, il nous faut la déclarer. Cela se fait à l'aide de la fonction setGeneric. À ce stade, une petite digression sur le concept de générique s'impose.

II-B-3-a. Générique versus Spécifique

En S4, les fonctions doivent être définies de deux manières : générique et spécifique. La définition générique d'une fonction est la donnée d'un comportement global (ou conceptuel). La définition spécifique d'une fonction est l'application de ce concept à un cas particulier. Un exemple va éclaircir les choses.

plot est une fonction qui représente graphiquement des données. Telle est sa définition générique. Le type précis de représentation n'entre pas en ligne de compte dans la définition générique. Celle-ci ne s'occupe pas de détail ou des cas particuliers, elle reste floue, générale…

plot appliqué à une variable numeric trace un histogramme. C'est une définition spécifique. Nous ne sommes plus au niveau du concept mais dans la discussion pratique : il faut définir le type de graphe qui sera précisément utilisé. Dans notre cas, c'est un histogramme.

Une fonction n'a qu'une définition générique, mais peut avoir plusieurs définitions spécifiques. Par exemple, plot appliqué à un (numeric,factor) trace des boîtes à moustaches. C'est une deuxième définition spécifique.

Dans R, chaque fonction spécifique doit nécessairement être connue du programme comme fonction générique : avant d'entrer dans les détails, il faut définir le concept global.

Une méthode nouvelle devra donc, en premier lieu, être déclarée comme fonction générique, puis comme fonction spécifique. Une fonction dont la générique existe déjà (comme print) n'a pas besoin d'être déclarée en générique et peut directement être déclarée en spécifique, comme nous l'avons fait au paragraphe précédent. Mais toute fonction nouvelle doit être déclarée en générique.

II-B-3-b. Définition formelle

Définir une fonction générique se fait grâce à setGenericqui prend deux arguments :

  • name est le nom de la méthode que nous allons définir ;
  • def est un exemple de fonction qui sera utilisé pour la définir.

À ce stade, il n'est pas possible de la typer puisqu'elle est générique et doit donc être utilisable pour plusieurs types différents.

 
Sélectionnez
> setGeneric(
+     name="compterManquantes",
+     def = function(object){standardGeneric("compterManquantes")}
+ )

[1] "compterManquantes"

compterManquantes a donc été ajouté à la liste des méthodes que R connaît. Nous pouvons maintenant la définir comme fonction spécifique pour l'objet Trajectoires :

 
Sélectionnez
> setMethod(
+     f ="compterManquantes",
+     signature ="Trajectoires",
+     definition = function(object){
+         return(sum(is.na(object@traj)))
+     }
+ )

[1]  "compterManquantes"

> compterManquantes(trajCochin)

[1]  1
II-B-3-c. lockBinding

Il n'y a pas de contrôle sur l'utilisation d'un setGeneric : si une fonction générique existe déjà, la nouvelle définition détruit l'ancienne - un peu de la même manière qu'affecter une valeur à une variable détruit la précédente valeur -. Sauf que, dans ce cas précis, une redéfinition est plus probablement liée au fait que le programmeur ignore que la fonction existe déjà… Pour se protéger de ce problème, il est possible de « verrouiller » la définition d'une méthode grâce à lockBinding :

 
Sélectionnez
> lockBinding("compterManquantes", .GlobalEnv)
> setGeneric(
+     name="compterManquantes",
+     def = function(object, value){
+         standardGeneric("compterManquantes")
+     }
+ )

Error in assign(name, fdef, where) :
    impossible de changer la
valeur d'un lien verrouillé pour 'compterManquantes'

Il n'est plus possible d'effacer « par erreur » le setGeneric.

Cette méthode présente toutefois un inconvénient majeur, celui de la non réexécutabilité du code. Pendant la phase de développement, on a tendance à exécuter notre code, le modifier et le réexécuter. lockBinding empêche une telle réexécution puisqu'une fonction générique ne peut-être définie qu'une fois (et que la ré-exécution est une seconde définition).

II-B-3-d. Déclaration des génériques

Une autre manière de se protéger contre l'écrasement des génériques est de regrouper la déclaration des génériques dans un fichier unique. En tout état de cause, une fonction générique ne concerne pas un objet particulier puisque, par définition, elle doit s'adapter à tous les objets. Il est donc préférable de déclarer toutes les fonctions génériques ensemble en début de programme, éventuellement classées par ordre alphabétique. Si, par erreur, nous devions décider de déclarer une générique deux fois, il serait alors facile de s'en rendre compte.

II-B-4. Voir les méthodes

Notre classe commence à s'étoffer. Il est temps de faire une petite pause et d'admirer notre travail. showMethods est la méthode de la situation. Il existe plusieurs manières de l'utiliser. L'une d'entre elles permet de voir les noms des méthodes que nous avons définies pour une classe donnée :

 
Sélectionnez
> showMethods( class ="Trajectoires")

Function : initialize(package methods)
.Object ="Trajectoires"
    (inherited from : .Object ="ANY")

Function : plot( package graphics)
x="Trajectoires"

Function : print(package base)
x="Trajectoires"

Function : show(package methods)
object ="Trajectoires"

Maintenant que nous avons listé ce qui existe, nous pouvons nous intéresser d'un peu plus près à une méthode particulière : getMethod permet d'afficher la définition (le contenu du corps de la fonction) d'une méthode pour un objet donné. Si la méthode en question n'existe pas, getMethod renvoie une erreur :

 
Sélectionnez
> getMethod(f ="plot", signature ="Trajectoires")

Method Definition :
function (x, y, ...)
{
    matplot(x@temps, t(x@traj), xaxt = "n", type = "l", ylab = "",
            xlab = "", pch = 1, col = 1, lty = 1)
    axis(1, at = x@temps)
}

Signatures :
          x
target   "Trajectoires"
defined   "Trajectoires"

> getMethod(f ="plot", signature ="Partition")

Error in getMethod(f = "plot", signature = "Trajectoires") :
    No
method found for function "plot" and signature Trajectoires

Plus simplement, existsMethod indique si une méthode est ou n'est pas définie pour une classe :

 
Sélectionnez
> existsMethod(f ="plot", signature ="Trajectoires")

[1] TRUE

> existsMethod(f ="plot", signature ="Partition")

[1] FALSE

Ce n'est pas vraiment nécessaire pour l'utilisateur, ça l'est plus pour le programmeur, qui peut écrire des choses du genre : « Si telle méthode existe pour tel objet, adopte le comportement 1, sinon le comportement 2 ».

II-C. Chapitre 6 - Construction

La construction regroupe tous les outils permettant de fabriquer une instance correcte d'un objet. Entrent dans cette catégorie les méthodes de création proprement dites (méthodes qui stockent les valeurs dans les attributs) et les méthodes de validation (méthodes qui vérifient que les valeurs attribuées sont conformes à ce que le programmeur souhaite).

II-C-1. Vérificateur

Le vérificateur est là pour contrôler qu'il n'y a pas d'incohérence interne dans l'objet. Par exemple, une taille doit être positive. On lui donne des règles et à chaque création d'objet, il vérifie que l'objet suit les règles.

Pour cela, on inclut les paramètres de vérification dans la définition de l'objet lui-même via l'argument validity. Pour l'objet Trajectoires, on peut vouloir vérifier que le nombre de groupes effectivement présents dans cluster est inférieur ou égal au nombre de groupes déclaré dans nbCluster.

 
Sélectionnez
> setClass (
+     Class="Trajectoires",
+     representation(temps = "numeric", traj = "matrix"),
+     val idi ty = function(object){
+         cat (" ~~~ Trajectoires : vérificateur ~~~\n")
+         if (length(object@temps)!= ncol(object@traj)){
+             stop("[ Trajectoire : validation ] Le nombre de mesures
+                  temporelles ne correspond pas au nombre de
+                  colonnes de la matrice ")
+         } else {}
+         return(TRUE)
+     }
+ )

[1] "Trajectoires"

> new(Class="Trajectoires", temps =1:2, traj =matrix(1:2, ncol =2))

   ~~~ Trajectoires : vérificateur ~~~
*** Class Trajectoires, method Show ***
* Temps = [1] 1 2
* Traj (limité à une matrice 10x10) =

[1]  1 2
* ... ...
******* Fin Show(trajectoires) *******

> new(Class="Trajectoires", temps =1:3, traj =matrix(1:2, ncol =2))

~~~ Trajectoires : vérificateur ~~~
Error in validityMethod(object) :
    [Trajectoire:validation] Le
nombre de mesures
              temporelles ne correspond pas au
nombre de
              colonnes de la matrice

Comme vous pouvez le constater, la fonction validity telle que nous venons de la définir ne prend aucune précaution concernant l'objet vide. Mais cela n'a pas d'importance. En effet, new n'appelle pas le vérificateur quand on ne lui donne pas d'argument.

Il est également possible de définir une classe puis, plus tard, de définir sa validité grâce à une fonction appelée setValidity. De la même manière, il est possible de définir la representation et le prototype en externe. Mais cette manière de faire est conceptuellement moins propre. En effet, la conception d'un objet doit être réfléchie et non pas faite d'ajouts à droite et à gauche…

Le vérificateur n'est appelé QUE lors de la création initiale de l'objet. Si ensuite il est modifié, rien ne va plus, il n'y a plus de contrôle. Nous pourrons bientôt corriger cela grâce aux « setteurs ». Pour l'instant, on note juste l'intérêt de proscrire l'utilisation de @ : la modification directe d'un attribut n'est pas soumise à vérification…

 
Sélectionnez
> trajStLouis <- new(
+     Class="Trajectoires", temps =c(1), traj =matrix(1)
+ )

    ~~~ Trajectoires : vérificateur ~~~
> ### Pas de vérification , le nombre de mesures temporelles ne
> ### correspond plus aux trajectoires
> (trajStLouis@temps <- c(1 ,2 ,3))

[1]  1 2 3

II-C-2. L'initiateur

Le vérificateur est une version simplifiée d'un outil plus général appelé l'initiateur. L'initiateur est une méthode permettant de fabriquer un objet. Il est appelé à chaque construction d'un objet, c'est-à-dire à chaque utilisation de la fonction new.

Reprenons nos trajectoires. Il serait assez plaisant que les colonnes de la matrice des trajectoires aient des noms, les noms des temps ou les mesures ont été prises. De même, les lignes pourraient être indicées par un numéro d'individu :

 
Sélectionnez
     T0    T1    T4    T5
I1   15   15.1  15.2  15.2
I2   16   15.9   16   16.4
I3  15.2   NA   15.3  15.3
I4  15.5  15.6  15.8   16

L'initiateur va nous permettre de faire tout ça. L'initiateur est une méthode qui, lors de l'appel de new, fabrique l'objet tel que nous le voulons. Le nom de la méthode est initialize. initialize utilise une fonction (définie par l'utilisateur) qui prend pour argument l'objet en train d'être construit et les différentes valeurs à affecter aux attributs de l'objet. Cette fonction travaille sur une version locale de l'objet. Elle doit se terminer par l'affectation des valeurs aux attributs de l'objet puis par un return(object).

 
Sélectionnez
> setMethod(
+     f ="initialize",
+     signature ="Trajectoires",
+     definition = function(.Object, temps, traj){
+         cat(" ~~~ Trajectoires : initiateur ~~~\n")
+         rownames(traj) <- paste("I" , 1:nrow(traj), sep="")
+         # Affectation des attributs
+         .Object@traj <- traj
+
+         .Object@temps <- temps
+         # return de l'objet
+         return(.Object)
+     }
+ )

[1] "initialize"

> new(
+     Class="Trajectoires",
+     temps =c(1 ,2 ,4 ,8),
+     traj =matrix(1:8, nrow =2)
+ )

   ~~~ Trajectoires : initiateur ~~~
*** Class Trajectoires, method Show ***
* Temps = [1] 1 2 4 8
* Traj (limité à une matrice 10x10) =
    [,1] [,2] [,3] [,4]
I1  1    3    5    7
I2  2    4    6    8
* ... ...
******* Fin Show(trajectoires) *******

La définition d'un initiateur désactive le vérificateur. Dans notre cas, temps peut à nouveau comporter plus ou moins de valeurs que de colonnes dans traj.

 
Sélectionnez
> new(
+     Class="Trajectoires",
+     temps =c(1, 2, 48),
+     traj =matrix(1:8, nrow =2)
+ )

   ~~~ Trajectoires : initiateur ~~~
*** Class Trajectoires, method Show ***
* Temps = [1] 1 2 48
* Traj (limité à une matrice 10x10) =
    [,1] [,2] [,3] [,4]
I1  1    3    5    7
I2  2    4    6    8
* ... ...
******* Fin Show(trajectoires) *******

Pour utiliser un initiateur et un vérificateur dans le même objet, il faut donc appeler « manuellement » le vérificateur grâce à l'instruction validObject. Notre initiateur incluant le vérificateur devient :

 
Sélectionnez
> setMethod(
+     f ="initialize",
+     signature ="Trajectoires",
+     definition = function(.Object, temps, traj){
+         cat(" ~~~~~ Trajectoires : initiateur ~~~~~\n")
+         if (!missing(traj)){
+             colnames(traj) <- paste("T", temps, sep="")
+             rownames(traj) <- paste("I", 1:nrow(traj), sep="")
+             .Object@temps <- temps
+             .Object@traj <- traj
+             validObject(.Object)
+         }
+         return(.Object)
+     }
+ )

[1] "initialize"

> new(
+     Class="Trajectoires",
+     temps =c(1, 2, 4, 8),
+     traj =matrix(1:8, nrow =2)
+ )

  ~~~~~ Trajectoires : initiateur ~~~~~
    ~~~ Trajectoires : vérificateur ~~~
*** Class Trajectoires, method Show ***
* Temps = [1] 1 2 4 8
* Traj (limité à une matrice 10x10) =
    T1 T2 T4 T8
I1  1  3  5  7
I2  2  4  6  8
* ... ...
******* Fin Show(trajectoires) *******

> new(
+     Class="Trajectoires",
+     temps =c(1, 2, 48),
+     traj =matrix(1:8, nrow =2)
+ )

   ~~~~~ Trajectoires : initiateur ~~~~~
Error in dimnames (x) <- dn :
   la longueur de 'dimnames' [2] n'est
pas égale à l'étendue du tableau

Cette nouvelle définition a supprimé l'ancienne. Vous aurez noté la condition portant sur missing(traj) pour prendre en compte l'objet vide.

Un constructeur ne prend pas nécessairement pour argument les attributs de l'objet.

Par exemple, si on sait (ça n'est pas le cas dans la réalité, mais imaginons) que l'IMC augmente de 0.1 toutes les semaines, on pourrait construire des trajectoires en fournissant le nombre de semaines et les IMC initiaux :

 
Sélectionnez
> setClass (
+     Class="TrajectoiresBis",
+     representation(
+         temps = "numeric",
+         traj = "matrix"
+     )
+ )

[1] "TrajectoiresBis"

> setMethod("initialize",
+     "TrajectoiresBis",
+     function(.Object, nbSemaine, IMCinit){
+         calculTraj <- function(init, nbSem){
+             return(init + 0.1 * nbSem)
+         }
+         traj <- outer(IMCinit, 1:nbSemaine, calculTraj)
+         colnames(traj) <- paste("T", 1:nbSemaine, sep="")
+         rownames(traj) <- paste("I", 1:nrow(traj), sep="")
+         .Object@temps <- 1:nbSemaine
+         .Object@traj <- traj
+         return(.Object)
+     }
+ )

[1] "initialize"

> new(Class="TrajectoiresBis", nbSemaine =4, IMCinit =c (16 ,17 ,15.6))

An object of class "TrajectoiresBis"
Slot "temps":
[1]  1 2 3 4

Slot "traj":
     T1    T2    T3    T4
I1  16.1  16.2  16.3  16.4
I2  17.1  17.2  17.3  17.4
I3  15.7  15.8  15.9  16.0

Le ne peut y avoir qu'un seul initiateur par classe. Il faut donc qu'il soit le plus général possible. La définition ci-dessus interdirait la construction d'une trajectoire à partir d'une matrice. Elle est donc fortement déconseillée car trop spécifique. Finalement, il vaut mieux laisser ce genre de transformation aux constructeurs grand public.

II-C-3. Constructeur grand public

Comme nous l'avions dit en introduction, le (gentil) programmeur, ayant conscience du fait que new n'est pas une fonction sympathique, ajoute des constructeurs « grand public ». Cela se fait grâce à une fonction (fonction classique, pas nécessairement une méthode S4) portant généralement le nom de la classe. Dans notre cas, ça sera la fonction trajectoires.

 
Sélectionnez
> trajectoires <- function(temps, traj){
+     cat("~~~~~ Trajectoires : constructeur ~~~~~\n")
+     new(Class="Trajectoires", temps =temps, traj = traj)
+ }
> trajectoires(temps =c(1 ,2 ,4) , traj =matrix(1:6, ncol =3))

 ~~~~~ Trajectoires : constructeur ~~~~~
   ~~~~~ Trajectoires : initiateur ~~~~~
     ~~~ Trajectoires : vérificateur ~~~
*** Class Trajectoires, method Show ***
* Temps = [1] 1 2 4
* Traj (limité à une matrice 10x10) =
    T1 T2 T4
I1  1  3  5
I2  2  4  6
* ... ...
******* Fin Show(trajectoires) *******

L'intérêt est de pouvoir faire un traitement plus sophistiqué. Par exemple, dans un grand nombre de cas, Tam mesure les trajectoires toutes les semaines et elle les stocke dans une matrice. Elle souhaite donc avoir le choix : soit définir l'objet trajectoires simplement en donnant une matrice, soit en donnant une matrice et les temps :

 
Sélectionnez
> trajectoires <- function(temps, traj){
+     if (missing(temps)){ temps <- 1:ncol(traj)}
+     new(Class="Trajectoires", temps =temps, traj = traj)
+ }
> trajectoires(traj =matrix(1:8 , ncol =4))

  ~~~~~ Trajectoires : initiateur ~~~~~
    ~~~ Trajectoires : vérificateur ~~~
*** Class Trajectoires, method Show ***
* Temps = [1] 1 2 3 4
* Traj (limité à une matrice 10x10) =
    T1 T2 T3 T4
I1  1  3  5  7
I2  2  4  6  8
* ... ...
******* Fin Show(trajectoires) *******

R accepte parfois que deux entités différentes portent le même nom. Dans le cas présent, il est possible de définir une fonction portant le même nom qu'une classe. Un inconvénient à cela est que l'on ne sait plus ensuite de quoi on parle. Nous vous conseillons plutôt de donner à la classe un nom avec une majuscule et à la fonction constructeur le même nom mais avec une minuscule.

Contrairement à l'initiateur, on peut définir plusieurs constructeurs. Toujours sous l'hypothèse que l'IMC augmente de 0.1 toutes les semaines, on peut définir trajectoiresRegulieres :

 
Sélectionnez
> trajectoiresRegulieres <- function(nbSemaine, IMCinit){
+     funcInit <- function(init, nbSem){ return(init + 0.1 * nbSem)}
+     traj <- outer(IMCinit, 1:nbSemaine, funcInit)
+     temps <- 1:nbSemaine
+     return(new(Class="Trajectoires", temps =temps, traj = traj))
+ }
> trajectoiresRegulieres(nbSemaine =3, IMCinit =c(14, 15, 16))

  ~~~~~ Trajectoires : initiateur ~~~~~
    ~~~ Trajectoires : vérificateur ~~~
*** Class Trajectoires, method Show ***
* Temps = [1] 1 2 3
* Traj (limité à une matrice 10x10) =
     T1    T2    T3
I1  14.1  14.2  14.3
I2  15.1  15.2  15.3
I3  16.1  16.2  16.3
* ... ...
******* Fin Show(trajectoires) *******

Ainsi, les deux constructeurs font appel à l'initiateur. D'où l'importance d'un initiateur généraliste.

II-C-4. Petit bilan

Lors de la construction d'un objet, il y a donc trois endroits ou il est possible d'effectuer des opérations : dans la fonction de construction, dans l'initiateur et dans le vérificateur. Le vérificateur ne peut faire que vérifier, il ne permet pas de modifier l'objet.

Par contre, on peut l'appeler sur un objet déjà construit. Pour ne pas trop se mélanger, il est donc préférable de spécialiser chacun de ces opérateurs et de lui réserver certaines tâches précises :

  • la fonction de construction est celle qui sera appelée par l'utilisateur. Elle est la plus générale et peut prendre des arguments variables, éventuellement des arguments qui ne sont pas des attributs de l'objet. Elle transforme ensuite ses arguments en des attributs. Nous conseillons donc de lui confier la transformation de ses arguments en futurs attributs (comme trajectoiresRegulieres a préparé des arguments pour new("Trajectoires")).
    La fonction de construction se termine toujours par new ;
  • l'initiateur est appelé par new. Il est chargé de donner à chaque attribut sa valeur, après modification éventuelle. On peut le charger des tâches qui doivent être effectuées pour tous les objets, quels que soit les constructeurs qui les appellent (comme le renommage des lignes et des colonnes de Trajectoires).
    Si l'initiateur n'a pas été défini, R appelle un initiateur par défaut qui se contente d'affecter les valeurs aux attributs, puis d'appeler le validateur.
    Finalement, l'initiateur doit appeler le vérificateur (l'initiateur par défaut appelle le vérificateur, l'initiateur défini par l'utilisateur doit faire un appel explicite) ;
  • le vérificateur contrôle la cohérence interne de l'objet. Il peut, par exemple, interdire certaines valeurs à certains attributs, vérifier que la taille des attributs est conforme à ce qui est attendu… Il ne peut pas modifier les valeurs des attributs, il doit se contenter de vérifier qu'ils suivent des règles.

Ce découpage des tâches a pour avantage de bien séparer les choses. Par contre, il n'est pas le meilleur en terme d'efficacité:

« En effet, précise un relecteur, l'initiateur par défaut de R est bien plus efficace que les initiateurs écrits par les programmeurs. Il est donc intéressant de l'utiliser. Pour cela, il suffit de ne pas le définir. Dans cette optique :

  • le constructeur fait tout le travail préparatoire ;
  • l'initiateur n'est pas défini pas l'utilisateur. C'est donc l'initiateur par défaut qui est appelé ;
  • le vérificateur contrôle la cohérence interne de l'objet. »

Cette manière de faire, un tout petit peu moins claire pour le débutant, est plus efficace par la suite.

Pour le néophyte, savoir quelle méthode est appelée et quand, est un vrai casse-tête. new utilise l'initiateur s'il existe, le vérificateur sinon (mais pas les deux, sauf appel explicite, ce que nous avons fait). D'autres instructions, comme as (section III.B.8is, as et as<-) font appel aux initiateurs et vérificateurs. Dans le doute, pour bien comprendre qui est appelé et quand, nous avons ajouté une petite ligne en tête de fonction qui affiche le nom de la méthode utilisée. « Top moche » ont commenté certains relecteurs. Hélas, ils ont raison. Mais la pédagogie prime ici sur l'esthétique…. D'ailleurs, quand nous en serons à l'héritage, les choses deviendront un peu plus compliquées et cet affichage « top moche » sera un peu plus nécessaire….

II-D. Chapitre 7 - Accesseur

Nous en avons déjà parlé, utiliser @ en dehors d'une méthode est fortement déconseillé… Pourtant, il est nécessaire de pouvoir récupérer les valeurs des attributs. C'est le rôle des accesseurs. Dans la langue de Molière, on les appelle les sélecteurs et les affectants. En français, option informatique, on parle plutôt de getteurs et setteurs, francisation de get et set.

II-D-1. Les getteurs

Un getteur est une méthode qui renvoie la valeur d'un attribut. En programmation classique, un grand nombre de fonctions prennent une variable et retournent une partie de ces arguments. Par exemple, names appliqué à un data.frame retourne les noms des colonnes ; nrow appliqué au même data.frame donne le nombre de lignes. Et ainsi de suite.

Pour nos trajectoires, nous pouvons définir plusieurs getteurs : bien sûr, il nous en faut un qui renvoie temps et un qui renvoie traj. Nos getteurs étant des méthodes nouvelles pour R, il faut les déclarer grâce à setGeneric puis les définir simplement grâce à un setMethod :

 
Sélectionnez
> ### Getteur pour 'temps'
> setGeneric("getTemps",
+     function(object){standardGeneric("getTemps")}
+ )

[1] "getTemps"

> setMethod("getTemps", "Trajectoires",
+     function(object){
+         return(object@temps)
+     }
+ )

[1]  "getTemps"

> getTemps(trajCochin)

[1]  1 2 4 5

> ### Getteur pour 'traj'
> setGeneric("getTraj",
+     function(object){standardGeneric("getTraj")}
+ )

[1]  "getTraj"

> setMethod("getTraj", "Trajectoires",
+     function(object){
+         return(object@traj)
+     }
+ )

[1]  "getTraj"

> getTraj(trajCochin)

      [,1]  [,2]  [,3]  [,4]
[1,]  15.0  15.1  15.2  15.2
[2,]  16.0  15.9  16.0  16.4
[3,]  15.2   NA   15.3  15.3
[4,]  15.7  15.6  15.8  16.0

Mais on peut aussi faire des getteurs plus élaborés. Par exemple, on peut avoir régulièrement besoin de l'IMC au temps d'inclusion :

 
Sélectionnez
> ### Getteur pour les IMC à l'inclusion
> ### (première colonne de 'traj')
> setGeneric("getTrajInclusion",
+     function(object){standardGeneric("getTrajInclusion")}
+ )

[1] "getTrajInclusion"

> setMethod("getTrajInclusion", "Trajectoires",
+     function(object){
+         return(object@traj[ ,1])
+     }
+ )

[1] "getTrajInclusion"

> getTrajInclusion(trajCochin)

[1]  15.0 16.0 15.2 15.7

II-D-2. Les setteurs

Un setteur est une méthode qui affecte une valeur à un attribut. Sous R, l'affectation est faite par <-. Sans entrer dans les méandres du programme, l'opérateur <- fait en réalité appel à une méthode spécifique. Par exemple, quand on utilise names(data) <- "A", R fait appel à la fonction names<-. Cette fonction duplique l'objet data, modifie l'attribut names de ce nouvel objet puis remplace data par ce nouvel objet. Nous allons faire pareil pour les attributs de nos trajectoires. setTemps<- permettra de modifier l'attribut temps. Pour cela, on utilise la fonction setReplaceMethod

 
Sélectionnez
> setGeneric("setTemps <-",
+     function(object, value){ standardGeneric("setTemps <-") }
+ )

[1]  "setTemps <-"

> setReplaceMethod(
+     f ="setTemps",
+     signature ="Trajectoires",
+     definition = function(object, value){
+         object@temps <- value
+         return(object)
+     }
+ )

[1]  "setTemps <-"

> (setTemps(trajCochin) <- 1:3)

[1]  1 2 3

Tout l'intérêt du setteur est de pouvoir faire des contrôles. Comme dans initialize, nous pouvons appeler explicitement le vérificateur :

 
Sélectionnez
> setReplaceMethod(
+     f ="setTemps",
+     signature ="Trajectoires",
+     definition = function(object, value){
+         object@temps <- value
+         validObject(object)
+         return(object)
+     }
+ )

[1] "setTemps <-"

> setTemps(trajCochin) <- c(1 ,2 ,4 ,6)

   ~~~ Trajectoires : vérificateur ~~~

> setTemps(trajCochin) <- 1:4

   ~~~ Trajectoires : vérificateur ~~~
Error in validityMethod(object) :
    [Trajectoire : validation] Le
nombre de mesures
               temporelles ne correspond pas au
nombre de
               colonnes de la matrice

II-D-3. Les opérateurs [ et [<-

Il est également possible de définir les getteurs grâce à l'opérateur [ et les setteurs grâce à [<-. Cela se fait comme pour une méthode quelconque en précisant la classe et la fonction à appliquer. Cette fonction prend quatre arguments :

  • x est l'objet ;
  • i désigne l'attribut auquel nous voulons accéder ;
  • si l'attribut désigné par i est complexe (une matrice, une liste…), j permet d'accéder à un élément particulier ;
  • enfin, drop est un booléen permettant de préciser si ce qui est retourné doit garder sa structure initiale ou non (par exemple si une matrice d'une seule ligne doit être considérée comme un vecteur ou comme une matrice).

Dans notre exemple, [ peut simplement retourner l'attribut correspondant à i.

 
Sélectionnez
> setMethod(
+     f ="[",
+     signature ="Trajectoires",
+     definition = function(x, i, j, drop){
+         switch(EXPR =i,
+             "temps"={ return(x@temps)},
+             "traj"={ return(x@traj)}
+         )
+     }
+ )

[1]  "["

Ensuite, l'appel de la fonction [ se fait sous la forme x[i="Attribut1", j=3, drop=FALSE], ou sous la forme simplifiée x["Attribut1",3,FALSE]. Dans notre cas, j et drop n'étant pas utilisés, on peut simplement les omettre :

 
Sélectionnez
> trajCochin[i="temps"]

[1]  1 2 4 6

> trajCochin["traj"]

      [,1]  [,2]  [,3]  [,4]
[1,]  15.0  15.1  15.2  15.2
[2,]  16.0  15.9  16.0  16.4
[3,]  15.2   NA   15.3  15.3
[4,]  15.7  15.6  15.8  16.0

La définition que nous venons d'écrire n'offre aucune protection contre les erreurs typographiques :

 
Sélectionnez
> trajCochin["trak"]

NULL

Il est donc important de finir l'énumération par le comportement à adopter quand l'attribut n'existe pas :

 
Sélectionnez
> setMethod(
+     f ="[",
+     signature ="Trajectoires",
+     definition = function(x, i, j, drop){
+         switch(EXPR =i,
+             "temps"= { return(x@temps) },
+             "traj" = { return(x@traj) },
+             stop("Cet attribut n'existe pas !")
+         )
+
+     }
+ )

[1]  "["

Nous sommes maintenant à l'abri d'une erreur de typographie.

drop est un argument à utiliser avec précaution. En effet, il a pour vocation de modifier le type de l'objet. Par exemple, si M est une matrice, quel est le type de M[,b] ? Cela dépend de b. Si b est un vecteur, alors M[,b] est une matrice. Si b est un entier, alors M[,b] est un vecteur.… Comme toujours, un comportement qui dépend est à proscrire.

Les setteurs se définissent selon le même principe grâce à l'opérateur [<-. On utilise setReplaceMethod. Là encore, une fonction décrit le comportement à adopter et là encore, il est important de veiller à contrôler une éventuelle faute de frappe :

 
Sélectionnez
> setReplaceMethod(
+     f ="[",
+     signature ="Trajectoires",
+     definition = function(x, i, j, value){
+         switch(EXPR =i,
+             "temps"= { x@temps <-value },
+             "traj" = { x@traj <-value },
+             stop("Cet attribut n'existe pas !")
+         )
+         validObject(x)
+         return(x)
+     }
+ )

[1]  "[ <-"

> trajCochin["temps"] <- 2:5

  ~~~ Trajectoires : vérificateur ~~~

Dans nos définitions de [ et [<-, nous avons listé les différents attributs possibles pour i (i=="temps" et i=="traj"). Il serait également possible de les numéroter (i==1 et i==2). L'accès au premier attribut se ferait alors via trajCochin[1] à la place de trajCochin["temps"]. C'est bien évidemment totalement impropre : on se mettrait à la merci de l'erreur typographique trajCochin[2] à la place de trajCochin[1] (alors que trajCochin["temps"] ne présente pas de danger puisqu'il provoque une erreur).

II-D-4. [, @ ou get ?

Quand doit-on utiliser get, quand doit-on utiliser @, quand doit-on utiliser [ ? @ est à réserver exclusivement aux méthodes internes à la classe : si une méthode de partition Partition a besoin de traj (c'est-à-dire d'un attribut d'une autre classe), il lui est formellement interdit d'utiliser @ pour accéder aux attributs directement, elle doit passer par [ ou par get.

À l'intérieur d'une classe (si une méthode de Partition a besoin de nbCluster), il y a deux écoles : ceux qui utilisent @ et ceux qui utilisent [ (ou get).

Entre get et [, il n'y a pas vraiment de différence, c'est simplement un jeu d'écriture : getTemps(trajCochin) ou trajCochin["temps"] ? La première notation rappelle les autres langages objet, la seconde est plus spécifique à R. Dans le cas d'un package et donc de méthodes qui ont pour vocation à être utilisées par d'autres, [ sera plus conforme à la syntaxe « classique » de R, donc plus intuitif.


précédentsommairesuivant

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2014 Christophe Genolini. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.