Projet IDAPA

Accueil > Ressources > Perl 5 > Manipuler des matrices creuses avec Perl

Manipuler des matrices creuses avec Perl

dimanche 6 décembre 2009, par Eric Sanjuan

Une matrice creuse est un tableau de données où les valeurs nulles sont très majoritaires. Il est plus aisé de manipuler ces matrices sous forme relationnelle ou arborescente que sous forme d’un tableau. Nous montrons comment implémenter l’approche arborescente avec perl 5. Pour disposer de scripts susceptibles d’évoluer facilement nous nous servons des fonctionnalités orienté objet.

Une matrice creuse est un tableau de données où les valeurs non nulles sont minoritaires. L’exemple même de telles matrices sont les tableaux manipulées en analyse de données textuelles contenant autant de colonnes que de mots et autant de lignes que de documents, les cellules donnant le nombre de fois que le mot sur la colonne a été trouvé dans le document associé à la ligne. Un autre exemple sont les tableaux disjonctifs issus d’un questionnaire type QCM. Ces tableaux contiennent autant de colonnes que d’items, et autant de lignes que de personnes interrogées.

Pré-requis

Nous allons utiliser la langage et interpréteur perl pour manipuler ces objets. Perl est installé par défaut dans presque toutes les distributions UNIX/LINUX et peut être installé sur windows avec Cygwin ou ActivePerl.

Pour tester les instructions perl qui suivent, il suffit de les recopier dans un fichier texte en utilisant un éditeur de textes tel que gedit ou mieux Geany sous LINUX, notepad ou notepad++ sous windows. On peut donner le nom et l’extension que l’on veut à ce fichier, par exemple test.txt. On le lance ensuite en tapant la commande :

perl test.txt

dans un terminal.

perl est donc un langage interprété et non compilé, il est donc plus lent que le C++ mais pas plus que le Java. Sa principale qualité est aussi son principal défaut. On peut tout écrire très vite (trop) en perl, des traitements de chaînes de caractères à l’utilisation de structures de données complexes. Comme par ailleurs il existe toujours une autre manière de faire, il existe autant de styles perl que de programmeurs perl.

Représentation matricielle et relationnelle d’une matrice

Prenons un petit exemple, considérons la matrice suivante :


\left[
\begin{array}{ccccc}
0&1.2&1.3&0&0\\
2.1&0&0&2.4&2.5\\
3.1&0&3.3&0&0\\
0&4.2&0&0&0
\end{array}
\right]

Si on voit cette matrice comme une relation ternaire entre un ensemble de lignes, un ensemble de colonnes et un ensemble de valeurs, la même matrice devient :


\begin{array}{|c|c|c|}
\hline
L&C&V\\
\hline
1&2&1.2\\
1&3&1.3\\
2&1&2.1\\
2&4&2.4\\
2&5&2.5\\
3&1&3.1\\
3&3&3.3\\
4&4&4.2\\
\hline
\end{array}

Seules les valeurs non nulles sont représentées, mais elles le sont avec leur coordonnées dans la matrice. A noter que du point de vue relationnel l’ordre des lignes ou des colonne n’a pas d’importance.

A noter que ce format relationnel correspond bien aux structures "data.frame" utilisées dans le logiciel statistique R.

Représentation arborescente d’une matrice

La représentation arborescente est plus dense et nécessite d’utiliser des pointeurs sur des tableaux associatifs. Voici un exemple de tableau associatif simple en Perl 5 :

$X={2=>"pair", 5=>"impair"}

$X est une valeur scalaire qui contient l’adresse mémoire où la structure a été créée.
Pour accéder aux valeurs de ce tableau on utilise les instructions de type :

$X->{2};

Lorsque on utilise ces instructions avec une affectation "=", soit on modifie une valeur existante, soit on crée une nouvelle entrée. Ainsi après l’instruction :

$X->{0}="pair"

le tableau associatif devient :

$X={ 0=>"pair", 2=>"pair", 5=>"impair"}

et si on écrit à la suite :

$X->{0}="impair"

alors le tableau est modifié en :

$X={ 0=>"impair", 2=>"pair", 5=>"impair"}

Pour une représentation arborescente de la matrice précédente on doit utiliser des tableaux associatifs bidimensionnels. La représentation qui en résulte est plus dense que la relationnelle puisque on ne doit pas répéter les numéros de lignes. En langage PERL cette matrice s’écrit alors :

$M={
1 => {2=>1.2, 3=>1.3},
2 => {1=>2.1, 4=>2.4, 5=>2.5},
3 => {1=>3.1, 3=>3.3},
4 => {2=>4.2}
};

Ce code demande à perl de créer un arborescence à deux niveaux. Le premier niveau contient quatre branches, et chaque branche porte respectivement 2, 3, 2 et 1 feuilles. D’un point de vue ensembliste nous avons un ensemble d’ensembles de couples. En particulier, l’ordre à l’intérieur des ensembles n’a pas d’importance. Ainsi le code précédent est équivalent à celui-ci :

$M={
3 => {1=>3.1, 3=>3.3},
1 => {3=>1.3, 2=>1.2},
4 => {2=>4.2},
2 => {4=>2.4, 1=>2.1,  5=>2.5}
};

$M contient alors l’adresse mémoire où cette arborescence a été créée.
Il est aussi possible de remplacer les entiers à gauche des flèches par des chaînes de caractères contenant le nom des lignes et des colonnes plutôt que les indices. Le code précédent pourrait alors devenir :

$T={
"L3" => {"C1"=>3.1, "C3"=>3.3},
"L1" => {"C3"=>1.3, "C2"=>1.2},
"L4" => {"C2"=>4.2},
"L2" => {"C4"=>2.4, "C1"=>2.1,  "C5"=>2.5}
};

Plus généralement on parle de clefs pour désigner les identifiants que l’on utilise à gauche des "=>" pour désigner les branches et sous-branches. Concrètement à gauche de la flèche on trouve toujours un identifiant et à droite on trouve soit une valeur, soit un autre ensemble. On peut alors considérer l’identifiant comme le nom donné à l’ensemble. D’un point de vue informatique, quand on trouve un ensemble à droite de la flèche, celui-ci est représenté par son adresse mémoire, on parle alors de pointeur.

Par ailleurs, Perl étant très faiblement typé, on peut manipuler toute valeur numérique ou chaîne de caractères comme un simple scalaire. Donc dans notre exemple on pourrait remplacer les valeurs numériques par des valeurs texte. Par exemple, on veut stocker ces valeurs réelles en utilisant la virgule au lieu du point comme séparateur décimal. Le code devient alors :

$V={
"L3" => {"C1"=>"3,1", "C3"=>"3,3"},
"L1" => {"C3"=>"1,3", "C2"=>"1,2"},
"L4" => {"C2"=>"4,2"},
"L2" => {"C4"=>"2,4", "C1"=>"2,1",  "C5"=>"2,5"}
};

Dans ce cas Perl n’interprète pas les valeurs comme numériques et la virgule est préservée. Par contre on ne pourra pas appliquer directement des opérations numériques sur ces scalaires.

Accès aux valeurs de l’arborescence

Pour accéder à une valeur particulière dans l’arborescence on utilise la syntaxe :

$X->{branche}{feuille}

par exemple pour afficher la valeur sur le deuxième ligne, cinquième colonne il faut écrire respectivement pour chaque représentation précédente :

print $M->{2}{5}."\n" ;
print $T->{"L2"}{"C5"}."\n" ;
print $V->{"L2"}{"C5"}."\n";

La fonction print affiche tout contenu scalaire. Le caractère "\n" ajoute un retour à la ligne en fin d’affichage et le point qui le précède est l’opération de concaténation.

A noter que si la valeur en ligne 2, colonne 5 n’existe pas, la chaîne vide est renvoyée. Le fonction "exists" permets de tester si la valeur existe ou pas. Par exemple, le code suivant affichera "oui" puisque il existe bien une première ligne, c’est à dire un sous-ensemble avec l’identifiant 1, puis "non" puisqu’il n’y a pas de valeur en cinquième colonne sur cette ligne.

if(exists $M->{1}){
 print "oui\n";
}
else {
 print "non\n";
}
if(exists $M->{1}{5}){
 print "oui\n";
}
else {
 print "non\n";
}

Notez la syntaxe classique du si ... alors avec une particularité propre à perl, il faut que le "if" et le "else " soient obligatoirement suivis par un bloc d’instructions entre accolades, même s’il n’y a qu’une seule instruction !

Maintenant si on affiche le contenu à la clef 2 de $M en utilisant le code :

print $M->{2}."\n" ;

on obtient une adresse mémoire en hexadécimal qui ressemble à ceci "HASH(0x817b2e8)". Le mot HASH indique que le contenu à cette adresse est une table de hachage (hash table) qui permet d’associer des valeurs à des clefs (comme une application mathématique). On parle plutôt de tableau associatif.

Charger une arborescence à partir d’un fichier au format relationnel

Voici le code d’une fonction qui prend en argument le nom d’un fichier supposé contenir une matrice au format relationnel, c’est à dire un fichier texte tabulé en trois colonnes. On suppose que l’on utilise la tabulation "\t" comme séparateur de colonnes et "\n" pour séparer les lignes.

Quelques éléments simples de syntaxe avant de donner le code.

- Dans un programme perl on utilise le "#" pour noter des commentaires.
Comme pour les script shell, tout cet qui est écrit sur une ligne à la suite d’un "#" est en fait ignoré par l’interpréteur.
- La fonction open permet d’ouvrir un fichier en lecture. Le nom du fichier est donnée en deuxième paramètre. Le premier paramètre est la variable utilisée pour pointer sur le ligne courante du fichier.
- L’opérateur "<$F>" renvoie la ligne sur laquelle pointe $F ($F ayant été initialisé avec open) et déplace le pointeur sur la ligne suivante.
- La fonction chomp permet de supprimer les caractères cachés de fin de ligne.
- La fonction split permet d’éclater une chaîne de caractères en une liste suivant le séparateur donnée en premier argument. En perl les listes sont données entre parenthèses. On utilise le caractère @ pour les déclarer : @L=("V1","V2","V3"). On accède aux valeurs en donnant l’indice de la valeur entre crochets : $L[2] renvoie "V3". Comme en C, la première valeur est indexée par 0. L’utilisation du $ signifie que l’on renvoie un scalaire (valeur simple de type numérique ou chaîne de caractères).
- On définie ses propres fonctions perl avec l’opérateur sub suivi d’un nom de fonction, puis d’un bloc d’instructions. En fait l’opérateur sub permet simplement de nommer un sous bloc d’instructions.
- Le tableau "_" contient les arguments transmis à la fonction. On accède aux éléments de ce tableau avec "$_[0], $_[1], ..."
- Réciproquement, fonction return permet de renvoyer une valeur à l’extérieur de la fonction et provoque l’arrêt de celle-ci : toutes les instructions suivantes sont ignorées.
- Un peu plus complexe,
le terme my permet de limiter la portée d’une variable au bloc d’instructions dans lequel il apparaît, tout en cachant les variables de même nom apparaissant dans des blocs englobants. Par exemple, le code suivant affiche 1, 2, 1 et non pas 1, 2, 2.

$X=1; #initialisation de la variable $X
print $X."\n";
{
 my $X=2; #autre variable de même nom
 print $X."\n";
}
print $X."\n";

Attention, toute structure de données uniquement référencée par des variables et des pointeurs locaux à un bloc (par exemple une fonction) est perdue à la fermeture de ce bloc.
Voici donc la fonction qui permet de générer une telle arborescence à partir d’un fichier tabulé sur trois colonnes :

sub read_rel{
        my $file=$_[0];
        my $h={};
        open(my $F,$file) or die;
        while (my $r = <$F>){
                chomp $r;
                my @r=split("\t",$r);
                if(@r==3) {
                        $h->{$r[0]}{$r[1]}=$r[2];
                }       
        }
        return $h;
}

Remarquer que quand on utilise une liste dans un contexte scalaire comme ici if(@r==3) , alors @r est remplacé par la taille de la liste.

Pour appeler cette fonction dans le corps du programme, en supposant que le fichier de données s’appelle data.csv, il suffit d’écrire à la suite de la déclaration de la fonction :

$M=read_rel("data.csv");

Parcourir l’arborescence

Pour cela il est nécessaire de récupérer les clefs sur chaque niveau. Si $M est un pointeur sur un tableau associatif, alors la fonction keys(%$M) renvoie la liste des clefs de cette arborescence. Le symbole % désigne en perl les tableaux associatifs, de même que le @ désigne les listes et le $ un simple scalaire.

Les clefs d’un tableau associatifs n’étant pas ordonnées il faut appliquer la fonction sort si on veut un ordre lexicographique (comme dans un dictionnaire) croissant. Si l’on veut un ordre numérique croissant il faut ajouter une instruction $a<=>$b à sort. Ainsi les instruction suivantes affichent "0 123 21" puis "0 21 123" :

@L=(21, 123, 0);
print join(" ",sort(@L))."\n";
print join(" ",sort{$a<=>$b}(@L))."\n";

Voici alors le code de la fonction qui permets de parcourir l’arborescence, et qui génère un fichier tabulé à trois colonnes. C’est la réciproque de la fonction permettant de charger les données. Pour ouvrir un fichier en écriture on accole le symbole ">" au nom de fichier comme en langage shell. Pour écrire dans ce fichier on mentionne la nom de son pointeur à la suite le print.

sub print_rel{
        my $h=$_[0];
        my $file=$_[1];
        open(my $O,">".$file) or die;
        for my $i (sort(keys(%$h))){
                my $p=$h->{$i};
                for my $j (sort(keys(%$p))){
                        print $O(join("\t",$i,$j,$h->{$i}{$j})."\n");
                }       
        }
}

Dans ce code la variable $p va successivement contenir les adresses de chaque sous-arborescence. Ainsi dans cette fonction on peut remplacer :

$h->{$i}{$j}

par

$p->{$j}

Pour appeler cette fonction il suffit d’écrire print_rel($M,"output.csv") en supposant que $M est un pointeur sur l’arborescence et que le fichier dans lequel on veut écrire s’appelle output.csv. Attention, si ce fichier existe, il sera écrasé par le nouveau contenu.

On peut modifier le code de la fonction précédente pour obtenir une fonction qui crée la copie physique d’une arborescence et renvoie un pointeur vers les nouvelles données. Cela devient :

sub clone{
        my $h=$_[0];
       my $g={};
        for my $i (keys(%$h)){
                my $p=$h->{$i};
                for my $j (keys(%$p)){
                        g->{$i}{$j}=$h->{$i}{$j};
                }       
        }
      return $g;
}

L’appel à cette fonction s’écrit $T=clone($M) en supposant que $T est le pointeur sur la copie de la nouvelle structure.

Si au lieu de la copie exacte, on veut récupérer la transposée de la fonction, il suffit de remplacer la ligne :

        g->{$i}{$j}=$h->{$i}{$j};

par

        g->{$j}{$i}=$h->{$i}{$j};

Si on reprend l’exemple de matrice précédent, cette nouvelle arborescence comprend 5 sous arborescences au lieu de 4.

Enfin, voici le code d’une fonction qui renvoie le carré de la matrice. Attention, cette fonction ne vérifie pas que que la matrice de départ est carrée. Elle revient à ajouter autant de valeurs nulles que nécessaire pour obtenir une matrice carrée. L’instruction += permets d’ajouter une nouvelle valeur à la valeur précédente si elle existe. Si elle n’existe pas, une nouvelle clef est ajoutée avec une valeur initialisée à 0.

sub square_rel{
        my $h=$_[0];
        my $s={};
        for my $k (keys(%$h)){
                my $p=$h->{$k};
                for my $i (keys(%$p)){
                        for my $j (keys(%$p)){
                                $s->{$i}{$j}+=$h->{$i}{$k}*$h->{$k}{$j};
                        }
                }
        }
        return $s;
}

Enfin voici un appel qui combine ces trois fonctions :

print_rel(square_rel(read_rel($ARGV[0])),$ARGV[1]);

La liste @ARGV contient les arguments transmis au programme perl. Ainsi ici on suppose que le programme a été appelé avec la commande :

perl test.txt data.csv output.csv

Créer sa bibliothèque orientée objet

Rapidement lorsque on commence à accumuler les fonctions, il devient nécessaire de les organiser en bibliothèques. En perl il s’agit de simples fichiers avec l’extension ".pm", dont la première ligne doit être :

package nom_biblio;

où "nom_biblio" doit correspondre au nom du fichier contenant la bibliothèque sans l’extension ".pm".
Le fichier doit aussi obligatoirement se terminer par "1 ;" ou tout test revoyant une valeur strictement positive.
Pour appeler une bibliothèque dans une programme perl il suffit d’ajouter l’instruction :

use nom_biblio;

en tête du programme.

Les mécanismes orienté objet permettent de lier des pointeurs au nom de la bibliothèque. Ainsi un pointeur permet non seulement d’accéder aux données mais aussi aux fonctions qui peuvent s’appliquer à ces données.
On appelle alors ces bibliothèques des classes d’objets dont les fonctions constituent des méthodes et chaque pointeur lié à la bibliothèque devient une instance de cette classe. Voici les éléments de base pour créer une classe en perl 5.
- Pour lier un pointeur à un nom de bibliothèque on utilise la fonction bless. Les fonctions qui utilisent bless, et donc initialisent une nouvelle instance d’objet sont appelées constructeurs.
- Pour appeler une fonction "fct" à partir d’un pointeur "$p" lié à la bibliothèque on écrit simplement $p->fct ce qui étend la notation pour accéder aux valeurs d’un tableau associatif. Attention dans ce cas la valeur de $p est transmise en premier paramètre de fct. Il faut en tenir compte dans l’écriture des fonctions.

Voici ce que devient le version orienté objet de nos trois fonctions ci-dessus. On choisit la première fonction de création de notre arborescence comme constructeur. Pour faciliter la manipulation des données, on ne va pas directement lier le pointeur sur cette arborescence des données mais à un pointeur sur ce pointeur. C’est à dire que l’on ajoute un niveau en amont à l’arborescence. Les données se retrouveront sous la nouvelle clef "data". On parle alors d’attribut de l’objet. On peut en ajouter d’autres tel que la dimension de la matrice, ou la liste de ses libellés de colonnes et lignes etc.

package square_mat;

sub read_rel{
        my $class=$_[0];
        my $file=$_[1];
        my $h={};
        open(my $F,$file) or die;

        while (my $r = <$F>){
                chomp $r;
                my @r=split("\t",$r);
                if(@r==3) {
                        $h->{$r[0]}{$r[1]}=$r[2];
                }       
        }
        my $M={};
        bless($M,$class);
        $M->{data}=$h;
        return $M;
}

sub clone_rel{
        my $M=$_[0];
        my $class=ref($M);
        my $C;
        my $h=$M->{data};
        my $hC={};
        for my $i (keys(%$h)){               
                my $p=$h->{$i};
                for my $j (keys(%$p)){
                        $hC->{$i}{$j}=$h->{$i}{$j};
                }       
        }       
        bless($C,$class);
        $C->{data}=$hC;
        return $C;
}

sub transpo_rel{
        my $M=$_[0];
        my $h=$M->{data};
        my $hC={};
        for my $i (keys(%$h)){               
                my $p=$h->{$i};
                for my $j (keys(%$p)){
                        $hC->{$j}{$i}=$h->{$i}{$j};
                }       
        }       
        $M->{data}=$hC;
        return $M;
}


sub print_rel{
        my $M=$_[0];
        my $h=$M->{data};
        my $file=$_[1];
        open(my $O,">".$file) or die;
       for my $i (sort(keys(%$h))){
                my $p=$h->{$i};
                for my $j (sort(keys(%$p))){
                        print $O(join("\t",$i,$j,$h->{$i}{$j})."\n");
                }       
        }
}

sub square_rel{
        my $M=$_[0];
        my $h=$M->{data};
        my $s={};
        for my $k (keys(%$h)){
                my $p=$h->{$k};
                for my $i (keys(%$p)){
                        for my $j (keys(%$p)){
                                $s->{$i}{$j}+=$h->{$i}{$k}*$h->{$k}{$j};
                        }
                }
        }
        $M->{data}=$s;
}
1;

Pour le tester il suffit d’insérer ce code dans un fichier texte portant le nom de square_mat.pm et d’appeler cette bibliothèque dans un programme tel que celui-ci en donnant au programme les même arguments que au précédent :

use square_mat;
use strict;
my $M=square_mat->read_rel($ARGV[0]);
$M->square_rel;
$M->print_rel($ARGV[1])

Pour travailler avec des tableaux de données issus de questionnaires, on peut améliorer cette classe en y ajoutant deux attributs correspondant respectivement à l’ensemble des individus et des questions. Ces attributs vont apparaître comme des clefs du tableau associatif représentant l’objet au même niveau que "data" que l’on va renommer en "results".
Voici cette version améliorée (donc un peu plus complexe) de la classe square_mat précédente. Pour la distinguer de la précédente on va l’appeler "dataset".

package dataset;
use strict;

sub new{
   my $proto = shift;
   my $class = ref($proto) || $proto;
   my $dataset={};
   $dataset->{'results'}={};
   $dataset->{'ind'}={};
   $dataset->{'ques'}={};

   bless($dataset,$class);
   return $dataset;
}


sub read_file{

   my $ht=$_[0];
   my $file=$_[1];
   open(my $F,$file) or die;


# On suppose qu'il s'agit d'un fichier tabulée à 3 colonnes

   my $f={};
   my $I={};
   my $Q={};
   my $i=0;
   my $q=0;

   while (my $r=<$F>){
        chomp $r;
       my @r=split(/\t/,$r);
        if(@r==3){
            $f->{$r[0]}{$r[1]}=$r[2];
            if(!exists($I->{$r[0]})){
                $I->{$r[0]}=++$i;
            }
            if(!exists($Q->{$r[1]})){
                $Q->{$r[1]}=++$q;
            }
        }
   }
   $ht->{'results'}=$f;
   $ht->{'ind'}=$I;
   $ht->{'ques'}=$Q;

return $ht;
}

sub print_mat{
   my $ht=shift;
   my %I=reverse(%{$ht->{'ind'}});
   my %Q=reverse(%{$ht->{'ques'}});
   my $nbi=keys(%I);
   my $nbq=keys(%Q);
   my $hr=$ht->{'results'};
   my @lQ;
   for(my $q=1;$q<=$nbq;$q++){
        push(@lQ,$Q{$q});
   }
   my @smat=(join("\t","ind",@lQ));
   for(my $i=1;$i<=$nbi;$i++){
        my @line;
        for my $q (@lQ){
            push(@line,$hr->{$I{$i}}{$q});
      }
        push(@smat,join("\t",$I{$i},@line));
   }
   return join("\n",@smat);
}

1;

Dans cette nouvelle bibliothèque, on se sert de la fonction ref qui teste si un pointeur est lié à une bibliothèque (c.a.d. s’il s’agit d’une instance d’une classe) et renvoie la classe si cet objet existe. De cette manière, on peut faire appel au constructeur à partir d’une instance de la même classe. Par exemple le code suivant crée deux instances différentes de cette classe :

use dataset;
my $dataset1 = dataset->new();
my $dataset2 = $dataset1->new();

Les individus et les questions sont représentées par des tableaux associatifs qui associent un numéro d’ordre à leurs identifiants. Ces tableaux associatifs sont initialisés lors de la lecture du fichier de données avec la méthode read_file. Ces tableaux associatifs ayant donc des valeurs uniques, il s’agit de bijections, peuvent être inversés avec la fonction reverse. Ceci permet d’avoir une méthode d’affichage matriciel "print_mat" qui sort les individus et les questions dans l’ordre dans lequel ils ont été initialement rencontrés. Dans cette méthode, la fonction push permet d’ajouter un élément en fin de liste.

On peut insérer dans cette nouvelle bibliothèque les méthodes précédentes "clone_rel", "transpo_rel" et "square_rel". Il suffit de renommer la clef ’data’ en "results". Le reste est inchangé.

Les Bases de données relationnelles et le XML en héritage

On peut rapidement faire évoluer cette bibliothèque pour y ajouter les opérations de transposée de matrice, multiplication matricielle et croisement de variables ou affichage matriciel. On obtient alors un programme efficace pour de larges matrices creuses dans la limite de la mémoire vive disponible. Pour des données encore plus larges, une solution est de dériver de cette classe, une version qui réalise les calculs sur un SGBDR tel que MySQL, SQL Light, PostGreSQL etc. Par ailleurs, le format de données csv étant dense mais peu robuste (il suffit d’un léger décalage pour qu’il devienne incohérent), c’est le format XML qui s’impose comme standard d’échange de données. Il est alors nécessaire d’ajouter les fonctionnalités adaptée à ce format.

Pour gérer de telles variantes qui repose sur l’utilisation de multiples bibliothèques externes, l’Orienté Objet propose le mécanisme d’agrégation et d’héritage. Les fonction inchangées étant reprises dans une classe parente dont hérite les différentes versions. Selon les besoins on peut ainsi se limiter à l’utilisation de la bibliothèque initiale qui est supposée être la plus autonome possible, ou ses dérivées plus complètes mais qui nécessitent l’installation de ressources supplémentaires.

Commençons par ajouter un accès à une base de données MySQL au code précédent. Le principe est de définir une nouvelle bibliothèque qui va faire appel à la bibliothèque DBI (Data Base Interface) que l’on peut télécharger du site CPAN si elle n’est pas installée par défaut avec votre distribution de perl. Cette nouvelles bibliothèque portera le nom de "dataset::mysql". A gauche des ": :" on doit trouver le nom de la bibliothèque dont elle hérite. A droite on peut donner le nom que l’on veut. En perl 5, il faut cependant que le fichier contenant cette bibliothèque porte ce nom avec l’extension ".pm" (ici mysql.pm) et soit placé dans un dossier portant le nom de la bibliothèque dont on hérite (ici dataset). Ainsi pour utiliser le code suivant, il faut créer un dossier dataset à côté de dataset.pm et créer dans ce dossier un nouveau fichier mysql.pm dans lequel on insérer le code suivant.

# file dataset/mysql.pm

package dataset::mysql;
use dataset;
@ISA = ("dataset");
use DBI();
use strict;

sub new{
   my $proto = shift;
   my $class = ref($proto) || $proto;
   my $dataset=$class->SUPER::new(@_);
   $dataset->{'user'}="spectateur";
   $dataset->{'password'}="***";
   $dataset->{'DB'}="cinema";
   return $dataset;
}


sub dbmysql{

   my $ht=$_[0];
   my $user=$ht->{'user'};
   my $password=$ht->{'password'};
   my $DB=$ht->{'DB'};

   $ht->{'dbh'}=DBI->connect("dbi:mysql:database=".$DB.";host=localhost",$user,$password) or die;
}

sub query{

   my $ht=$_[0];
   my $requete=$_[1];
   my $dbh=$ht->{'dbh'};
   my $sth = $dbh->prepare($requete);
   my $rv = $sth->execute or die;


# On suppose que le résultat de la requête n'a que trois colonnes

   my $f={};
   my $I={};
   my $Q={};
   my $i=0;
   my $q=0;

   while (my @r=$sth->fetchrow_array){
        $f->{$r[0]}{$r[1]}=$r[2];
        if(!exists($I->{$r[0]})){
        $I->{$r[0]}=++$i;
        }
       if(!exists($Q->{$r[1]})){
            $Q->{$r[1]}=++$q;
        }
   }
   $ht->{'results'}=$f;
   $ht->{'ind'}=$I;
   $ht->{'ques'}=$Q;
   return $ht;
}

1;

Dans ce code, le mécanisme d’héritage appairait à trois endroits :

- la liste @ISA (lire "is a" de l’anglais "être un") énumère les bibliothèques dont on hérite.
Toutes ces bibliothèques doivent aussi être déclarées en préambule avec la fonction use.
C’est ainsi le cas de la bibliothèque dataset. Par contre la bibliothèque DBI apparaît avec
l’instruction mais non pas dans la liste ISA. Cela signifie que les objets de DBI sont
"utilisés" ou agrégés mais qu’ils sont de nature différente. On ne prétend pas
accéder directement à leur méthodes ni qu’il soit possible d’accéder aux nôtres au travers
d’un objet DBI.

- l’objet SUPER que l’on utilise dans le constructeur "new", permets justement d’accéder
directement aux méthodes des classes dont on hérité. Ainsi le constructeur de dataset::mysql
utilise celui de dataset en lui ajoutant des attributs qui contiennent tous les éléments nécessaires
à l’accès à la base de données (utilisateur, mot de passe et nom de la base).
L’instruction SUPER est nécessaire
pour ne pas confondre les deux constructeurs. On dit que le constructeur initial est surchargé.
On peut procéder de même avec toutes les autres méthodes. Lorsque au travers d’un objet
dataset::mysql on sollicite une méthode, d’abord l’interpréteur recherche celle-ci dans
dataset::mysql, si il ne la trouve pas, il va le rechercher dans dataset, et sinon dans les autres
bibliothèques définies avec use.

Les méthodes méthodes introduites dans dataset::mysql permettent :
- de se connecter à la base de données avec la mathode "connect" de DBI.
- de lancer une requête sur la base de données et d’insérer le résultat enregistrement par enregistrement
dans un tableau associatif.

Un mot sur la manière dont la requête est lancée sur le base de données.
On procède en trois étapes :
* on prépare la requête (c’est à dire qu’on la soumet à MySQL qui vérifie la syntaxe.
A ce niveau on peut introduire des variables avec de " ?".
* on execute la requête autant de fois que nécessaire. A ce niveau si on a utiliser des variables, alors
il faut donner leurs valeurs en argument.
* on lit les résultats si il y en a.