Bonnes pratiques¶
Écrire du Perl maintenable exige de dépasser la reputation de "write-only language" que le langage s'est parfois attire. Les pragmas strict et warnings, les conventions de nommage issues de Perl Best Practices de Damian Conway et l'utilisation disciplinee des idiomes du langage permettent de produire un code Perl lisible et robuste. Ce chapitre couvre les conventions, les idiomes courants, les anti-patterns a éviter et la gestion d'erreurs moderne.
Conventions fondamentales¶
Pragmas obligatoires¶
Tout fichier Perl de production doit commencer par ces deux lignes :
use strict; # Interdit les variables non declarees, les barewords
use warnings; # Active tous les avertissements du compilateur
Depuis Perl 5.36, use v5.36; les active automatiquement, en plus des signatures et d'autres features modernes :
#!/usr/bin/perl
# Perl moderne : une seule ligne active tout
use v5.38;
# Equivalent a :
# use strict;
# use warnings;
# use feature 'signatures';
# use feature 'say';
# ... et plus encore
Ne jamais omettre strict et warnings
Sans use strict, une faute de frappe dans un nom de variable créé silencieusement une nouvelle variable vide. Sans use warnings, les comportements indefinis (undef, conversions de type) passent inapercus. Ces deux pragmas sont non negociables.
Nommage¶
# Variables scalaires : snake_case
my $nom_utilisateur = 'alice';
my $prix_unitaire = 49.99;
# Tableaux : noms au pluriel
my @elements_actifs = (1, 2, 3);
# Hashes : noms descriptifs
my %config_application = (
hote => 'localhost',
port => 3000,
);
# Constantes : MAJUSCULES_AVEC_UNDERSCORES
use constant {
MAX_TENTATIVES => 3,
DELAI_TIMEOUT => 30,
VERSION_API => '1.0',
};
# Sous-routines : snake_case, verbe + complement
sub calculer_total { ... }
sub valider_formulaire { ... }
sub envoyer_notification{ ... }
# Modules et classes : CamelCase
package MonApp::Service::Email;
Formatage avec perltidy¶
# .perltidyrc — configuration a la racine du projet
# Style recommande (proche de PBP — Perl Best Practices)
--indent-columns=4
--maximum-line-length=100
--continuation-indentation=4
--opening-brace-on-new-line
--closing-brace-indentation=0
--comma-arrow-breakpoints=3
--vertical-tightness=0
--paren-tightness=2
# Application du formatage
perltidy lib/MonApp/Service/Email.pm
perltidy -r lib/ # Recursif
# Verification sans modification (mode diff)
perltidy --check-syntax lib/**/*.pm
Idiomes Perl¶
Expressions régulières¶
#!/usr/bin/perl
use v5.36;
my $email = 'alice@example.com';
# Validation simple
if ($email =~ /\A[\w.+-]+@[\w-]+\.[a-z]{2,}\z/i) {
say 'Email valide';
}
# Capture nommee (plus lisible que $1, $2)
my $date = '2025-04-12';
if ($date =~ /(?<annee>\d{4})-(?<mois>\d{2})-(?<jour>\d{2})/) {
say "Annee : $+{annee}, Mois : $+{mois}, Jour : $+{jour}";
}
# Substitution non destructive (ne modifie pas l'original)
my $texte = 'Bonjour le monde';
my $modifie = $texte =~ s/monde/Perl/r;
say $texte; # Bonjour le monde (inchange)
say $modifie; # Bonjour Perl
# Regex compilee pour les boucles (gain de performance)
my $patron = qr/\b\d{4}-\d{2}-\d{2}\b/;
for my $ligne (@lignes) {
if ($ligne =~ $patron) {
# traitement
}
}
# Mode verbeux pour les regex complexes
my $url = qr{
\A # Debut de chaine
(https?) # Protocole (groupe 1)
:// # Separateur
([\w.-]+) # Hote (groupe 2)
(?::(\d+))? # Port optionnel (groupe 3)
(/[^\s?]*)? # Chemin optionnel (groupe 4)
}xi;
map et grep¶
#!/usr/bin/perl
use v5.36;
my @nombres = 1..10;
# grep : filtre une liste
my @pairs = grep { $_ % 2 == 0 } @nombres;
my @grands = grep { $_ > 5 } @nombres;
# map : transforme une liste
my @doubles = map { $_ * 2 } @nombres;
my @chaines = map { "item_$_" } @nombres;
# Combinaison map + grep (pipeline fonctionnel)
my @resultats = map { { id => $_, label => "Label $_" } }
grep { $_ % 2 == 0 }
@nombres;
# Hash a partir de deux tableaux
my @cles = qw(a b c);
my @valeurs = (1, 2, 3);
my %hash = map { $cles[$_] => $valeurs[$_] } 0..$#cles;
# => (a => 1, b => 2, c => 3)
# Inversion d'un hash
my %inverse = reverse %hash;
Contexte scalaire et liste¶
#!/usr/bin/perl
use v5.36;
my @tableau = (1, 2, 3, 4, 5);
# Contexte scalaire : retourne le nombre d'elements
my $taille = @tableau; # 5
my $derniere_index = $#tableau; # 4 (index du dernier element)
# Forcer le contexte scalaire
if (scalar @tableau > 3) { ... }
# Contexte liste vs scalaire avec localtime
my @components = localtime; # Contexte liste : (sec, min, heure, ...)
my $epoch = time; # Secondes depuis epoch
# Wantarray : adapter le comportement selon le contexte
sub resultat {
if (wantarray) {
return (1, 2, 3); # Contexte liste
} else {
return 42; # Contexte scalaire
}
}
Closures et fonctions d'ordre supérieur¶
#!/usr/bin/perl
use v5.36;
# Fabrique de fonctions (closure)
sub creer_multiplieur($facteur) {
return sub($n) { return $n * $facteur };
}
my $doubler = creer_multiplieur(2);
my $tripler = creer_multiplieur(3);
say $doubler->(5); # 10
say $tripler->(5); # 15
# Memoization avec une closure
sub memoize($fn) {
my %cache;
return sub {
my $cle = join ',', @_;
$cache{$cle} //= $fn->(@_);
return $cache{$cle};
};
}
my $fib;
$fib = memoize(sub($n) {
return $n if $n <= 1;
return $fib->($n-1) + $fib->($n-2);
});
say $fib->(10); # 55
Anti-patterns a éviter¶
Variables globales¶
# MAUVAIS : variable globale implicite
our $compteur = 0;
sub incrementer { $compteur++ }
# BON : encapsulation dans un objet ou passage explicite
sub incrementer($compteur_ref) {
$$compteur_ref++;
}
my $c = 0;
incrementer(\$c);
Regex sans commentaires¶
# MAUVAIS : regex illisible
if ($ligne =~ /^(\d{1,3}\.){3}\d{1,3}\s+-\s+\S+\s+\[([^\]]+)\]\s+"(\S+)\s+(\S+)/) { ... }
# BON : regex avec mode /x et commentaires
if ($ligne =~ /
^(\d{1,3}\.){3}\d{1,3} # Adresse IP
\s+-\s+ # Separateur ident
\S+ # Utilisateur
\s+\[([^\]]+)\] # [Horodatage]
\s+"(\S+)\s+(\S+) # "METHODE /chemin
/x) { ... }
Barewords et chaînes magiques¶
# MAUVAIS : bareword comme cle de hash (fragile)
my %h = (cle => valeur); # valeur est un bareword si pas de guillemets
# BON : guillemets explicites
my %h = (cle => 'valeur');
# MAUVAIS : ouverture de fichier avec filehandle bareword
open(FICHIER, '<', 'mon.txt') or die $!;
# BON : filehandle lexical
open(my $fh, '<', 'mon.txt') or die "Ouverture impossible : $!";
String eval¶
# MAUVAIS : eval d'une chaine (securite, performance, maintenabilite)
my $code = "sub calcul { return $valeur_externe * 2 }";
eval $code; # Danger : injection de code
# BON : eval de bloc pour la gestion d'erreurs uniquement
my $resultat = eval { fonction_dangereuse() };
if ($@) {
warn "Erreur : $@";
}
Gestion d'erreurs¶
die et eval¶
#!/usr/bin/perl
use v5.36;
# Lancer une exception
sub diviser($a, $b) {
die "Division par zero\n" if $b == 0;
return $a / $b;
}
# Capturer avec eval
my $resultat = eval { diviser(10, 0) };
if ($@) {
warn "Erreur capturee : $@"; # "Division par zero"
}
# Exceptions objets (plus riches que des chaines)
package MonApp::Exception;
use Moo;
has message => (is => 'ro', required => 1);
has code => (is => 'ro', default => 500);
sub throw($class, %args) {
die $class->new(%args);
}
package main;
use v5.36;
eval {
MonApp::Exception->throw(
message => 'Ressource non trouvee',
code => 404,
);
};
if (ref $@ && $@->isa('MonApp::Exception')) {
printf "Exception %d : %s\n", $@->code, $@->message;
}
Try::Tiny¶
#!/usr/bin/perl
use v5.36;
use Try::Tiny;
# Interface claire inspiree de try/catch
try {
my $resultat = operation_risquee();
traiter($resultat);
}
catch {
if (/connexion refusee/i) {
warn "Probleme reseau : $_";
reconnexion_retry();
} else {
die $_; # Relance les exceptions non gerees
}
}
finally {
# Toujours execute, meme en cas d'exception
nettoyer_ressources();
};
autodie¶
#!/usr/bin/perl
use v5.36;
use autodie; # Les fonctions systeme lancent des exceptions automatiquement
# Sans autodie : obligation de verifier chaque appel
open(my $fh, '<', 'fichier.txt') or die "Erreur : $!";
# Avec autodie : exception automatique si echec
open(my $fh, '<', 'fichier.txt'); # Die automatiquement si erreur
# autodie peut etre limite a certaines fonctions
use autodie qw(open close);
Performance¶
Profiling avec Devel::NYTProf¶
# Installation
cpanm Devel::NYTProf
# Profilage d'un script
perl -d:NYTProf mon_script.pl
# Generation du rapport HTML
nytprofhtml
# Rapport texte
nytprofcsv
Interpréter NYTProf
NYTProf rapporte le temps cumule par sous-routine (exclusive time) et par ligne. Concentrez-vous d'abord sur les sous-routines avec le plus de temps exclusif — ce sont les hotspots réels, pas les fonctions appelantes.
Optimisations courantes¶
#!/usr/bin/perl
use v5.36;
# 1. Precompiler les regex utilisees en boucle
my $patron = qr/\d{4}-\d{2}-\d{2}/; # Compile une seule fois
for my $ligne (@lignes) {
if ($ligne =~ $patron) { ... }
}
# 2. Eviter les copies inutiles de grandes structures
sub traiter_gros_tableau($tableau_ref) {
# Passe la reference, pas une copie
for my $elem (@$tableau_ref) { ... }
}
# 3. Hashes pour les lookups O(1) plutot que grep/exists sur tableau
my %index_ids = map { $_->{id} => 1 } @enregistrements;
if (exists $index_ids{$id_recherche}) { ... } # O(1)
# 4. chomp en masse
chomp(my @lignes = <$fh>); # Plus rapide que de chompier en boucle
# 5. sort avec Schwartzian Transform pour tris couteux
my @trie = map { $_->[0] }
sort { $a->[1] <=> $b->[1] }
map { [ $_, calcul_couteux($_) ] }
@liste;
Modules XS pour les hotspots¶
# JSON::XS est 10-50x plus rapide que JSON::PP pour de gros documents
use JSON::XS; # Requis : compilateur C a l'installation
my $json = JSON::XS->new->utf8->pretty;
my $texte = $json->encode(\%donnees);
my $data = $json->decode($texte_json);
# List::Util est en XS (core Perl) — toujours preferer a une boucle
use List::Util qw(sum min max first reduce any all none);
my $somme = sum(@nombres); # XS, tres rapide
my $max = max(@nombres);
my $pair = first { $_ % 2 == 0 } @nombres;