.. _prog-e3-fonctions: ********* Fonctions ********* Les fonctions sont des blocs d'instructions qui permettent de regrouper un ensemble d'opérations pour les réutiliser. Elles jouent un rôle crucial dans la structuration et l'organisation du code, facilitant la lecture, la maintenance et la réutilisation. On peut donc décrire une fonction par les rôles suivants. - l'abstraction consiste à regrouper un ensemble d'instructions sous un nom unique, ce qui permet de les réutiliser facilement ; - le contrôle est assuré par le transfert de l'exécution vers la fonction lorsqu'elle est appelée, et le retour à l'endroit où elle a été appelée une fois son exécution terminée ; - la composition permet de construire des programmes plus complexes en combinant plusieurs fonctions ; - les fonctions permettent de mettre en oeuvre la répétition via le mécanisme de récursion, où une fonction s'appelle elle-même pour résoudre un problème. En programmation, une fonction est une boite noire : - à laquelle on passe entre ``0`` et ``n`` arguments via ses paramètres; - et qui retourne entre ``0`` et ``1`` valeur. .. note:: Certains langages un peu anciens font la différence entre une fonction (au sens mathématique du terme) qui associe une valeur de retour à une ou plusieurs valeurs d'entrée et une procédure qui est un regroupement d'instructions ne produisant aucune valeur de retour. Dans l'informatique moderne, on s'écarte de l'analogie pure avec les mathématiques et tout est fonction. Une fonction est dite **sans effet de bord**, si la seule interaction observable avec le monde extérieur est sa valeur de retour. Une fonction est dite **pure** si elle est **sans effet de bord** et qu'en plus elle produit la même valeur de retour lors de plusieurs exécutions avec les mêmes arguments. Pour illustrer ces deux concepts, quelques exemples : - une fonction qui calcule la racine carrée d'un nombre réél est sans effet de bord puisqu'elle ne fait que retourner une valeur, sans modifier quoi que ce soit d'autre dans son environnement. Elle est également pure puisqu'elle retourne toujours le même résultat pour le même paramètre ; - une fonction d'affichage a des effets de bords puisqu'elle modifie une autre zone mémoire. Elle est donc impure ; - une fonction qui récupère l'heure courante change sa valeur de retour à chaque appel. Elle est sans effet de bord mais impure. Les fonctions pures sont intéressantes car elles ne modifient pas l'état du programme à l'extérieur de leur corps. Ce qui rend leur maintenance aisée. Paramètres et arguments ======================= Les paramètres sont des variables locales à la fonction, qui sont initialisées avec les valeurs passées en arguments lors de l'appel de la fonction. Les arguments sont les valeurs réelles passées à la fonction lors de son appel. Prenons un exemple simple pour illustrer la différence entre paramètres et arguments. Considérons la fonction suivante qui calcule le carré d'un nombre. .. code-block:: c :linenos: // carre.c #include int carre(int x) { return x * x; } int main() { int a = 5; int b = carre(a); printf("Le carré de %d est %d\n", a, b); return 0; } Ici, ``a`` est un argument et ``x`` est un paramètre. C'est la même chose en Python. .. code-block:: python :linenos: # carre.py def carre(x): return x * x def main(): a = 5 b = carre(a) print(f"Le carré de {a} est {b}") if __name__ == "__main__": main() Se pose maintenant la question du mécanisme utilisé pour passer les arguments aux paramètres. C'est ce que l'on appelle le passage par valeur, le passage par référence ou le passage par copie de référence. On va voir que le langage C utilise les deux premiers mécanismes, tandis que Python utilise le troisième. .. quiz:: quiz-params-args :title: Paramètres et arguments - :quiz:`{"type":"TF","answer":"T"}` Les paramètres sont des variables locales à la fonction. - :quiz:`{"type":"TF","answer":"T"}` Les arguments sont les valeurs passées lors de l'appel de la fonction. - :quiz:`{"type":"TF","answer":"F"}` Un paramètre et un argument ont obligatoirement le même nom. - En C, le paramètre ``x`` de la fonction ``carre(int x)`` est initialisé avec la valeur :quiz:`{"type":"SC", "values":"de l'argument,du paramètre","answer":"de l'argument"}` - Le premier mécanisme de passage d'arguments à une fonction C est le passage par :quiz:`{"type":"SC","values":"valeur,référence,pointeur,adresse","answer":"valeur"}` Passage par valeur (C) ====================== Le passage par valeur (ou par copie de valeur) consiste à passer une copie de la valeur de l'argument au paramètre de la fonction. Ainsi, toute modification du paramètre à l'intérieur de la fonction n'affecte pas l'argument original. Le passage par valeur est l'un des deux mécanismes utilisés par le langage C. Rappelons nous qu'une variable C est définie par 4 paramètres : - son nom ; - son type (qui fixe la taille réservée en mémoire) ; - son adresse ; - et sa valeur. Les fonctions C utilisent ce mécanisme de passage par **copie de valeur**. A l'intérieur de la fonction C, le lien avec le nom et l'adresse de la variable passée en argument est perdu. Pour illustrer ça, implémentons un programme "naïf" d'échange de variable: .. code-block:: c :linenos: // swap1.c #include void swap1(int x, int y) { int tmp = 0; tmp = x; x = y; y = tmp; } int main() { int a = 1; int b = 2; printf("AVANT : a = %d, b = %d\n", a, b); swap1(a, b); printf("APRES : a = %d, b = %d\n", a, b); return 0; } .. important:: La fonction :func:`swap1` ne retourne aucune valeur. Une telle fonction est de type ``void``. .. admonition:: Exercice Compiler et exécuter le programme ci-dessus. Quel est le résultat attendu ? Quel est le résultat obtenu ? Pourquoi ? Pour comprendre, coller le code ci dessus dans `Python Tutor `_ et observer l'évolution pas à pas de la mémoire au cours de l'exécution. Les valeurs passées à la fonction sont affectées à des variables **locales à la fonction**, et la fonction :func:`swap1` ne peut agir que sur ces variables locales et ne peut donc pas modifier les variables qui appartiennent au segment mémoire de la fonction :func:`main`. De façon plus détaillée, dans la fonction principale :func:`main`, observons l'état de la mémoire avant l'appel à la fonction :func:`swap1` : .. image:: images/c-07-fonctions-fig-01.png :scale: 100% :align: center A l'intérieur de la fonction :func:`swap1` les variables ``x``, ``y`` et ``tmp`` sont créées en mémoire (observer le plan d'adressage). ``x`` et ``y`` sont initialisées avec les **valeurs** de ``a`` et ``b``. Observons l'évolution de la mémoire durant l'exécution de :func:`swap1`. .. image:: images/c-07-fonctions-fig-02.png :scale: 100% :align: center La permutation a bien lieu **à l'intérieur** de la fonction mais **sur des variables différentes** de celles des variables initiales. La zone mémoire qui nous intéresse n'ayant pas été affectée, la permutation attendue n'a pas eu lieu. .. image:: images/c-07-fonctions-fig-03.png :scale: 100% :align: center On va voir que l'utilisation de pointeurs apporte une solution élégante à ce problème. Mais avant cela, un petit détour par la mémoire s'impose. .. quiz:: quiz-pass-by-value-c :title: Passage par valeur en C - :quiz:`{"type":"TF","answer":"T"}` Le passage par valeur crée une copie de la valeur de l'argument. - :quiz:`{"type":"TF","answer":"F"}` Une modification du paramètre affecte l'argument original. - :quiz:`{"type":"TF","answer":"T"}` Le paramètre est initialisé avec la valeur de l'argument, pas l'adresse. - Dans la fonction ``swap1``, les variables ``x`` et ``y`` sont des :quiz:`{"type":"FB","answer":"copies","flags":"fuzzy"}` des valeurs de ``a`` et ``b``. - Les variables :quiz:`{"type":"SC","values":"a et b,x et y","answer":"a et b"}` ne sont pas modifiées après l'appel à ``swap1`` La mémoire ========== Souvenons nous de la façon que :ref:`la mémoire est divisée en plusieurs segments ` : - le segment de code qui contient le code exécutable ; - le segment de données qui contient les variables globales ; - le segment de pile (stack) qui contient les variables locales et les adresses de retour des fonctions ; - le segment de tas (heap) qui contient les variables allouées dynamiquement. | .. image:: images/c-07-fonctions-drawio-fig-04.png :scale: 100% :align: center | La mémoire statique est utilisée pour stocker le code exécutable et les variables globales (variables déclarées en dehors de toute fonction). La mémoire de tas (heap) est utilisée pour stocker les variables allouées dynamiquement (ce qui a été abordée dans le paragraphe :ref:`prog-e3-allocation-dynamique`. La mémoire de pile (stack) est utilisée pour stocker les variables locales et les adresses de retour des fonctions. Pour le code ci dessus, la mémoire est organisée comme indiqué dans la figure ci dessous. | .. image:: images/c-07-fonctions-drawio-fig-05.png :scale: 100% :align: center | Lors de l'appel à la fonction :func:`main`, une zone mémoire spécifique et exclusive à la fonction est réservée, dans laquelle on crée les variables ``a`` et ``b``. Lors de l'appel à la fonction :func:`swap1`, une autre zone mémoire exclusive (distincte de la précédente) est réservée, dans laquelle les variables ``x`` et ``y`` sont créées. Le fait que les zones mémoire sont exclusives à chacune des fonctions, interdit à chacune des fonctions d'accéder à la zone mémoire de l'autre. En particulier, :func:`swap1` ne peut pas accéder à la mémoire réservée pour :func:`main` et donc pas modifier les variables ``a`` et ``b``. .. quiz:: quiz-memory-segments :title: Organisation de la mémoire - :quiz:`{"type":"TF","answer":"T"}` La mémoire est divisée en plusieurs segments (code, données, pile, tas). - :quiz:`{"type":"TF","answer":"T"}` La pile stocke les variables locales et les adresses de retour. - :quiz:`{"type":"TF","answer":"F"}` Les variables globales sont stockées dans la pile. - Les variables globales sont stockées dans le segment :quiz:`{"type":"FB","answer":"mémoire statique","flags":"sequence,fuzzy"}` - Le segment :quiz:`{"type":"SC","values":"code,données,pile,tas","answer":"tas"}` stocke les variables allouées dynamiquement Passage par référence (C) ========================= Le passage par référence (ou passage par adresse) consiste à passer l'adresse de l'argument au paramètre de la fonction. Ainsi, toute modification du paramètre à l'intérieur de la fonction affecte directement l'argument original. C'est le second mécanisme utilisé par le langage C pour passer les arguments aux paramètres. On vient de voir que pour permuter les variables, il fallait identifier sans ambiguité **la zone mémoire** des variables concernées. C'est ici que les pointeurs qui **localisent** la zone mémoire à modifier s'avèrent indispensables. .. code-block:: c :linenos: // swap2.c #include void swap2(int *x, int *y) { int tmp=0; tmp = *x; *x = *y; *y = tmp; } int main() { int a = 1; int b = 2; printf("AVANT : a = %d, b = %d\n", a, b); swap2(&a, &b); printf("APRES : a = %d, b = %d\n", a, b); return 0; } .. admonition:: Exercice Compiler et exécuter le programme ci-dessus. Répond il correctement au problème posé ? Pourquoi ? Pour comprendre, coller le code ci dessus dans `Python Tutor `_ et observer l'évolution pas à pas de la mémoire au cours de l'exécution. Effectivement, à l'exécution on obtient bien le résultat attendu :: $ ./swap2 AVANT : a = 1, b = 2 APRES : a = 2, b = 1 Observons maintenant en détail la zone mémoire lors de l'exécution du programme. Dans la fonction principale :func:`main`, observons l'état de la mémoire avant l'appel à la fonction :func:`swap2` : .. image:: images/c-07-fonctions-fig-06.png :scale: 100% :align: center A l'intérieur de la fonction :func:`swap` les variables ``x``, ``y`` et ``tmp`` sont créées en mémoire (observer le plan d'adressage). ``x`` et ``y`` sont initialisées avec les **adresses** de ``a`` et ``b``. On peut accéder à leur contenu avec l'opérateur de déréférencement ``*``. .. image:: images/c-07-fonctions-fig-07.png :scale: 100% :align: center Les valeurs de ``x`` et ``y`` ne sont pas modifiées. On les utilise pour accéder à la zone mémoire de ``a`` et ``b``. .. image:: images/c-07-fonctions-fig-08.png :scale: 100% :align: center Après exécution de la fonction :func:`swap2`, la permutation est opérationnelle. .. image:: images/c-07-fonctions-fig-09.png :scale: 100% :align: center Pour le code ci dessus, la mémoire est organisée comme indiqué dans la figure ci dessous. | .. image:: images/c-07-fonctions-drawio-fig-10.png :scale: 100% :align: center | Exercice -------- Ecrire une fonction :func:`sumult` qui calcule le produit et la somme de 2 entiers. Puisqu'une fonction C ne peut retourner qu'une seule valeur, quel mécanisme doit on mettre en oeuvre pour atteindre l'objectif ? Quelle conséquence pour les arguments ? Combien au total ? Utiliser la fonction :func:`sumult` dans la fonction :func:`main` pour produire l'affichage ci dessous. Avec les valeurs ``3`` et ``4`` définies à l'intérieur du programme, on doit obtenir :: $ gcc -std=c99 -Wall -Wextra sumult.c -o sumult $ ./sumult 3 + 4 = 7 3 x 4 = 12 .. #include void sumult(int a, int b, int* sum, int* mult){ *sum = a + b; *mult = a * b; } int main(){ int a = 3; int b = 4; int sum, mult; sumult(a, b, &sum, &mult); printf("%d + %d = %d\n", a, b, sum); printf("%d x %d = %d\n", a, b, mult); } .. quiz:: quiz-pass-by-reference :title: Passage par référence - :quiz:`{"type":"TF","answer":"T"}` Le passage par référence passe l'adresse de l'argument. - :quiz:`{"type":"TF","answer":"T"}` Une modification du paramètre affecte l'argument original. - :quiz:`{"type":"TF","answer":"T"}` On utilise l'opérateur ``&`` pour passer une adresse en argument. - On accède à la valeur pointée par un pointeur avec l'opérateur :quiz:`{"type":"FB","answer":"*","size":1}` - Dans ``swap2(&a, &b)``, ``&a`` est :quiz:`{"type":"FB","answer":"l'adresse","flags":"fuzzy"}` de la variable ``a``. - :quiz:`{"type":"SC","values":"swap1,swap2","answer":"swap2"}` échange correctement les variables de l'appelant ? Passage par copie de référence (Python) ======================================= Le passage par copie de référence est un mécanisme qui combine les concepts de passage par valeur et de passage par référence. Il consiste à passer une copie de l'adresse d'une variable (ou d'un objet) à une fonction, permettant ainsi à la fonction de modifier l'objet original tout en travaillant avec une copie de son adresse. Ce mécanisme est utilisé par Python. .. note:: La différence entre la copie de référence et le passage par référence pur est que dans le premier cas, la fonction reçoit une copie de l'adresse, ce qui signifie qu'elle peut modifier l'objet pointé par cette adresse, mais elle ne peut pas changer l'adresse elle-même. En revanche, dans le passage par référence pur, la fonction reçoit directement l'adresse originale, ce qui lui permet de modifier à la fois l'objet et l'adresse. On considère la fonction suivante qui manipule un objet atomique (immutable) de type entier. L'objet est passé par copie de référence à la fonction :func:`incrementer`. La fonction retourne un nouvel objet entier, qui est une copie de l'objet original avec sa valeur incrémentée. .. code-block:: python :linenos: # incrementer.py def incrementer(x): return x + 1 def main(): a = 5 b = incrementer(a) print(f"Avant l'appel : a = {a}") print(f"Après l'appel : b = {b}") if __name__ == "__main__": main() Après exécution, on obtient :: Avant l'appel : a = 5 Après l'appel : b = 6 On considère maintenant la fonction suivante qui manipule un objet mutable de type liste. L'objet est passé par copie de référence à la fonction :func:`ajouter_element`. La fonction modifie directement l'objet original en ajoutant un élément à la liste. .. code-block:: python :linenos: # ajouter_element.py def ajouter_element(liste, element): liste.append(element) def main(): ma_liste = [1, 2, 3] print(f"Avant l'appel : {ma_liste}") ajouter_element(ma_liste, 4) print(f"Après l'appel : {ma_liste}") if __name__ == "__main__": main() Après exécution, on obtient :: Avant l'appel : [1, 2, 3] Après l'appel : [1, 2, 3, 4] .. quiz:: quiz-pass-by-reference-copy :title: Passage par copie de référence (Python) - :quiz:`{"type":"TF","answer":"T"}` Python utilise le passage par copie de référence. - :quiz:`{"type":"TF","answer":"T"}` Une fonction Python peut modifier une liste passée en argument. - :quiz:`{"type":"TF","answer":"F"}` Une fonction Python peut modifier un entier passé en argument. - Dans la fonction ``incrementer(x)``, le paramètre ``x`` est :quiz:`{"type":"SC", "values":"mutable,immutable","answer":"immutable","flags":"fuzzy"}` - La fonction ``ajouter_element`` peut modifier la liste parce qu'elle est :quiz:`{"type":"SC", "values":"mutable,immutable","answer":"mutable","flags":"fuzzy"}` - ``incrementer`` doit retourner une valeur pour modifier le résultat car :quiz:`{"type":"SC","values":"les listes sont immutables,les entiers sont mutables","answer":"les entiers sont mutables"}`