Recommend Me


Vendredi 17 février 2006

Perl :attributes

(Linux Magazine France n°49 - Avril 2003 - sources)

Les attributs sont apparus dans Perl en même temps que les Threads, avec la version 5.005[1]. Le module attrs[2] offrait uniquement la possibilité de poser un lock exclusif à une fonction dans le cas d’une utilisation des threads. Avec la version 5.6[3], attrs est remplacé par le module attributes[4] plus “souple”. Il est en plus possible de définir ses propres attributs, grâce au package Attribute::Handlers, et ce non seulement pour des sous-programmes, mais également pour des variables.

1. Qu’est ce qu’un attribut ?

Les attributs sont des informations liées aux variables ou aux fonctions (dans la suite on utilisera le terme de fonction en lieu et place de “sous-programme”) qui peuvent être placées pour modifier leur comportement. Prenons l’exemple d’un hachage : on peut alors imaginer de lui accrocher un attribut (disons :nocase) de telle sorte que l’on puisse retrouver les valeurs sans tenir compte de la case des clés. Notez bien que j’ai conscience de la facilite qu’il y a à imposer un tel comportement avec un tie. Mais écrire :

use Attribute::NoCase; # ...     my %hash :nocase;

je trouve ça plus jolie que :

use Tie::NoCase # ...     my %hash; tie %hash 'Tie::NoCase';

Pour être tout à fait franc, vous verrez plus loin que dans la plupart des cas, la création d’un attribut applicable à une variable revient à “cacher” la déclaration avec le Tie.

2. Attribute

L’utilisation des attributs est relativement simple. Comme nous l’avons dit, ils peuvent être appliqués à des fonctions :

sub test :attrsub1 :attrsub2 {   # ...     }

que l’on peut aussi écrire

sub test :attribute1 attribute2 {   # ...     }

ou à des variables :

my $var :attrvar1 :attrvar2 = $something;

que l’on peut aussi écrire

my $var :attrvar1 attrvar2 = $something;

Le module “attribute” est très succinct dans le sens où il ne met à notre disposition que des attributs pour des fonctions; je ne m’y attarderai donc pas trop. Si vous souhaitez rentrer dans le détail je vous engage à lire la documentation[4]. Afin de ne pas vous laisser en reste, je me contenterai d’un exemple qui part d’une question qui m’a été posée il n’y a pas longtemps. Le problème était le suivant : “Comment créer une fonction lvaluable ?” - Réponse simple, “grâce aux attributs” !

Commençons par rappeler qu’une fonction “lvaluable” est une fonction qui se place à gauche (l pour left) d’une expression. On comprendra facilement que le plus simple “élément” de type lvalue est la variable :

my $nom = "Gregoire Lejeune";

(En effet $nom est à gauche dans l’expression). Mais pour une fonction c’est moins évident. Une telle fonction peut donc “recevoir” une valeur. Ceci se comprend mieux en examinant le fonctionnement de substr :

my $nom = "Gregoire Lejeune"; substr( $nom, 0, 8 ) = "Arthur"; print $nom; ## Affiche "Arthur Lejeune"

Il est important de bien comprendre que dans le cas de substr, la fonction renvoie une “partie” de la variable passée en paramètre, cette “partie” étant remplacée par le biais de l’affectation. Ceci peut être généralisé pour toute fonction lvaluable en disant qu’une telle fonction renvoie une lvalue ou une valeur qui peut être placée à gauche d’une affectation. Bon ce n’est pas très clair alors passons par un exemple totalement inutile. Nous allons considérer un hachage et créer une fonction lvaluable permettant de lui ajouter une entrée : addtohash( %hash, $key ) = $value.

#!/usr/bin/perl ## usage: addtohash( %hash, $key ) = $value sub addtohash(\%$) : lvalue {   ${$_[0]}{$_[1]}; } my %hash = (   key1 => "value1",   key2 => "value2" ); addtohash( %hash, 'key3' ) = "value3"; foreach( keys %hash ) {   print $_ . " => " . $hash{$_} . "\n"; }

Compris ? Nous pouvons donc arrêter là l’exposé sur les lvalues et nous recentrer sur les attributs à proprement parler.

3. Attribute::Handlers

La création d’attributs est quelque peu obscure et il n’y a pas abondance de documentation. Heureusement Damian Conway[5] a eu la bonne idée de créer le package Attribute::Handlers pour nous simplifier la tâche. Ce package met à notre disposition un nouvel attribut (:ATTR).

#!/usr/bin/perl use Attribute::Handlers; sub attribut :ATTR {   print "Nom du package dans lequel a été déclaré l'attribut : " . $_[0] . "\n";   print "Attribut appliqué a un(e) " . *{$_[1]}{NAME} . "\n";   print "Nom de l'attribut : " . $_[3] . "\n";   print "Données associées à l'attribut : " . $_[4] . "\n";   print "Phase : " . $_[5] . "\n"; } my $val :attribut(TEST) = 12; print "\$val = " . $val . "\n";

Comme le montre l’exemple ci-dessus, la création d’un attribut se fait tout simplement en appliquant :ATTR à une fonction dont le nom correspond au nom de notre nouvel attribut. Dans le cas des attributs nous parlerons de descripteur (handler) plutôt que de fonction. Les arguments reçus par notre “descripteur” sont les suivants :

  • $_[0] contient le nom du package dans lequel l’attribut a été déclaré (”main” dans notre exemple) ;
  • $_[1] est une référence sur la table des symboles. Nous pouvons donc utiliser *{$_[1]}{NAME} pour déterminer à quoi a été appliqué notre attribut (voir perldata et perlmod pour plus d’info) ;
  • $_[2] est une référence sur “l’élément” auquel est rattaché l’attribut ;
  • $_[3] correspond au nom de l’attribut (mon_attribut) ;
  • $_[4] contient les données associées à l’attribut (TEST) ;
  • $_[5] indique à quelle “phase” s’est faite l’invocation (nous y reviendrons plus tard).

L’exécution donnera donc le résultat suivant :

    Nom du package dans lequel a été déclaré l'attribut : main      Attribut appliqué à un(e) LEXICAL      Nom de l'attribut : attribut      Données associées à l'attribut : TEST      Phase : CHECK      $val = 12;

Nous pouvons maintenant utiliser notre attribut avec “n’importe quoi”. Ainsi si nous ajoutons ceci à notre exemple :

print "\$val = " . $val . "\n"; sub test :attribut(FONC) {   print "Dans 'test' !\n"; } &test( );

Nous obtenons :

    Nom du package dans lequel a été déclaré l'attribut : main      Attribut appliqué à un(e) test      Nom de l'attribut : attribut      Données associées à l'attribut : FONC      Phase : CHECK      Nom du package dans lequel a été déclaré l'attribut : main      Attribut appliqué a un(e) LEXICAL      Nom de l'attribut : attribut      Données associées à l'attribut : TEST      Phase : CHECK      $val = 12      Dans 'test' !

Tâchons maintenant de donner une utilité à tout cela ;)

4. Créer ses propres Attributs

Connaissant maintenant les informations dont nous disposons au niveau du descripteur, nous pouvons passer à la pratique. Nous allons nous baser sur un exemple connu qui consiste à créer un anneau de valeurs. Ceci peut être aisément réalisé grâce à un Tie (Cf. perltie) :

Anneau.pm

package Anneau; sub TIESCALAR {     my ($class, @values) = @_;     bless  \@values, $class;     return \@values; } sub FETCH {     my $self = shift;     push(@$self, shift(@$self));     return $self->[-1]; } sub STORE {     my ($self, $value) = @_;     unshift @$self, $value;     return $value; } 1;

Anneau.pl

#!/usr/bin/perl use Anneau; my $a; tie $a, 'Anneau', qw/bleu rouge vert/; for( 1..4 ) {   print $a."\n"; }

Le résultat de l’exécution de Anneau.pl donnant alors :

    bleu      rouge      vert      bleu

Pour notre attribut, nous créons un module Anneau::Attribut (utilisant Anneau.pm) dans lequel est déclaré une fonction (le descripteur) ayant pour celui de l’attribut que nous souhaitons définir. Le code est le suivant :

Anneau/Attribute.pm

package Anneau::Attribute; use Attribute::Handlers; use Anneau; sub UNIVERSAL::anneau :ATTR {     *{ $_[ 1 ] }{NAME} =~ /LEXICAL/ or do {       die ( ":anneau n'est utilisable qu'avec des variables lexicals" );     };     my @values = split( /\s{1,}/, $_[4] );     tie ${ $_[ 2 ] }, 'Anneau', @values; } 1;

Détaillons un peu.

En ligne 2 nous chargeons le module Attribute::Handlers.

La création de notre attribut se fait par la déclaration de la ligne 4. Le fait d’utiliser UNIVERSAL comme zone de retenue pour la fonction anneau nous permet d’être assuré qu’elle sera exportée partout. Une telle déclaration est à prendre avec des pincettes mais comme il est dit souvent, “there is more than one way to do it” ;).

Lignes 7-9 : notre anneau ne pouvant être qu’une variable lexicale, on vérifie que l’attribut n’a pas été utilisé avec autre chose.

Lignes 7-9 : notre anneau ne pouvant être qu’une variable lexicale, on vérifie que l’attribut n’a pas été utilisé avec autre chose.

Au paragraphe 3 (Attribute::Handlers) nous avons vu que les données associées à l’attribut sont passées comme 5ème paramètre de notre fonction. Cependant contrairement à ce que nous pourrions espérer, nous recevons ces paramètres sous forme d’une chaîne. Il faut découper cette chaîne pour en récupérer chaque mot que l’on stockera dans un tableau, ce dernier regroupant l’ensemble des valeurs à mettre dans l’anneau (ligne 10).

Pour donner la propriété d’anneau à notre variable, nous n’avons plus qu’à la lier à notre tie comme cela est fait dans l’exemple Anneau.pl. Ceci est fait ligne 13 en utilisant la référence sur la variable reçu comme troisième paramètre.

En exécutant maintenant le petit programme suivant nous serons ravis de constater que le résultat est bien celui que nous attendions !

Anneau2.pl

#!/usr/bin/perl use Anneau::Attribute; my $a :anneau(bleu rouge vert); for( 1..4 ) {   print $a."\n"; }

Nous sommes d’accord que notre module Anneau.pm est utilisable sous deux formes : Tie (directement) ou attribut (par le biais de Anneau::Attribut). Dans tous les cas, notre Tie n’est utilisable qu’avec un scalaire. Si nous écrivons

tie %hash, 'Anneau', qw/bleu rouge vert/;

à l’exécution notre programme nous crachera une erreur précisant qu’il n’existe pas de méthode ‘Anneau::TIEHASH’ dans le package Anneau. De même si nous faisons une déclaration sous la forme

my %hash :anneau(bleu rouge vert);

l’erreur reçue sera : Not a SCALAR reference at Anneau/Attribute?.pm line 13. Dans le cadre d’une utilisation avec Tie il n’y a pas de solution simple; alors qu’avec la version attribut, nous pouvons éviter une erreur aussi violente en vérifiant que la condition ref($_[2]) eq “SCALAR” est vrai. Mais il est possible d’aller encore plus loin. Je ne sais pas bien ce que pourrait représenter un anneau de valeur pour un hash ou un tableau, quoiqu’il en soit il est très facile de définir un jeu d’attributs pour chacun des types. Pour cela il suffit simplement de créer un descripteur par type de donnée et de préciser en paramètre de l’attribut celui auquel il s’applique. Exemple :

#!/usr/bin/perl use Attribute::Handlers; sub type :ATTR(SCALAR) {   print "Attribut $_[3] affecté à un scalaire.\n"; } sub type :ATTR(ARRAY) {   print "Attribut $_[3] affecté à un tableau.\n"; } sub type :ATTR(HASH) {   print "Attribut $_[3] affecté à un hashage.\n"; } sub type :ATTR(CODE) {   print "Attribut $_[3] affecté à une fonction.\n"; } my $s :type; my @l :type; my %h :type; sub fonc :type {   return; }

SCALAR, ARRAY, HASH et CODE peuvent être combinés. Vous pouvez ainsi déclarer quelque chose sous la forme :

sub test :ATTR(ARRAY,HASH) {   # ... }

Notez que la combinaison des quatre types peut être remplacée par ANY ou… rien du tout. Donc les trois formes d’écriture suivantes ont le même effet :

sub test :ATTR(SCALAR,ARRAY,HASH,CODE) { ... } sub test :ATTR(ANY) { ... } sub test :ATTR { ... }

Avant d’aller plus loin, revenons une dernière fois sur notre anneau. En fait nous pouvons parfaitement continuer à utiliser notre module Anneau.pm pour créer l’attribut, sans recourir à Anneau::Attribute. Ceci est possible en spécifiant ‘autotie’ au chargement du module Attribute::Handlers :

Anneau3.pl

#!/usr/bin/perl use Anneau; use Attribute::Handlers autotie => { anneau => 'Anneau' }; my $var :anneau("bleu", "rouge", "vert"); for( 1..4 ) {   print $var."\n"; }

A la ligne 4 nous chargeons le module Attribute::Handlers en précisant par le biais d’autotie que l’on crée l’attribut en utilisant le module Anneau.

Il existe une autre option pour Attribute::Handlers qui est ‘autotieref’. Pour comprendre la différence, disons simplement qu’écrire

use Attribute::Handlers autotie => { attribut => 'Tie::Something' }; my $var :attribut(@data);

est la même chose que

tie my $var, 'Tie::Something', @data;

Alors que

use Attribute::Handlers autotieref => { attribut => 'Tie::Something' }; my $var :attribut(@data);

revient à écrire

tie my $var, 'Tie::Something', \$var, @data;

5. RAWDATA

Dans notre module anneau nous avons dû faire un petit traitement afin de transformer la chaîne de paramètres passée à notre attribut. Ce travail de découpage aurait pu être facilement évité en passant directement des données sous forme de tableau avec une écriture de la forme :

my $a :anneau("bleu", "rouge", "vert");

Nous ne recevons plus alors une chaîne, mais une référence sur un tableau anonyme contenant les données passées en paramètre de l’attribut. Il faut néanmoins être très prudent car on serait en droit de penser qu’en écrivant quelque chose comme

my $a :mon_attribut( keys1 => "val1", keys2 => "val2" );

nous devrions recevoir une référence sur un hachage… Et bien non. Ici aussi nous recevrons une référence sur un tableau ! Dans tous les cas le 5ème argument de notre descripteur sera ou une chaîne (dans le cas ou les arguments ne peuvent pas être interprétés comme du Perl valide ou s’il s’agit véritablement d’une chaîne) ou une référence sur un tableau anonyme. Il est possible de forcer un mode plutôt que l’autre. A savoir, on peut explicitement demander à ce que les arguments ne soient pas interprétés (l’inverse n’étant évidement pas possible). Il suffit d’ajouter le mot clé RAWDATA lors de la déclaration de l’attribut.

#!/usr/bin/perl use Attribute::Handlers; sub notrawdata :ATTR {   print "Arguments de $_[3] : ".$_[4]."\n";   if( ref( $_[4] ) eq "ARRAY" ) {     print join( ", ", @{$_[4]} ), "\n";   } } sub rawdata :ATTR(RAWDATA) {   print "Arguments de $_[3] : ".$_[4]."\n";   if( ref( $_[4] ) eq "ARRAY" ) {     print join( ", ", @{$_[4]} ), "\n";   } } sub text {   return( 1, 2, 3, 4, 5 ); } my $var1 :notrawdata(1, 2, 3); my $var2 :notrawdata("toto", "titi", "tata"); my $var3 :notrawdata(toto, titi, tata); my $var5 :notrawdata(key1 => "val1", key2 => "val2"); my $var6 :notrawdata( &text ); my $var7 :notrawdata(toto titi tata); my $var8 :rawdata(1, 2, 3); my $var9 :rawdata("toto", "titi", "tata"); my $var10 :rawdata(toto, titi, tata); my $var11 :rawdata(key1 => "val1", key2 => "val2"); my $var12 :rawdata( &text ); my $var13 :rawdata(toto titi tata);

donne :

    Arguments de notrawdata : ARRAY(0x81954a8)      1, 2, 3      Arguments de notrawdata : ARRAY(0x8195490)      toto, titi, tata      Arguments de notrawdata : ARRAY(0x814fab8)      toto, titi, tata      Arguments de notrawdata : ARRAY(0x81954c0)      key1, val1, key2, val2      Arguments de notrawdata : ARRAY(0x8195430)      1, 2, 3, 4, 5      Arguments de notrawdata : toto titi tata      Arguments de rawdata : 1, 2, 3      Arguments de rawdata : "toto", "titi", "tata"      Arguments de rawdata : toto, titi, tata      Arguments de rawdata : key1 => "val1", key2 => "val2"      Arguments de rawdata :  &text      Arguments de rawdata : toto titi tata

Vous pouvez bien entendu combiner RAWDATA avec SCALAR, HASH, ARRAY, CODE et ANY en les séparant par des virgules dans la déclaration.

6. Attributs et phases de traitement

Par défaut, l’appel aux attributs se fait à la fin de la phase de compilation (CHECK). Ceci est acceptable dans la plupart des cas. Il est cependant possible de spécifier à quellle phase ces appels devront être effectués en utilisant BEGIN, END, CHECK et INIT :

#!/usr/bin/perl use Attribute::Handlers::Prospective; sub begin :ATTR(BEGIN) {   print "Utilisation de $_[3] avec $_[1] en phase $_[5]\n"; } sub check :ATTR(CHECK) {   print "Utilisation de $_[3] avec $_[1] en phase $_[5]\n"; } sub init :ATTR(INIT) {   print "Utilisation de $_[3] avec $_[1] en phase $_[5]\n"; } sub end :ATTR(END) {   print "Utilisation de $_[3] avec $_[1] en phase $_[5]\n"; } BEGIN {   print "BEGIN\n";   my $begin :begin :check :init :end = "begin"; } END {   print "END\n";   my $end :begin :check :init :end = "end"; } print "INIT\n"; my $init :begin :check :init :end = "init";

Pour combiner la phase avec le reste, comme pour RAWDATA, il suffit de le préciser dans la déclaration de l’attribut en le séparant du reste par une virgule. Notez aussi qu’il est possible de combiner les phases :

sub test :ATTR(ARRAY,HASH,RAWDATA,BEGIN,END) { ... }

L’exemple précédant est intéressant à plusieurs niveaux. Tout d’abord parce qu’il illustre le principe dont nous venons de parler, sur les phases d’exécution, ensuite parce qu’il me permet d’introduire le package Attribute::Handlers::Prospective. Ce package, lui aussi développé par Damian Conway, ressemble furieusement à Attribute::Handlers. La principale différence que l’on retiendra vient avec le second argument passé à la fonction “attribut”. $_[1] permet ici de récupérer le nom de “l’élément” auquel a été affecté l’attribut :

#!/usr/bin/perl use Attribute::Handlers::Prospective; sub attribut :ATTR(ANY) {   *{$_[1]}{NAME} =~ /LEXICAL\((.*)\)/ or do {     die "Attribut non applicable a $_[1]";   };   my $name = $1;   print "'" . $_[3] . "' appliqué à " . $name . "\n"; } my $val1 :attribut; my @val2 :attribut; my %val3 :attribut;

Ceci donnera en sortie :

    'attribut' appliqué à $val1      'attribut' appliqué à @val2      'attribut' appliqué à %val3

Conclusion

Aprés ce bref petit voyage dans le monde merveilleux des attributs en Perl, je laisse libre court à votre imagination. Tous les exemples que je vous ai proposés dans cette petite présentation ayant un intérêt somme toute limité, vous trouverez sur le CD-ROM du magazine un module (Monitor.pm) tiré de l’ouvrage “Programmation avancée en Perl” (O’Reilly) et modifié pour utiliser des attributs. Ce module permet de monitorer des variables en les déclarant sous la forme :

use Monitor; # ... my $var :monitor(DEBUG);

Vous pouvez alors visualiser “l’accès” à vos variable si un export DEBUG a été fait avant l’exécution de votre programme.

Je ne saurais trop vous encourager à regarder les modules Attribute sur le CPAN[6] et principalement le code de Attribute::Handlers. Vous trouverez aussi, dans la section développement Perl 6, sur perl.com[7] les RFCs décrivant les évolutions prévues avec la prochaine version ; en espérant que cela vous donnera envie d’approfondir le sujet et au risque de vous rendre impatient ;)


[1] http://perldoc.com/perl5.8.0/pod/perl5005delta.html#New-Modules
[2] http://perldoc.com/perl5.8.0/lib/attrs.html
[3] http://perldoc.com/perl5.8.0/pod/perl56delta.html#New-syntax-for-declaring-subroutine-attributes
[4] http://perldoc.com/perl5.8.0/lib/attributes.html
[5] http://www.csse.monash.edu.au/~damian/
[6] http://www.cpan.org
[7] http://dev.perl.org/rfc/

• • •

Pas de commentaire »

Pas encore de commentaire.

RSS des commentairesTrackBack URI

Laisser un commentaire

You must be logged in to post a comment.

Powered by: WordPress • Template adapted from the Simple Green' Wench theme - RSS