Initiation au Lua avec Scribunto/Gestion de l'environnement
Dans ce chapitre, nous allons étudier quelques aspects plus liés avec l'utilisateur et l'intégration du Lua dans un des projets wikimédia, c'est-à-dire Scribunto. Nous commencerons par la gestion des erreurs. Nous verrons ensuite quelques commandes utiles permettant d’utiliser un module dans le but de réaliser une certaine fonction.
Gestion des erreurs
modifierDans ce paragraphe, nous allons dire quelques mots sur la gestion des erreurs.
Exposé du problème
modifierLorsqu'on fait un programme pour notre usage personnel, on sait généralement ce qu’il contient et on peut alors l’utiliser correctement. Si ce programme est destiné à être utilisé par quelqu’un d'autre, alors on n’est pas sûr que l'autre utilisateur aura bien compris comment marche le programme. L'utilisateur va, peut-être, faire des erreurs en l'utilisant. Le bon programmeur doit être capable d'anticiper toutes les erreurs qu'un utilisateur peut faire et doit prévoir, dans son programme, des instructions pour permettre au programme de réagir correctement en cas d'erreur d'utilisation.
Prenons un exemple :
Reprenons la fonction p.alerte4 que nous avons écrite au troisième chapitre
local p = {}
function p.alerte4(frame)
local poids = tonumber(frame.args[1])
local reponse
if poids < 55 then
reponse = "Votre poids est acceptable"
else
if poids < 60 then
reponse = "Attention, vous commencez à grossir !"
else
reponse = "Grosse vache !!"
end
end
return reponse
end
return p
Nous avons testé cette fonction en écrivant :
{{#invoke:Balance|alerte4|56}}, et nous avons obtenu : Attention, vous commencez à grossir !
Supposons maintenant que l'utilisatrice, pour qui nous avons écrit cette fonction, n'a pas bien compris comment marche cette fonction et écrive le nombre 56 en toute lettre :
{{#invoke:Balance|alerte4|cinquante-six}}, elle obtiendra alors : Erreur Lua dans Module:Balance à la ligne 35 : attempt to compare nil with number.
Que c'est-il passé. En fait, rien de bien compliqué, nous avons vu, dans le chapitre exposant les fonctions préprogrammées de base, que la fonction tonumber ne comprenait pas le français et était incapable de convertir la chaîne de caractère "cinquante-six" en nombre 56. Et dans ce cas là, elle nous retourne nil. par conséquent la variable poids contenait nil au moment où elle a été comparée au nombre 55, ce qui a provoqué l'erreur de script.
Gestion programmée de l'erreur
modifierComment remédier à ce problème ? Il nous suffit simplement d’éviter que la variable poids soit comparée à un nombre lorsqu'elle contient nil et que dans ce cas la fonction nous retourne un message d'erreur informant l'utilisatrice de l'erreur qu'elle a commise. Nous devons donc encore perfectionner notre programme en écrivant une nouvelle fonction p.alerte5 qui met en œuvre ce que l’on vient de dire :
local p = {}
function p.alerte5(frame)
local poids = tonumber(frame.args[1])
local reponse
if poids == nil then
reponse = "Vous n'avez pas rentré un nombre sous un format reconnaissable !"
else
if poids < 55 then
reponse = "Votre poids est acceptable !"
else
if poids < 60 then
reponse = "Attention, vous commencez à grossir !"
else
reponse = "Grosse vache !!"
end
end
end
return reponse
end
return p
Testons cette nouvelle fonction :
En écrivant :
{{#invoke:Balance|alerte5|56}}, nous obtenons : Attention, vous commencez à grossir !
Et en écrivant :
{{#invoke:Balance|alerte5|cinquante-six}}, nous obtenons : Vous n'avez pas rentré un nombre sous un format reconnaissable !
Nous voyons que nous obtenons une réponse appropriée dans tous les cas de figure.
Gestion à l'aide de pcall
modifierUne autre façon de gérer les erreurs pouvant se produire à l'appel d'une fonction est d’utiliser la fonction pcall dont le rôle est justement de gérer les erreurs à l'appel d'une fonction. Il suffit d'invoquer la fonction en lui donnant comme paramètres : le nom de la fonction, ses paramètres et le message d'erreur s'il y a problème.
Pour tester cette fonction écrivons, dans le Module:Balance une nouvelle fonction p.alerte6 ainsi :
local p = {}
function p.alerte6(frame)
local poids = pcall(tonumber,frame.args[1],"Vous n'avez pas rentré un nombre sous un format reconnaissable !")
local reponse
if poids < 55 then
reponse = "Votre poids est acceptable"
else
if poids < 60 then
reponse = "Attention, vous commencez à grossir !"
else
reponse = "Grosse vache !!"
end
end
return reponse
end
return p
Testons pour voir si ça marche :
En écrivant :
{{#invoke:Balance|alerte6|56}}, nous obtenons : Attention, vous commencez à grossir !
Et en écrivant :
{{#invoke:Balance|alerte6|cinquante-six}}, nous obtenons : Vous n'avez pas rentré un nombre sous un format reconnaissable !
Rapidité d'exécution d'un programme
modifierEn informatique, un programme, bien qu’il s'exécute correctement, peut ne pas donner entièrement satisfaction si son exécution ne se fait pas assez rapidement. Nous n'avons pas évoqué le problème jusqu'à maintenant, mais le programmeur doit garder présent à l'esprit que son programme doit s'exécuter le plus rapidement possible pour éviter de trop monopoliser la machine qui exécute son programme. Bien souvent, pour exécuter une tache particulière, plusieurs solutions s'offrent à nous. Nous devons alors choisir, parmi toutes les solutions possibles, celle dont le temps d’exécution sera le plus bref possible. Prenons un exemple :
Supposons que nous voulions écrire une fonction qui nous renvoie la valeur du polynôme p(x) = 7x4+5x3+3x2+x+2. Une première façon d'écrire cette fonction pourrait être :
local p = {}
function p.poly(frame)
return 7*frame.args[1]^4+5*frame.args[1]^3+3*frame.args[1]^2+frame.args[1]+2
end
return p
Cette façon d'écrire le programme, bien que fonctionnant parfaitement, n’est pas correcte. Pourquoi ?
En fait, lorsqu'on écrit frame.args[1], on fait appel à la valeur de x que nous a fourni l'utilisateur dans la commande #invoke et qui est donc quelque chose d'extérieur au programme. Allez chercher cette valeur met en œuvre des routines qui sont, à elles seules, des programmes qu’il faut appeler et exécuter. tout cela demande du temps. On comprend donc aisément qu'écrire quatre fois frame.args[1] va demander un temps d'exécution plus long que si l’on ne l'écrivait qu'une seule fois. Il est donc bien préférable d'écrire la fonction en s'arrangeant pour n'avoir qu'une seule exécution de frame.args[1]. À la première façon d'écrire le programme, nous préférerons donc la deuxième façon suivante :
local p = {}
function p.poly(frame)
local x = frame.args[1]
return 7*x^4+5*x^3+3*x^2+x+2
end
return p
On pourrait, à ce niveau, être satisfait, de notre programme et penser que l’on a la meilleure façon possible de l'écrire. En fait, il n'en est rien ! Il est encore possible d'améliorer le temps d'exécution de la fonction p.poly. Cette façon d'écrire un polynôme, en informatique, n’est pas correcte. Si nous comptons les multiplications et les additions, nous voyons qu’il y a 9 multiplications et 4 additions. Peut-on imaginer une façon d'écrire un polynôme de façon à réduire le nombre d'opérations. Cela est possible en remarquant, tout simplement, que :
Si nous comptons le nombre de multiplication et d'addition dans l’expression x(x(x(7x+5)+3)+1)+2, nous voyons qu’il y a 4 multiplications et 4 additions. Nous avons 5 multiplications de moins à faire dans la calcul de x(x(x(7x+5)+3)+1)+2 que dans la calcul de 7x4+5x3+3x2+x+2, d'où le gain de temps. Nous pouvons donc améliorer encore le temps d'exécution de la fonction p.poly en l'écrivant :
local p = {}
function p.poly(frame)
local x = frame.args[1]
return (((7*x+5)*x+3)*x+2)*x+1
end
return p
Nous pouvons ainsi espérer avoir la meilleure façon possible d'écrire notre fonction !
La question qui peut maintenant venir à l'esprit est la suivante : Supposons que l’on ait plusieurs façons d'écrire un programme, comment peut-on savoir laquelle de ces façons est la plus rapide ?
Pour cela, nous disposons d'une fonction préprogrammée qui se nomme os.clock et qui est capable de nous donner une approximation du temps d'exécution du programme que l’on est en train d'écrire. Nous allons donc, dans un Module:Polynôme essayer de comparer les temps d'exécution des trois façons d'écrire le programme de calcul de la fonction p.poly vue précédemment. En fait, nous allons écrire trois fonctions : p.poly1, p.poly2 et p.poly3 qui, en plus du résultat nous renvoie le temps d'exécution.
local p = {}
function p.poly1(frame)
local temps = os.clock()
local reponse = "Le résultat est "
reponse = reponse..7*frame.args[1]^4+5*frame.args[1]^3+3*frame.args[1]^2+frame.args[1]+2
temps = os.clock() - temps
reponse = reponse.." et le temps d'exécution est "..temps
return reponse
end
function p.poly2(frame)
local temps = os.clock()
local x = frame.args[1]
local reponse = "Le résultat est "
reponse = reponse..7*x^4+5*x^3+3*x^2+x+2
temps = os.clock() - temps
reponse = reponse.." et le temps d'exécution est "..temps
return reponse
end
function p.poly3(frame)
local temps = os.clock()
local x = frame.args[1]
local reponse = "Le résultat est "
reponse = reponse..(((7*x+5)*x+3)*x+1)*x+2
temps = os.clock() - temps
reponse = reponse.." et le temps d'exécution est "..temps
return reponse
end
return p
Nous remarquons que, pour chaque fonction, nous prenons le temps en début de fonction et en fin de fonction et nous faisons la différence des deux temps pour avoir une estimation du temps d'exécution de la fonction seule. Si nous ne faisions pas cela, nous aurions, à la fin de la troisième fonction, le temps cumulé des trois fonctions car le compteur temps ne s’arrête pas d'une fonction à l'autre.
{{#invoke:Polynôme|poly1|7}} nous donne : Le résultat est 18678 et le temps d'exécution est 2e-05
{{#invoke:Polynôme|poly2|7}} nous donne : Le résultat est 18678 et le temps d'exécution est 0
{{#invoke:Polynôme|poly3|7}} nous donne : Le résultat est 18678 et le temps d'exécution est 0
Nous voyons maintenant clairement que la meilleure des trois fonctions est bien la troisième et la plus mauvaise est bien la première.
En fait, le temps d'exécution obtenu par cette méthode est très approximatif, car nous fonctionnons sur un système multitâche et l'exécution d'une fonction peut être perturbée selon la charge du processeur. Dans l'exécution ci-dessus, nous pouvons même avoir quelquefois un temps d'exécution, de la troisième fonction, supérieur à la seconde. Pour mieux se rendre compte de la rapidité d'exécution de chaque fonction, il est donc conseillé de purger le cache de la page plusieurs fois pour relancer plusieurs fois l'exécution des fonctions et ainsi mieux se rendre compte. |
Comment utiliser des fonctions écrites dans un autre module
modifierDans les chapitres précédents, nous avons dit qu'une variable ou une fonction déclarée avec le mot-clé local n'est utilisable qu’à l'intérieur du module où elles sont déclarées. Cette affirmation sous-entend que si l’on n'emploie pas le mot-clé local, alors la variable ou la fonction déclarées devrait pouvoir être utilisée dans un autre module. Dans ce paragraphe, nous allons donc étudier comment utiliser les variables et les fonctions créées dans un autre module sans le mot-clé local.
Le principal intérêt de cette possibilité va être de pouvoir se confectionner des modules contenant des fonctions qui peuvent être utiles dans plusieurs autres modules en évitant ainsi de devoir les réécrire dans chaque module.
La fonction préprogrammée qui va nous permettre d'appeler le contenu d'un autre module est la fonction require. Il y a deux façons d’utiliser la fonction require selon que l’on souhaite récupérer des objets, dans un autre module, qui ne se trouve pas dans la table que l’on a pris l'habitude d'appeler p (mais qui pourrait s'appeler autrement) ou que l’on souhaite récupérer des objets qui sont dans une table p.
Première façon : On souhaite récupérer des objets indépendants de la table p
modifierIl est, bien sûr, indispensable que les objets que l’on souhaite récupérer ne soient pas déclarés en local.
Prenons un exemple : Dans le Module:Fonction, nous avions écrit une fonction f qui élève un nombre au carré. Dans un autre Module:Aspire, essayons d'y inclure le module Fonction et de créer une fonction carre qui appelle la fonction f pour élever un nombre au carré :
local p = {}
require("Module:Fonction")
function p.carre(frame)
return f(frame.args[1])
end
return p
La fonction require attend une chaîne de caractères. Nous avons donc dû mettre Module:Fonction entre guillemet. Ne pas oublier, aussi, le mot Module. Si l’on avait écrit require("Fonction"), nous aurions eu une erreur de script.
{{#invoke:Aspire|carre|7}} nous donne : 49
Nous avons bien obtenu 7 au carré qui donne 49. On peut aussi vérifier que l’on peut récupérer une variable déclarée dans le module que l’on appelle avec require a condition que cette variable ne soit pas déclarée avec le mot clé local, sinon on obtient une erreur de script.
Deuxième façon : L'objet que l’on souhaite récupérer est dans une table p
modifierC'est un peu comme si l’on souhaitait utiliser la commande #invoke à partir d'un autre module. Mais cette commande ne marche pas si elle est utilisée dans un module. Nous allons donc transférer le contenu de la table p dans une table locale au module appelant.
Prenons un exemple : Dans un Module:Ingère écrivons une fonction compo qui appelle la fonction p.cube se trouvant dans le Module:Réservoir
Le contenu du module Réservoir est :
local p = {}
function p.cube(nombre)
return nombre^3
end
return p
La grosse différence avec ce que l’on avait l'habitude de voir est que l’on ne trouve pas frame entre les parenthèses de la fonction p.cube puisque, cette fois, nous n'avons pas l'intention d’utiliser la commande #invoke. À part cela, le reste est identique !
Question : Peut-on appeler, à partir d'un autre module, une fonction qui aurait frame entre parenthèse ? Réponse : Non, car frame implique obligatoirement l’utilisation de la commande #invoke. Et cette commande ne peut pas être utilisée dans un module. |
Le contenu du module Ingère est :
local p = {}
local t = require("Module:Réservoir")
function p.compo(frame)
local a = frame.args[1]
return t.cube(a)
end
return p
Le contenu de la table p du module Réservoir est transféré dans la table t du module Ingère. La fonction p.cube est devenue la fonction t.cube.
{{#invoke:Ingère|compo|3}} nous donne alors : 27
Priorité de l'interpréteur
modifierEn général, l'endroit le plus adéquat pour appeler un module est de le faire à partir d'un modèle. Ceci est fortement conseillé pour éviter de surcharger l'espace principal avec la commande #invoke. Par conséquent, le plus souvent, les modèles appelleront les modules. Quelquefois, on risque de devoir faire le contraire. C'est-à-dire d'appeler un modèle dans un module. Que se passe-t-il alors ? Nous allons tester cette opération en prenant un exemple. Essayons d'écrire un module qui aurait pour fonction d'encadrer un texte en faisant appel au Modèle:Encadre. Dans un Module:Cadre, nous serions tenté d'écrire une fonction p.cadre1 ainsi :
local p = {}
function p.cadre1(frame)
return "{{Encadre|contenu="..frame.args[1].."}}"
end
return p
{{#invoke:Cadre|cadre1|Coucou, je suis dans un cadre!}} nous donne alors : {{Encadre|contenu=Coucou, je suis dans un cadre!}}
Et là, avec un grand désarroi, nous constatons que cela ne marche pas. Que s'est-il passé ? En fait, l'interpréteur de mediawiki évalue les modèles avant d’avoir les retours des modules. Et, par conséquent, quand le module Cadre nous ramène {{Encadre|contenu=Coucou, je suis dans un cadre!}}, il est déjà trop tard !
Heureusement, la situation n’est pas désespérée car nous disposons, dans notre lua avec scribunto, d'une fonction préprogrammée frame:preprocess qui va évaluer les modèles avant que ceux-ci ne soient retournés. Pour expérimenter cela, nous allons donc écrire, dans le Module:Cadre, une nouvelle fonction p.cadre2, ainsi :
local p = {}
function p.cadre2(frame)
return frame:preprocess("{{Encadre|contenu="..frame.args[1].."}}")
end
return p
{{#invoke:Cadre|cadre2|Coucou, je suis dans un cadre!}} nous donne enfin :
Coucou, je suis dans un cadre! |
Et là, ça marche, youpi !