LES FONCTIONS
Dans un programme nous avons très souvent un bloc d'instructions
qui effectue un calcul particulier et qui est utilisé à
plusieurs endroits avec, éventuellement, des modifications
concernant la valeur de ses paramètres. Il serait très
fastidieux de écrire plusieurs fois le même code. Nous
pouvons à la place utiliser le concept de fonction qui
est justement associée à un ensemble d'instructions
considérées, lors de l'exécution, comme un tout.
Une fonction est déterminée par son nom et c'est par ce nom
qu'elle sera appelée chaque fois qu'on voudra l'exécuter.
Scilab, comme d'ailleurs tous les autres langages de programmation, possède un très grand nombre de fonctions. Ces fonctions sont
automatiquement chargées lors du lancement d'une session.
LES ARGUMENTS D'UNE FONCTION
Très peu de langages (Pascal de façon assez laxiste, Ada de façon très rigoureuse) font la distinction entre les paramètres dont
les valeurs seront utilisés comme des entrées pour pouvoir
effectuer les calculs et ceux dont on cherche à recupérer les résultats de ces calculs et qui sont, par conséquent, utilisés comme
des sorties.
Les paramètres aussi bien d'entrée ou de sortie sont
appelés arguments de la fonction. La forme
générale de la définition d'une fonction est
function[s1, ..., sN] = nomFonction(e1, ..., eM)
< corps de la fonction >
[endfunction]1
Les arguments e1, ..., eM sont les arguments d'entrée et les
arguments s1, ..., sN sont les arguments de sortie d la fonction.
On voit ainsi qu'en Scilab il y a une distinction absolu entre arguments de
sortie et arguments d'entrée ce qui augmente la fiabilité du
logiciel créé.
Bien sûr un argument peut être en même temps un argument
d'entrée et de sortie. Dans ce cas son nom doit figurer à gauche et
à droite du signe d'égalité dans la définition de la
fonction.
PASSAGE DES ARGUMENTS
L'appel à la fonction précédente se fait à
l'intérieur d'un programme, dit programme appelant,
à l'aide de l'instruction
[y1, ..., yN] = nomFonction(x1, ..., xM)
On remarque qu'il n'est pas nécessaire d'utiliser les mêmes noms que
lors de la définition de la fonction.
Il est intéressant de comprendre la façon dont s'établit la
communication entre le programme appelant et la fonction appelée.
Examinons d'abord un argument d'entrée, par exemple xK auquel
correspond l'argument eK de la définition de la fonction. Le
passage de l'argument entre les deux programmes se fait, comme on dit,
par valeur. C'est-à-dire la valeur de xK est
stockée dans une autre adresse mémoire à laquelle pointe
l'argument eK. De cette façon la fonction recupère la
valeur de l'argument mais pas son adresse. Donc si dans la fonction on
modifie la valeur de cet argument, cette modification n'affectera pas la
variable xK et au retour de l'appel de la fonction, xK aura la valeur qu'elle avait avant l'appel à la fonction. En somme
modifier les valeurs des arguments en entrée ne peuvent pas être
modifiées par une fonction, sauf à l'intérieur de celle-ci.
En ce qui concerne les arguments de sortie, le passage se fait
par adresse. Soit, par exemple, l'argument yL de
l'appel, auquel correspond l'argument sL de la
définition de la fonction. sL pointe à l'adresse
de yL. Ainsi toute modification de la valeur de sL
se répercutera aussitôt à l'argument yL dans la
mesure qu'une seule et même adresse correspond à ces deux
arguments.
Deux erreurs sont à éviter lors de l'écriture du code d'une
fonction :
- Modifier la valeur d'un argument en entrée. Cette modification ne
se propagera au dela de la fonction ell-même.
- Passer comme arguments d'entrée des très grands tableaux. Étant donnée le passage s'effectue en valeur, Scilab duplique
ces tableaux pour pouvoir les utiliser dans la fonction. Nous
risquons ainsi d'avoir un dépassement de la capacité de la
mémoire ce qui
provoquera l'arrêt du programme. Nous verrons par la suite une méthode qui permet d'éviter cette situation.
PORTÉE DES VARIABLES
Nous abordons maintenant un problème qui est traité de manière
laxiste par tous les langages de programmation impérative, sauf Ada. Il
s'agit de la portée des variables, c'est-à-dire de la
question suivante : si, à l'intérieur d'une fonction, on définit
une variable, est-ce qu'elle visible, sans passage par argument, par
d'autres fonctions et lesquelles ? La réponse de Scilab est affirmative
mais l'affaire n'est pas simple. Examinons donc en détail la portée
des variables en essayant de comprendre son mécanisme.
Soit la fonction f1 dans laquelle on définit une
variable nomVar en lui attribuant une valeur quelconque. On
suppose que dans le déroulement du programme c'est la
première fois où on rencontre le nom de cette variable et
par conséquent c'est à ce
moment là que Scilab associe nomVar avec une adresse mémoire, mettons addrNomVar1. Cette variable est appélée
variable locale. Elle est visible, en lecture seulement, par toute
fonction qui est appelée par f1 après sa définition et
aussi par toute fonction appelée par les fonctions appelées par
f1. On dit bien en lecture seulement, ce qui signifie que si, dans
une fonction f2 appelée par f1, on veut assigner une
autre valeur à nomVar, alors Scilab crée une autre variable
avec le même nom mais avec une adresse différente, par exemple
addrNomVar2. Ainsi quand on retourne à la fonction f1,
nomVar est associée à addrNomVar1 et ella ainsi la
valeur définie dans f1. Il reste à savoir ce qui se passe
si f2 appelle à son tour une autre fonction f3 et
celle-ci utilise aussi la variable nomVar. En Scilab, f3 a
accès à la variable nomVar de f2, c'est-à-dire
à l'adresse addrNomVar2. Elle ne pourra pas accéder à
la variable nomVar de l'adresse addrNomVar1.
Pour illustrer ce propos, examinons le programme suivant :
function[res] = mult()
res = x*test;
endfunction
function[res] = cube();
test = x*x;
res = mult();
endfunction;
// Programme principal
test = 1;
x = 2;
y = cube()
test
Le résultat de l'exécution du programme est
test = 1.
y = 8.
test = 1.
exec done
Étudions le comportement du programme. Au programme principal, on définit deux variables test et x avec comme valeurs 1 et 2
respectivement. Notons que ces variables sont, pour Scilab, des variables
locales bien qu'elles soient visibles par la totalité du programme (ce
qui n'est pas le cas avec le C ou le Fortran). On appelle la fonction
cube qui doit retourner la valeur de x à la puissance
3. Dans le programme cube, on modifie la valeur de test. Donc Scilab crée une autre variable avec le même nom et une adresse
différente pour stocker la nouvelle valeur de test. Ensuite on
appelle la fonction mult qui utilise la valeur de test. La valeur qui est utilisée ici est la valeur qui a eu test dans la
fonction cube et non pas la valeur donnée par le programme
principal, ce qui explique le résultat final y = 8. Au retour
au programme principal, test retrouve la valeur qu'il a eu au
programme principal, ce qui explique l'affichage test = 1. Notons
pour terminer que la valeur assignée à test par la fonction
cube est perdue après la fin de cette fonction. C'est la
raison pour laquelle on appelle ces variables locales, car leur existence
est solidaire de la fonction dans laquelle elles sont définies. Elles
sont créées par des fonctions et elles disparaissent dès la fin
des celles-ci.
Scilab possède aussi des variables globales. On le déclare
par l'intermédiaire de l'instruction global. Ainsi global('x') détermine x comme une variable globale. Cette
variable à une portée générale en lecture et en écriture. Il faut seulement que chaque fonction qu'elle l'utilise, fasse référence à celle-ci par l'intermédiare de la même déclaration global. En effet si cette déclaration est absente et
la fonction utilise x, Scilab considérera x comme étant une variable locale définie dans cette fonction.
Reprenons le même programme et déclarons la variable test comme
globale. On a donc
function[res] = mult()
res = x*test;
endfunction
function[res] = cube();
global('test');
test = x*x;
res = mult();
endfunction;
// Programme principal
global('test');
test = 1
x = 2;
y = cube()
test
Les résultats sont maintenant
test = 1.
y = 8.
test = 4.
exec done
On voit que la valeur de test a été changée, c'est-à-dire que nous avons pu modifier le contenu de l'adresse qui est associée à test.
Cette dernière possibilité fournit la différence qui existe
entre variable locale et variable globale. Sans cette dernière possibilité, les deux types de variables seraient identiques.
Les variables globales peuvent aussi permettre le contournement des problèmes de mémoire. Nous avons signaler plus haut que lorsqu'on passe
par arguments d'entrée des grands tableaux, il y a risque de dépassement de la capacité de la mémoire car les entrées d'une
fonction sont dupliquées. Ce risque peut être évité si on
utilise pour ces grands tableaux des variables globales, parce que dans ce
cas les variables ne sont pas dupliquées.
En terminant ce paragraphe, il est utile de faire une remarque générale concernant le passage des valeurs des variables entre
différentes fonctions. Scilab, comme d'autres langages de
programmation, est assez laxiste et permet le passage des variables
en entrée sans mention explicite dans la liste des arguments en
entrée. Il faut le dire haut et fort : l'utilisation de cette
pratique est un de plus grands dangers qui menacent la fiabilité
du logiciel. Écrire des fonctions qui utilisent des variables qui
n'apparaissent pas comme des arguments en entrée, ne facilite
pas la tâche de conception du logiciel, surtout quand ce
logiciel est développé par une ou plusieurs équipes. Il
ne facilite pas non plus la maintenance du logiciel. Il faut bien
savoir que l'industrie du logiciel se trouve toujours confronter
à la fiabilité et à la sécurité du logiciel et
il n'est pas raisonnable d'alourdir la difficulté de
l'écriture du code avec des pratiques contestables. Il est en
effet regrettable que les concepteurs de Scilab
n'ont pas voulu s'inspirer des techniques de production du logiciel utilisées par Ada mais il n'est pas nécessaire de le suivre dans cette
voie. Nous pouvons essayer d'écrire des fonctions de sorte que chaque
variable en entrée y figure explicitement dans la liste des arguments en
entrée, aussi fastidieuse soit-elle cette pratique. Bien évidemment
cette remarque ne concerne pas les variables globales qui, comme nous avons
vu, pour être utilisées, doivent être déclarées par la
commande global.
PLACE DES FONCTIONS
Une fonction peut être placée :
- Soit dans le même fichier que le programme principal.
- Soit dans un fichier à part qu'on appellera nomFic.sci .
Dans ce cas le fichier nomFic.sci doit être invoqué par la
fonction appelante ou, ce qui est plus pratique, par le programme principal
à l'aide de la commande
getf("nomFic.sci")
Bien sûr un fichier peut contenir plusieurs fonctions.
La bonne pratique de programmation impose d'avoir des fichiers qui
contiennent le même type des fonctions. Par exemple un fichier qui
contient des fonctions qui effectuent des calculs, un autre avec des
fonctions d'écriture ou d'affichage, etc.
La bonne pratique de programmation impose aussi que chaque fonction ne fait
qu'une seule chose. Par exemple une fonction fait un calcul mais
l'affichage du résultat de ce calcul doit se faire à l'extérieur
de cette fonction, à l'aide d'une autre fonction spécialisée
à l'affichage.
Ainsi un programme acquiert une forme très modulaire, sa conception se
trouve facilitée et aussi sa maintenance.
STYLES DE PROGRAMMATION
L'écriture d'un code est pour beaucoup de personnes l'occasion de
montrer leurs penchants artistiques, de laisser libre cours à leur
imagination ou encore d'exercer leur dexterité intellectuelle. Or rien
n'est plus desastreux. En effet la fabrication du logiciel est une pratique
industrielle et comme telle elle doit obéir à des normes et
utiliser des techniques codifiées.
En règle générale, lorsqu'on écrit un programme nous
devons avoir constamment à l'esprit deux préoccupations :
économiser le temps d'exécution et économiser l'espace
mémoire. Bien sûr ceci ne doit pas se faire au détriment de
la lisibilité du code. Le code doit rester toujours lisible.
Dans ce paragraphe on examinera des styles de programmation qui permettent
de minimiser le temps d'exécution et le prochain paragraphe sera consacré à la minimisation de la mémoire utilisée.
Scilab est un langage vectoriel ce qui signifie qu'un calcul se fait plus
rapidement en vectorisant qu'en faisant des boucles. Le programme suivant présente trois façons de calculer la somme des
éléments d'un tableau et il utilise la fonction timer() qui
fournit le temps écoulé entre deux appels consécutifs à
cette fonction.
// Test du temps d'exécution
clear();
x=rand(1,900000);
// Somme élément par élément
timer();
s1 = 0;
for i= 1:900000
s1 = s1 + x(i);
end
t1 =timer();
disp(t1,"Temps somme élément par élément");
// Produit scalaire
timer();
y = ones(900000,1);
s2 = x*y;
t2 =timer();
disp(t2,"Temps somme par produit scalaire");
// Somme cumulative
timer();
s3=cumsum(x); // cumsum calcul somme cumulée éléments de x
t3 =timer();
disp(t3,"Temps somme cumulée");
Les résultats de l'exécution sont les suivants :
Temps somme élément par élément
6.9299648
Temps somme par produit scalaire
0.0901296
Temps somme cumulée
0.0600864
exec done
On en conclut que
- si on utilise des fonctions de la bibliothèque Scilab (s'il en
existe) on minimise le temps d'exécution;
- si on utilise le produit scalaire pour faire la somme, donc une
programmation vectorisée, on consomme 1,5 fois de plus que le temps mis
pour le calcul avec les fonctions de Scilab;
- si on exécute une boucle pour le calcul de la somme, alors on
augmente le temps d'exécution de plus de 70 fois !
Un tableau à deux dimensions est considéré par Scilab comme un
tableau à une dimension. En effet Scilab stocke d'abord dans un tableau
la première colonne du tableau bidimensionnel, dans la suite de la première colonne, la deuxième colonne et aini de suite. Nous avons donc
intérêt, dans la mesure du possible, de tarvailler de façon
vectorisée sur les colonnes que sur les lignes. Ainsi le programme
suivant
// Test du temps d'exécution sur un tableau à 2D
clear();
x=rand(3000,3000);
// Somme par extraction des colonnes
y = ones(1,3000);
timer();
s1 = 0;
for j=1:3000
s1 =s1 + y * x(:,j);
end
t1 = timer();
disp(t1,"Temps somme par colonnes");
// Somme par extraction des lignes
y = y';
timer();
s2 = 0;
for i= 1:3000
s2 = s2 + x(i,
* y;
end
t2 =timer();
disp(t2,"Temps somme par lignes");
Les résultats sont les suivants
Temps somme par colonnes
0.3605184
Temps somme par lignes
0.5708208
exec done
Nous constatons que nous avons une augmentation du temps de calcul de plus
de 50% si on travaille sur les lignes que sur les colonnes.
Il y a en Scilab des outils de "profiling" qui permettent de
deceler les instructions les plus gourmandes en temps
d'exécution. Pour utiliser ces outils il faut à
l'invocation des fonctions utiliser l'option "p". Reprenons
l'exemple précédent. Nous construisons le fichier
somme.sci qui contient les fonctions de calcul :
function s1 = somme(x)
// Somme élément par élément
s1 = 0;
for i= 1:900000
s1 = s1 + x(i);
end
endfunction
function s2 = sommePS(x)
// Produit scalaire
y = ones(900000,1);
s2 = x*y;
endfunction
function s3 = sommeCum(x)
// Somme cumulative
s3=cumsum(x);
endfunction
L'exécution du programme principal suivant :
// Test du temps d'exécution
clear();
getf('somme.sci','p');
x=rand(1,900000);
s1 = somme(x);
profile(somme)
donne les résultats suivants2
Ligne | Nb exec | Temps | Interp |
function s1 = somme(x) | 1 | 0 | 0 |
s1 = 0; | 1 | 0 | 2 |
for i= 1:900000 | 900000 | 0.00107 | 0 |
s1 = s1 + x(i); | 900000 | 0.003447 | 6 |
end | 1 | 0 | 0 |
endfunction | 1 | 0 | 0 |
La première colonne indique le nombre d'exécutions de la ligne, la
deuxième le temps cumulé et la troisième l'effort d'interprétation pour une exécution de la ligne correspondante. On peut aussi la
fonction showprofile(nomFonction) qui fournit aussi le texte du
programme, mais le temps cumulé est mis à zéro.
TECHNIQUES DE PROGRAMMATION
L'autre préoccupation majeure de la programmation est la minimisation de
mémoire. Il n'y a pas de méthodes générales qui permettent
de reduire la quantité de la mémoire utilisée. Tout au plus nous
pouvons énumérer quelques principes qui contribuent à l'écriture des programmes "économiques".
- Ne pas engendrer des variables nouvelles chaque fois qu'on a besoin
de stocker une valeur. Nous pouvons réutiliser une variable
déjà existante et qui ne sert plus. Par exemple au
programme précédent nous avons stocké un vecteur-ligne
de 900 000 valeurs dans y. Après nous avions besoin
d'un vecteur-colonne de 900 000 valeurs. Nous avons affecté ce
vecteur à y aussi en économisant une quantité
substantielle de mémoire.
- Économiser l'espace mémoire. Considérons l'exemple
suivant : soit un processus décrit par la relation
y(k)=ay(k-1)+by(k-2)+c ; k ³ 3 |
|
où a,b et c constantes de valeur connue et y(1)=0,y(2)=1.
On sait que ce processus converge et on cherche sa valeur à la
convergence.
Nous pouvons envisager d'écrire la fonction suivante :
function [yConv] = processus(a, b, c)
y(1) = 0;
y(2) = 1;
eps = 0.000001; // Précision convergence
k = 2;
while abs(y(k)-y(k-1)) > eps
k = k+1;
y(k) = a*y(k-1)+b*y(k-2)+c;
end;
yConv = y(k);
endfunction;
Supposons que le processus converge pour k=100000 itérations. Nous avons donc consommé 100 000 cellules mémoire pour stocker
le tableau y ce qui n'est absolument pas indispensable. En effet
pour le calcul de y(k) nous avons besoin seulement des valeurs de
y(k-1) et y(k-2). Nous avons donc la fonction :
function [yConv] = processus(a, b, c)
y2 = 0;
y1 = 1;
eps = 0.000001; // Précision convergence
while abs(y1-y2) > eps
y = a*y1+b*y2+c;
y2 = y1;
y1 = y;
end;
yConv = y;
endfunction;
qui consomme trois cellules mémoire pour le calcul du
processus.
L'égalité entre les nombres flottants n'existe pas. En effet considérons le programme suivant
format('v',24);
delta = 10^(-16);
tf = 0.00000000121;
t = 0.0000000012;
dt = (tf - t)/100000;
while t <> tf
t = t + dt;
end;
Le test while permet l'arrêt de la boucle lorsque t = tf, chose
qui n'arrivera jamais et donc nous avons écrit une boucle
infinie.
On peut à la place écrire le programme
format('v',24);
delta = 10^(-16);
tf = 0.00000000121;
t = 0.0000000012;
while t < tf
t = t + delta;
end;
qui s'arrêtera dès que le test du while sera satisfait.
LES FICHIERS
Si on crée une matrice a et un vecteur b, on peut le stocker dans un
fichiers des données comme suit :
-->a=[1 2 3; -3 4 5]; b=1:10;
-->save('donnees.txt',a,b)
On peut lire ce fichier
-->clear()
-->load('donnes.txt')
-->a
a =
1. 2. 3.
- 3. 4. 5.
-->b
b =
1. 2. 3. 4. 5. 6. 7. 8. 9. 10.
On peut aussi stocker des valeurs numériques formatées :
-->u=file('open','data.dat','new');
-->write(u,a,'(3f10.6)')
-->file('close',u)
Pour la lecture on a
-->clear()
-->u=file('open','data.dat','old');
-->a=read(u,-1,3)
a =
1. 2. 3.
- 3. 4. 5.
-->file('close',u) // Ne pas oublier de fermer le fichier
EXERCICES
EXERCICE 1.- Écrire un programme qui inverse les éléments d'une matrice
m×n .
Exemple : La matrice
donnera
EXERCICE 2.- Edward Lorenz a decouvert les processus chaotiques en étudiant
le comportement d'un système gazeux. Pour son étude Lorenz a
simplifié les équations de Navier-Stockes et il a aboutit au
système suivant de trois équations différentielles ordinaires :
où a,b et r sont des constantes dont les valeurs déterminent le
comportement du système. Pour avoir un comportement chaotique on prend
Le but de cet exercice est de résoudre le système et de tracer sa
trajectoire en deux et trois dimensions.
Travail à faire :
- On écrit d'abord la fonction lorenz qui reproduit les trois
équations ci-dessus. Comme cette fonction doit être utilisée par
la fonction de Scilab ode, qui fournit la résolution des systèmes d'équations différentielles, sa définition doit être du type
où t est un vecteur avec les instants du temps où la solution sera
calculée, y vecteur des points de la solution et ydot est le vecteur de
dérivées de x,y et z par rapport à t.
- On écrit ensuite le programme principal.
- On stocke dans un vecteur colonne que l'on peut nommer y0 les valeurs
initiales :
- x(0) = -3.2917495672888E+00
- y(0) = -6.3058819810691E+00
- z(0) = 8.1821792963329E+00
- On calcule les instants d'intégration : on prend t0 = 0 et tf =
50 et comme pas d'intégration dt = 0.01. Les instants d'intégration
sont donnés par l'instruction
t=t0 : dt : tf;
- Ensuite on appelle la résolution du système par l'instruction
y ode(y0, t0, t, lorenz);
et on récupère dans y la trajectoire selon les trois dimensions.
- On trace les trajectoires en deux dimensions selon les plans x-y, x-z
et y-z.
- On trace la trajectoire en trois dimensions.