La programmation modulaire =========================== Il s'agit de décomposer un grand programme en morceaux (**modules**) connectés entre eux par des **interfaces** bien définies. Ces modules doivent être aussi indépendants que possible. .. glossary:: module ensemble de ressources liées sémantiquement interface mode d’emploi du module, avec en plus un principe de masquage des informations (partie publique, partie secrète) signature suite de déclarations, types, exceptions, valeurs, modules, etc. implantation suite de définitions, qui doit comporter tout ce qui est requis par la signature Type abstrait et langage de modules : la programmation modulaire permet d'aller très loin dans la programmation structurée. Définir des fonctions dans un fichier séparé -------------------------------------------- Les fonctions peuvent être définies dans un fichier et le programme dans un autre fichier séparé. Dans ce cas, pour pouvoir être exécuté directement avec la commande python `nomfichierprogramme.py`, le fichier du programme doit importer d’abord les fonctions du fichier dans lequel les fonctions sont définies. 1. Fichier de fonctions ~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python # Fichier foncmaxliste.py # Recherche le premier élément maximal dans une liste ou #dans une chaine de caractères def max_list(L) : k = len(L) max, x = L[0], 0 i = 1 while i < k : if max < L[i]: max = L[i] x = i i = i + 1 return max, x 2. Fichier de programme ~~~~~~~~~~~~~~~~~~~~~~~~ Pour utilser les fonctions définies dans d’autres fichiers, le fichier de programme doit commencer par les instructions qui importent ces fichiers de fonctions ou directement les fonctions de ces fichiers. Dans la syntaxe ci-dessous, on importe une ou toutes les fonctions du fichier `foncmaxlist.py`. .. code-block:: python # Fichier progmaxlist.py from foncmaxliste import max_list # ou plus simple: # from foncmaxliste import * print max_list([4,5,6,9,12,5,10,3,18,5,6,7]) couple = max_list([4,5,6,9,12,5,10,3,18,5,6,7]) print ’Max de L est ’, couple[0] print ’et se trouve à la position ’, couple[1] print max_list(’totovaaumarche’) couple = max_list(’totovaaumarche’) print ’Max de L est ’, couple[0] print ’et se trouve à la position ’, couple[1] Au lieu d’importer les fonctions, on peut importer le fichier qui définit les fonctions avec la syntaxe qui suit. Dans ce cas, le fichier de programme sera changé comme suit : .. code-block:: python # Fichier prog2maxlist import foncmaxliste print foncmaxliste.max_list([4,5,6,9,12,5,10,3,18,5,6,7]) # la syntaxe indiquant le chemin d’acces a la fonction max_list utiliser ‘‘.’’ couple = foncmaxliste.max_list([4,5,6,9,12,5,10,3,18,5,6,7]) print ’Max de L est ’, couple[0] print ’et se trouve à la position ’, couple[1] print foncmaxliste.max_list(’totovaaumarche’) couple = foncmaxliste.max_list(’totovaaumarche’) print ’Max de L est ’, couple[0] print ’et se trouve à la position ’, couple[1] L’exécution directe du premier fichier de programme:: python prog max list.py L’exécution directe du seconde fichier de programme:: python prog2 max list.py Définition de l'implémentation d'un module ------------------------------------------- Tout fichier qui contient au moins une définition d’une fonction ou d’une variable est appelé un module (une bibliothèque). Le nom du module est le nom du fichier enlevé le suffixe `.py`. Ainsi, un fichier de programme qui contient au moins une définition d’une fonction ou un fichier qui ne contient que des définition de fonctions sont des modules. On peut importer un module ou des fonctions ou variables d’un module dans un programme, comme nous avons vu dans les exemples ci-dessus. .. important:: on peut importer un module, ou bien lancer un module en tant que programme executable .. code-block:: python if __name__ == '__main__': main() Pour faciliter la programmation, Python définit un certain nombre de **modules internes**, appelés les builtins (la librairie standard). Par exemple : – Lors de l’ouverture d’une session interactive, on est dans un module interne nommé main . Toutes les variables définies par affectation au niveau de ce module sont valides globalement dans la session. – D’autres modules internes sont string, math, random Dans une session de travail sous l’interpréteur Python, la première importation d’un mo- dule qui, à part des fonctions qu’elle définit, contient des instruction de programme fait exécuter ces instructions. Dans la même session, les importations suivantes ne font pas exécuter ces instructions. Pour les exécuter, on utilise la fonction reload(nomdumodule) (sans sufffixe .py). Exemples d'interface -------------------- :: type: son type arguments arg1 : description de l'argument 1 arg2 : description de l'argument 2 préconditions: arg1 > 10 postconditions: result < 19 raises: TypeError, AssertionError, SystemError... test: tests nominaux pour chaque cas spécifié - L'interface racine carrée :: racine: type: float -> float arguments x: float, flottant dont on veut calculer la racine pré: x >= 0 test: racine 25.0 -> 5.0 ; racine (-25) -> raises TypeError - L'interface `lendemain` Il faut définir auparavant un type spécifique appelé `date` :: lendemain: le lendemain est la date qui désigne le jour suivant de la date passée en argument type: date -> date arguments : d: date description: la date dont on veut calculer le lendemain Le langages des modules ------------------------- .. code-block:: ocaml module type PILE = (* signature (interface) du module *) sig type ’a t val create : unit -> ’a t val push : ’a -> ’a t -> unit val pop : ’a t -> ’a end (* implémentation du module *) module Pile : PILE = (* le module est restreint par la signature PILE *) struct type ’a t = ’a list ref let create () = ref [] let push x p = p := x::!p let pop p = match !p with [...] let rec print p = match p with [...] end - `struct .. end` introduit une collection de définitions, valeurs, types ou modules. C'est une **structure**. - `module Nom = struct .. end` permet de donner un nom à cette structure et c'est ça un module. C'est une structure nommée. - `sig ... end` introduit une signature de module : une interface pour un module. On restreint souvent une structure par une signature pour "cacher" certaines définitions. Une signature de module fournit une **interface** entre l'extérieur et l'intérieur d'un module. En dehors du module, on accède à ses composants grâce à la notation pointée .. code-block:: ocaml let p = Pile.create() Pile.push 45 p **Signature inférée** : Lorsque la déclaration d'interface n'existe pas. Contrainte de type par signature ------------------------------------ .. code-block:: ocaml module M = struct type t = int * int * int let make d m y = d,m,y end module M : sig type t = int * int * int val make : ’a -> ’b -> ’c -> ’a * ’b * ’c end .. ifconfig:: exercice ici la signature inférée est du type le plus général. Cela peut poser des difficultés. Lesquelles ? .. ifconfig:: correction une incohérence entre type attendu et type obtenu .. code-block:: ocaml # let d = M.make 52 24 137 ;; val d : int * int * int = (52, 24, 137) .. code-block:: ocaml module M = struct type t = int * int * int ;; let make d m y = d, m, y ;; end ;; let d = M.make 8 5 8 ;; module type S = sig type t ;; val make : int -> int -> int -> t ;; end ;; module MS = (M:S) ;; MS.make 5 1 2 ;; autre exemple .. code-block:: ocaml module type DATE = sig type t = int * int * int val mmax : int -> int -> int val make : int -> int -> int -> t val get_day : t -> int val get_month : t -> int val get_year : t -> int end on est alors obligé de contrôler la vraissemblance des valeurs dans chaque fonction du module, sinon le contrôle n'est pas maîtrisé... .. code-block:: ocaml M.get_month (23,45,67);; - : int = 45 Le problème vient du fait que l’implantation du type Date.t est **publique**. Le Type abstrait de données permet de masquer l’implantation du type. .. code-block:: ocaml module type DATE = sig type t val mmax : int -> int -> int val make : int -> int -> int -> t val get_day : t -> int val get_month : t -> int val get_year : t -> int end usage:: # module Date = (M:DATE) ;; module Date : DATE Le contrôle est maîtrisé:: Date.get_month (23,45,67);; This expression has type int * int * int but is here used with type Date.t Si (M:S) alors la structure M est une instance de la signa- ture S Type et signature ------------------ :: # module type A = sig val a: int -> int end ;; module type A = sig val a : int -> int end # module B = struct let a x = x + 1 ;; end;; module B : sig val a : int -> int end # module C = (B:A) ;; module C : A # C.a 2 ;; - : int = 3 # Module auquel on impose une signature ----------------------------------------- :: module type DATE = sig type t val make: int -> t val get_year: t -> int val get_month: t -> int end ;; module MR = struct type t = int * int let make x y = (x, y) let get_month (x, y) = x let get_year (x, y) = y end ;; module date = (MR:DATE) ;; Structure et signature ----------------------- Une structure peut avoir plusieurs signatures .. code-block:: ocaml # module Cpt = struct let x = ref 0 let reset () = x := 0 let next () = incr x; !x end .. ifconfig:: exercice Créer deux modules n’ayant pas les mêmes droits sans toucher à l’implantation. .. ifconfig:: correction une vue administrateur .. code-block:: ocaml module type ADM = sig val reset : unit -> unit val next : unit -> int end :: module Adm = (Cpt:ADM) # (* le compteur lui-même est invisible *) # Adm.x;; Unbound value Adm.x une vue utilisateur .. code-block:: ocaml module type USR = sig val next : unit -> int end :: # module Usr = (Cpt:USR) # Usr.next();; - : int = 1 # Usr.reset();; Unbound value Usr.reset Contrainte partage du code (et du compteur) : impossible de construire deux composants administrateurs et utilisateurs autonomes. - une unité qui contient les trois modules - un composant qui publie les deux modules mais pas le module commun (un package) L'héritage par inclusion ------------------------ .. code-block:: ocaml # module Cpt2 = struct include Cpt let get() = !x let next() = x := 2 * !x; !x end ;; module Cpt2 : sig val x : int ref val reset : unit -> unit val get : unit -> int val next : unit -> int end Les foncteurs -------------- Si un langage possède un langage de modules, on peut aller plus loin : on peut considérer un module comme étant une expression de base du langage. - La signature d'un module peut être considérée comme le type du module - La structure du module peut être considéré comme sa valeur Quel est l'intérêt ? On peut alors définir des **foncteurs**. foncteur "fonction" d'une structure vers une autre structure. On peut ainsi paramétrer un module par un autre module. .. code-block:: ocaml module Nom (M1 :S1 ) (M2 :S2 ) (M3 :S3 ) ... = struct ... end On applique un foncteur à des paramètres modules, pour obtenir un nouveau module : .. code-block:: ocaml module M = F (Titi) (Toto) exemple, le module Set:: module OrderedInt = struct type t=int let compare = compare end ;; module OrderedInt : sig type t = int val compare : ’a -> ’a -> int end # module IntSet = Set.Make(OrderedInt) module IntSet : sig type elt = OrderedInt.t type t = Set.Make(OrderedInt).t val empty : t Ou, plus court:: module IntSet = Set.Make(struct type t=int let compare = compare end) Le parallèle peut se faire entre les classes et les modules. - module : Encapsulation données/traitements - classes : différenciation données/traitements et possibilité de création de plusieurs instances classes et types, inférences de types de classes