x = 11 y = 2 z = x / y | # le résultat est 5 et non 5.5 car la division est entière x = float (11) y = float (2) z = x / y # le résultat est 5.5 car c’est une division entre deux réels Pour éviter d’écrire le type float, on peut également écrire 11.0/2 de façon à spécifier explicitement que la valeur 11.0 est réelle et non entière. L’opérateur // permet d’effectuer une division entière lorsque les deux nombres à diviser sont réels, le résultat est un entier mais la variable est de type réel si l’un des nombres est de type réel. 2.2.3 Booléen Les booléens sont le résultat d’opérations logiques et ont deux valeurs possibles : True ou False. Voici la liste des opérateurs qui s’appliquent aux booléens. opérateur | signification | exemple | and or | et, ou logique | x = TrueorFalse (résultat = True) | not | négation logique | x = notx | x = 4 < 5 print x | # affiche True | print not x | # affiche False | Voici la liste des opérateurs de comparaisons qui retournent des booléens. Ceuxci s’applique à tout type, aux entiers, réels, chaînes de caractères, t-uples Une comparaison entre un entier et une chaîne de caractères est syntaxiquement correcte même si le résultat a peu d’intérêt. opérateur | signification | exemple | < > | inférieur, supérieur | x = 5 < 5 | <= >= | inférieur ou égal, supérieur ou égal | x = 5 <= 5 | == != | égal, différent | x = 5 == 5 opérateur | signification | is | test d’identification | in | test d’appartenance | Ces deux opérateurs seront utilisés ultérieurement, in avec les listes, les dictionnaires, les boucles (paragraphe 3.3.2), is lors de l’étude des listes (paragraphe 2.3.2) et des classes (chapitre 4). Bien souvent, les booléens sont utilisés de manière implicite lors de tests (paragraphe 3.2.1) ce qui n’empêche pas de les déclarer explicitement. 2.2.4 Chaîne de caractères 2.2.4.1 Création d’une chaîne de caractères Définition 2.8 : chaîne de caractères Le terme "chaîne de caractères" ou string en anglais signifie une suite finie de caractères, autrement dit, du texte. Ce texte est compris entre deux guillemets ou deux apostrophes, ces deux symboles sont interchangeables. L’exemple suivant montre comment créer une chaîne de caractères. Il ne faut pas confondre la partie entre guillemets ou apostrophes, qui est une constante, de la variable qui la contient. t = "string = texte" print type (t), t t = ’string = texte, initialisation avec apostrophes’ print type (t), t t = "morceau 1" \ "morceau 2" # second morceau ajouté au premier par l’ajout du symbole \, # il ne doit rien y avoir après le symbole \, # pas d’espace ni de commentaire print t t = """première ligne seconde ligne""" # chaîne de caractères qui s’étend sur deux lignes print t | Le résultat de ce petit programme est le suivant : <type ’str’> string = texte <type ’str’> string = texte, initialisation avec apostrophes morceau 1morceau 2 première ligne seconde ligne Par défaut, le Python ne permet pas l’insertion de caractères tels que les accents dans les chaînes de caractères, le paragraphe 1.5.2 (page 22) explique comment résoudre ce problème. De même, pour insérer un guillemet dans une chaîne de caractères encadrée elle-même par des guillemets, il faut le faire précéder du symbole \. La séquence \” est appelée un extra-caractère (voir table 2.1). Remarque 2.9 : préfixe "r", chaîne de caractères Il peut être fastidieux d’avoir à doubler tous les symboles \ d’un nom de fichier. \” | guillemet | \0 | apostrophe | \n | passage à la ligne | \\ | insertion du symbole \ | \% | pourcentage, ce symbole est aussi un caractère spécial | \t | tabulation | \r | retour à la ligne, peu usité, il a surtout son importance lorsqu’on passe d’un système Windows à Linux car Windows l’ajoute automatiquement à tous ses fichiers textes | Table 2.1 : Liste des extra-caractères les plus couramment utilisés à l’intérieur d’une chaîne de caractères (voir page. Il est plus simple dans ce cas de préfixer la chaîne de caractères par r de façon à éviter que l’utilisation du symbole \ ne désigne un caractère spécial. Les deux lignes suivantes sont équivalentes : s = "C:\\Users\\Dupre\\" s = r"C:\Users\Dupre\" Sans la lettre "r", tous les \ doivent être doublés, dans le cas contraire, Python peut avoir des effets indésirables selon le caractère qui suit ce symbole. 2.2.4.2 Manipulation d’une chaîne de caractères x = 5.567 s = str (x) print type(s), s | # <type ’str’> 5.567 | print len(s) | # affiche 5 | Il existe d’autres fonctions qui permettent de manipuler les chaînes de caractères. syntaxe 2.10 : fonctions et chaînes de caractères res = s.fonction ( ) Où s est une chaîne de caractères, fonction est le nom de l’opération que l’on veut appliquer à s, res est le résultat de cette manipulation. La table 2.3 (page 38) présente une liste non exhaustive des fonctions disponibles dont un exemple d’utilisation suit. Cette syntaxe variable.fonction(arguments) est celle des classes . opérateur | signification | exemple | + | concaténation de chaînes de caractères | t = ”abc” + ”def” | + = | concaténation puis affectation | t += "abc" | in, notin | une chaîne en contient-elle une autre? | "ed" in "med" | ? | répétition d’une chaîne de caractères | t = ”abc” ? 4 | [n] | obtention du nième caractère, le premier caractère a pour indice 0 | t = ”abc” printt[0] # donnea | [i : j] | obtention des caractères compris entre les indices i et j ? 1 inclus, le premier caractère a pour indice 0 | t = ”abc” printt[0 : 2] # donneab | Table 2.2 : Opérations applicables aux chaînes de caractères. count( sub[,st [,end]]) | Retourne le nombre d’occurrences de la chaîne de caractères sub, les paramètres par défaut st et end permettent de réduire la recherche entre les caractères d’indice st et end exclu. Par défaut, st est nul tandis que end correspond à la fin de la chaîne de caractères. | find( sub[,st [,end]]) | Recherche une chaîne de caractères sub, les paramètres par défaut st et end ont la même signification que ceux de la fonction count. Cette fonction retourne -1 si la recherche n’a pas abouti. | isalpha() | Retourne True si tous les caractères sont des lettres, False sinon. | isdigit() | replace( old,new[,co]) | Retourne une copie de la chaîne de caractères en remplaçant toutes les occurrences de la chaîne old par new. Si le paramètre optionnel co est renseigné, alors seules les co premières occurrences seront remplacées. | split( [sep[,mspl]]) | Découpe la chaîne de caractères en se servant de la chaîne sep comme délimiteur. Si le paramètre mspl est renseigné, au plus mspl coupures seront effectuées. | strip([s]) | Supprime les espaces au début et en fin de chaîne. Si le paramètre s est renseigné, la fonction strip supprime tous les caractères qui font partie de s au début et en fin de chaîne. | upper() | Remplace les minuscules par des majuscules. | lower() | Remplace les majuscules par des minuscules. | join(words) | Fait la somme d’un tableau de chaînes de caractères (une liste ou un T-uple). La chaîne de caractères sert de séparateur qui doit être ajouté entre chaque élément du tableau words. Un exemple de cette fonction figure en page 39. | Table 2.3 : Quelques fonctions s’appliquant aux chaînes de caractères, l’aide associée au langage Python fournira la liste complète (voir page. Certains des paramètres sont encadrés par des crochets, ceci signifie qu’ils sont fa- cultatifs. st = "langage python" st = st.upper () # mise en lettres majuscules i = ("PYTHON") # on cherche "PYTHON" dans st print i # affiche 8 print st.count ("PYTHON") # affiche 1 print st.count ("PYTHON", 9) # affiche 0 s = "un;deux;trois" mots = s.split (";") | # mots est égal à [’un’, ’deux’, ’trois’] | mots.reverse () | # retourne la liste, mots devient égal à # [’trois’, ’deux’, ’un’] | s2 = ";".join (mots) | # concaténation des éléments de mots séparés par ";" | print s2 | # affiche trois;deux;un | 2.2.4.3 Formatage d’une chaîne de caractères Python offre une manière plus concise de former une chaîne de caractères à l’aide de plusieurs types d’informations en évitant la conversion explicite de ces informations (fonction str) et leur concaténation. Il est particulièrement intéressant pour les nombres réels qu’il est possible d’écrire en imposant un nombre de décimales fixe. Le format est le suivant : syntaxe 2.11 : chaîne de caractères, formatage " . %c1 . %c2 " % (v1,v2) c1 est un code choisi parmi ceux de la table 2.4 (page 40). Il indique le format dans lequel la variable v1 devra être transcrite. Il en est de même pour le code c2 associé à la variable v2. Les codes insérés dans la chaîne de caractères seront remplacés par les variables citées entre parenthèses après le symbole % suivant la fin de la chaîne de caractères. Il doit y avoir autant de codes que de variables, qui peuvent aussi être des constantes. Voici concrètement l’utilisation de cette syntaxe : x = 5.5 d = 7 s = "caractères" res = "un nombre réel %f et un entier %d, une chaîne de %s, \n" \ "un réel d’abord converti en chaîne de caractères %s" % (x,d,s, str(x+4)) print res res = "un nombre réel " + str (x) + " et un entier " + str (d) + \ ", une chaîne de " + s + \ ",\n un réel d’abord converti en chaîne de caractères " + str(x+4) print res un nombre réel 5.500000 et un entier 7, une chaîne de caractères, un réel d’abord converti en chaîne de caractères 9.5 un nombre réel 5.5 et un entier 7, une chaîne de caractères, un réel d’abord converti en chaîne de caractères 9.5 La première option permet néanmoins un formatage plus précis des nombres réels en imposant par exemple un nombre défini de décimal. Le format est le suivant : syntaxe 2.12 : chaîne de caractères, formatage des nombres "" % x où n est le nombre de chiffres total et d est le nombre de décimales, f désigne un format réel indiqué par la présence du symbole % x = 0.123456789 print x # affiche 0.123456789 print "%1.2f" % x # affiche 0.12 print "%06.2f" % x # affiche 000.12 Il existe d’autres formats regroupés dans la table 2.4. L’aide reste encore le meilleur réflexe car le langage Python est susceptible d’évoluer et d’ajouter de nouveaux formats. d | entier relatif | e | nombre réel au format exponentiel | f | nombre réel au format décimal | g | nombre réel, format décimal ou exponentiel si la puissance est trop grande ou trop petite | s | chaîne de caractères | Table 2.4 : Liste non exhaustive des codes utilisés pour formater des informations dans une chaîne de caractères (voir page. 2.2.5 T-uple Définition 2.13 : T-uple Les T-uple sont un tableau d’objets qui peuvent être de tout type. Ils ne sont pas modifiables. Un T-uple apparaît comme une liste d’objets comprise entre parenthèses et séparés par des virgules. Leur création reprend le même format : x = (4,5) | # création d’un T-uple composé de 2 entiers | x = ("un",1,"deux",2) | # création d’un T-uple composé de 2 chaînes de caractères | | # et de 2 entiers, l’ordre d’écriture est important | x = (3,) # le résultat est un entier | Ces objets sont des vecteurs d’objets. Il est possible d’effectuer les opérations regroupées dans la table 2.5. Etant donné que les chaînes de caractères sont également des tableaux, ces opérations reprennent en partie celles de la table 2.2. xins | vrai si x est un des éléments de s | xnotins | réciproque de la ligne précédente | s + t | concaténation de s et t | s ? n | concatène n copies de s les unes à la suite des autres | s[i] | retourne le ième élément de s | s[i : j] | retourne un T-uple contenant une copie des éléments de s d’indices i à j exclu. | s[i : j : k] | retourne un T-uple contenant une copie des éléments de s dont les indices sont compris entre i et j exclu, ces indices sont espacés de k : i,i + k,i + 2k,i + 3k, | len(s) | nombre d’éléments de s | min(s) | plus petit élément de s, résultat difficile à prévoir lorsque les types des éléments sont différents | max(s) | plus grand élément de s | sum(s) | retourne la somme de tous les éléments | Table 2.5 : Opérations disponibles sur les T-uples, on suppose que s et t sont des T-uples, x est quant à lui quelconque. Remarque 2.14 : T-uple, opérateur [] Les T-uples ne sont pas modifiables, cela signifie qu’il est impossible de modifier un de leurs éléments. Par conséquent, la ligne d’affectation suivante n’est pas correcte : a = (4,5) a [0] = 3 | # déclenche une erreur d’exécution | Le message d’erreur suivant apparaît : Traceback (most recent call last): File "<pyshell#78>", line 1, in -toplevela[0]=3 TypeError: object doesn’t support item assignment | Pour changer cet élément, il est possible de s’y prendre de la manière suivante : a = (4,5) a = (3,) + a[1:2] # crée un T-uple d’un élément concaténé # avec la partie inchangée de a Il existe d’autres types comme le type complex permettant de représenter les nombres complexes. Ce type numérique suit les mêmes règles et fonctionne avec les mêmes opérateurs (excepté les opérateurs de comparaisons) que ceux présentés au paragraphe 2.2.2 et décrivant les nombres. print complex (1,1) | # affiche (1+1j) | Le langage Python offre la possibilité de créer ses propres types immuables mais ils seront définis à partir des types immuables présentés jusqu’ici. 2.3 Types modifiables (ou mutable) Les types modifiables sont des conteneurs (ou containers en anglais) : ils contiennent d’autres objets, que ce soit des nombres, des chaînes de caractères ou des objets de type modifiable. Plutôt que d’avoir dix variables pour désigner dix objets, on en n’utilise qu’une seule qui désigne ces dix objets. Définition 2.15 : type modifiable (ou mutable) Une variable de type modifiable peut être modifiée, elle conserve le même type et le même identificateur. C’est uniquement son contenu qui évolue. 2.3.1 Liste 2.3.1.1 Définition et fonctions Définition 2.16 : liste Les listes sont des collections d’objets qui peuvent être de tout type. Elles sont modifiables. Une liste apparaît comme une succession d’objets compris entre crochets et séparés par des virgules. Leur création reprend le même format : x = [4,5] | # création d’une liste composée de deux entiers | x = ["un",1,"deux",2] | # création d’une liste composée de # deux chaînes de caractères # et de deux entiers, l’ordre d’écriture est important | x = [3,] | # création d’une liste d’un élément, sans la virgule, # le résultat reste une liste | x = [ ] | # crée une liste vide | x = list () | # crée une liste vide | y = x [0] | # accède au premier élément | y = x [-1] | # accède au dernier élément xinl | vrai si x est un des éléments de l | xnotinl | réciproque de la ligne précédente | l + t | concaténation de l et t | l ? n | concatène n copies de l les unes à la suite des autres | l[i] | retourne l’élément ième élément de l, à la différence des T-uples, l’instruction l[i] = ”a” est valide, elle remplace l’élément i par ”a”. Un indice négatif correspond à la position len(l) + i. | l[i : j] | retourne une liste contenant les éléments de l d’indices i à j exclu. Il est possible de remplacer cette sous-liste par une autre en utilisant l’affectation l[i : j] = l2 où l2 est une autre liste (ou un T-uple) de dimension différente ou égale. | l[i : j : k] | retourne une liste contenant les éléments de l dont les indices sont compris entre i et j exclu, ces indices sont espacés de k : i,i+k,i+ 2k,i+3k, Ici encore, il est possible d’écrire l’affectation suivante : l[i : j : k] = l2 mais l2 doit être une liste (ou un T-uple) de même dimension que l[i : j : k]. | len(l) | nombre d’éléments de l | min(l) | plus petit élément de l, résultat difficile à prévoir lorsque les types des éléments sont différents | max(l) | plus grand élément de l | sum(l) | retourne la somme de tous les éléments | dell[i : j] | supprime les éléments d’indices entre i et j exclu. Cette instruction est équivalente à l[i : j] = []. | list(x) | convertit x en une liste quand cela est possible | Table 2.6 : Opérations disponibles sur les listes, identiques à celles des T-uples, on suppose que l et t sont des listes, i et j sont des entiers. x est quant à lui quelconque. 2.3.1.2 Exemples L’exemple suivant montre une utilisation de la méthode sort. x = [9,0,3,5,4,7,8] | # définition d’une liste | print x | # affiche cette liste | x.sort () | # trie la liste par ordre croissant | print x | Voici les deux listes affichées par cet exemple : [9, 0, 3, 5, 4, 7, 8] [0, 3, 4, 5, 7, 8, 9] Pour classer les objets contenus par la liste mais selon un ordre différent, il faut définir une fonction qui détermine un ordre entre deux éléments de la liste. C’est la fonction compare de l’exemple suivant. l.count(x) | Retourne le nombre d’occurrences de l’élément x. Cette notation suit la syntaxe des classes développée au chapitre 4. count est une méthode de la classe list. | l.index(x) | Retourne l’indice de la première occurrence de l’élément x dans la liste l. Si celle-ci n’existe pas, une exception est déclenchée (voir le paragraphe 5 ou l’exemple page 45). | l.append(x) | Ajoute l’élément x à la fin de la liste l. Si x est une liste, cette fonction ajoute la liste x en tant qu’élément, au final, la liste l ne contiendra qu’un élément de plus. | l.extend(k) | Ajoute tous les éléments de la liste k à la liste l. La liste l aura autant d’éléments supplémentaires qu’il y en a dans la liste k. | l.insert(i,x) | Insère l’élément x à la position i dans la liste l. | l.remove(x) | Supprime la première occurrence de l’élément x dans la liste l. S’il n’y a aucune occurrence de x, cette méthode déclenche une exception. | l.pop([i]) | Retourne l’élément l[i] et le supprime de la liste. Le paramètre i est facultatif, s’il n’est pas précisé, c’est le dernier élément qui est retourné puis supprimé de la liste. | l.reverse(x) | Retourne la liste, le premier et dernier élément échange leurs places, le second et l’avant dernier, et ainsi de suite. | l.sort([f,rev]) | Table 2.7 : Opérations permettant de modifier une liste on suppose que l est une liste, x est quant à lui quelconque. def compare (x,y): # crée une fonction if x > y : return -1 # qui retourne -1 si x<y, elif x == y : return 0 # 0 si x == y else : return 1 # 1 si x < y x.sort (compare) # trie la liste x à l’aide de la fonction compare # cela revient à la trier par ordre décroissant print x | Le programme affiche cette fois-ci la ligne : L’exemple suivant illustre un exemple dans lequel on essaye d’accéder à l’indice d’un élément qui n’existe pas dans la liste : x = [9,0,3,5,0] print x.index (1) # cherche la position de l’élément 1 Comme cet élément n’existe pas, on déclenche ce qu’on appelle une exception qui se traduit par l’affichage d’un message d’erreur. Le message indique le nom de l’exception générée (ValueError) ainsi qu’un message d’information permettant en règle générale de connaître l’événement qui en est la cause. Traceback (most recent call last): File "c:/temp/temp", line 2, in -toplevelprint x.index(1) ValueError: list.index(x): x not in list | Pour éviter cela, on choisit d’intercepter l’exception (voir paragraphe 5). x = [9,0,3,5,0] try: print x.index(1) except ValueError: print "1 n’est pas présent dans la liste x" else: print "trouvé" Ce programme a pour résultat : 1 n’est pas présent dans la liste x 2.3.1.3 Fonctions range, xrange Les listes sont souvent utilisées dans des boucles ou notamment par l’intermédiaire de la fonction range. Cette fonction retourne une liste d’entiers. syntaxe 2.17 : range range (debut, fin [,marche]) Exemple : print range (0,10,2) | # affiche [0, 2, 4, 6, 8] | La fonction xrange est d’un usage équivalent à range. Elle permet de parcourir une liste d’entiers sans la créer vraiment. Elle est plus rapide. print xrange (0,10,2) | # affiche xrange(0,10,2) | Cette fonction est souvent utilisée lors de boucles pour parcourir tous les éléments d’un T-uple, d’une liste, d’un dictionnaire Le programme suivant permet par exemple de calculer la somme de tous les entiers impairs compris entre 1 et 20 ex- clu. s = 0 for n in range (1,20,2) : # ce programme est équivalent à s += n # s = sum (range(1,20,2)) Le programme suivant permet d’afficher tous les éléments d’une liste. x = ["un", 1, "deux", 2, "trois", 3] for n in range (0, len(x)) : print "x [%d] = %s" % (n, x [n]) # le résultat est présenté à droite | | x [0] = un x [1] = 1 x [2] = deux x [3] = 2 x [4] = trois x [5] = 3 | 2.3.1.4 Boucles et listes Il est possible aussi de ne pas se servir des indices comme intermédiaires pour accéder aux éléments d’une liste quand il s’agit d’effectuer un même traitement pour tous les éléments de la liste x. x = ["un", 1, "deux", 2] for el in x : print "la liste inclut : ", el | L’instruction forelinx : se traduit littéralement par : pour tous les éléments de la liste, faire Ce programme a pour résultat : la liste inclut : un la liste inclut : 1 la liste inclut : deux la liste inclut : 2 Il existe également des notations abrégées lorsqu’on cherche à construire une liste à partir d’une autre. Le programme suivant construit la liste des entiers de 1 à 5 à partir du résultat retourné par la fonction range. y = list () for i in range(0,5) : y.append (i+1) print y | # affiche [1,2,3,4,5] | Le langage Python offre la possibilité de résumer cette écriture en une seule ligne. Cette syntaxe sera reprise au paragraphe 3.3.2.2. | # affiche [1,2,3,4,5] | Cette définition de liste peut également inclure des tests ou des boucles imbriquées. y = [ i for i in range(0,5) if i % 2 == 0] # sélection les éléments pairs print y # affiche [0,2,4] z = [ i+j for i in range(0,5) \ for j in range(0,5)] # construit tous les nombres i+j possibles print z # affiche [0, 1, 2, 3, 4, 1, 2, 3, 4, 5, 2, # 3, 4, 5, 6, 3, 4, 5, 6, 7, 4, 5, 6, 7, 8] | 2.3.1.5 Collage de séquences, fonction zip On suppose qu’on dispose de n séquences d’éléments (T-uple, liste), toutes de longueur l. La fonction zip permet de construire une liste de T-uples qui est la juxtaposition de toutes ces séquences. Le iième T-uple de la liste résultante contiendra tous les iième éléments des séquences juxtaposées. Si les longueurs des séquences sont différentes, la liste résultante aura même taille que la plus courte des séquences. a = (1,0,7,0,0,0) b = [2,2,3,5,5,5] c = [ "un", "deux", "trois", "quatre" ] d = zip (a,b,c) print d # affiche [(1, 2, ’un’), (0, 2, ’deux’), # (7, 3, ’trois’), (0, 5, ’quatre’)] Il arrive fréquemment de constuire une chaîne de caractères petits bouts par petits bouts comme le montre le premier exemple ci-dessous. Cette construction peut s’avérer très lente lorsque le résultat est long. Dans ce cas, il est nettement plus rapide d’ajouter chaque morceau dans une liste puis de les concaténer en une seule fois grâce à la méthode join. s = "" while <condition> : s += | | s = [] while <condition> : s.append ( ) s = "".join (s) | 2.3.2 Copie A l’inverse des objets de type immuable, une affectation ne signifie pas une copie. Afin d’éviter certaines opérations superflues et parfois coûteuses en temps de traitement, on doit distinguer la variable de son contenu. Une variable désigne une liste avec un mot (ou identificateur), une affection permet de créer un second mot pour désigner la même liste. Il est alors équivalent de faire des opérations avec le premier mot ou le second comme le montre l’exemple suivant avec les listes l et l2. l = [4,5,6] l2 = l print l | # affiche [4,5,6] | print l2 | # affiche [4,5,6] | l2 [1] = "modif" print l | # affiche [4, ’modif’, 6] | print l2 | # affiche [4, ’modif’, 6] | Dans cet exemple, il n’est pas utile de créer une seconde variable, dans le suivant, cela permet quelques raccourcis. l = [[0,1], [2,3]] l1 = l [0] l1 [0] = "modif" # ligne équivalente à : l [0][0] = "modif" | Par conséquent, lorsqu’on affecte une liste à une variable, celle-ci n’est pas recopiée, la liste reçoit seulement un nom de variable. L’affectation est en fait l’association d’un nom avec un objet (voir paragraphe 4.6). Pour copier une liste, il faut utiliser la fonction copy du module copy . Ce point sera rappelé au paragraphe 4.6.3 (page 118). import copy l = [4,5,6] l2 = (l) print l # affiche [4,5,6] print l2 # affiche [4,5,6] print l # affiche [4,5,6] print l2 # affiche [4, ’modif’, 6] | L’opérateur == permet de savoir si deux listes sont égales même si l’une est une copie de l’autre. Le mot-clé is permet de vérifier si deux variables font référence à la même liste ou si l’une est une copie de l’autre comme le montre l’exemple suivant : import copy l = [1,2,3] l2 = (l) l3 = l print l == l2 # affiche True print l is l2 # affiche False print l is l3 # affiche True | Remarque 2.18 : fonction copy et deepcopy Le comportement de la fonction copy peut surprendre dans le cas où une liste contient d’autres listes. Pour être sûr que chaque élément d’une liste a été correctement recopiée, il faut utiliser la fonction deepcopy. La fonction est plus longue mais elle recopie toutes les listes que ce soit une liste incluse dans une liste elle-même incluse dans une autre liste elle-même incluse import copy l = [[1,2,3],[4,5,6]] l2 = (l) l3 = copy.deepcopy (l) l [0][0] = 1111 print l # affiche [[1111, 2, 3], [4, 5, 6]] print l2 # affiche [[1111, 2, 3], [4, 5, 6]] print l3 # affiche [[1, 2, 3], [4, 5, 6]] print l is l2 # affiche False print l [0] is l2 [0] # affiche True print l [0] is l3 [0] # affiche False | La fonction deepcopy est plus lente à exécuter car elle prend en compte les références récursives comme celles de l’exemple suivant où deux listes se contiennent l’une l’autre. l = [1,"a"] ll = [l,3] # ll contient l l [0] = ll # l contient ll print l # affiche [[[ ], 3], ’a’] print ll # affiche [[[ ], ’a’], 3] import copy z = copy.deepcopy (l) print z # affiche [[[ ], 3], ’a’] | 2.3.3 Dictionnaire L’avantage principal des dictionnaires est la recherche optimisée des clés. Par exemple, on recense les noms des employés d’une entreprise dans une liste. On souhaite ensuite savoir si une personne ayant un nom précisé à l’avance appartient à cette liste. Il faudra alors parcourir la liste jusqu’à trouver ce nom ou parcourir toute la liste si jamais celui-ci ne s’y trouve pas . Dans le cas d’un dictionnaire, cette recherche du nom sera beaucoup plus rapide à écrire et à exécuter. 2.3.3.1 Définition et fonctions Définition 2.19 : dictionnaire Les dictionnaires sont des listes de couples. Chaque couple contient une clé et une valeur. Chaque valeur est indicée par sa clé. La valeur peut-être de tout type, la clé doit être de type immuable, ce ne peut donc être ni une liste, ni un dictionnaire. Chaque clé comme chaque valeur peut avoir un type différent des autres clés ou valeurs. Un dictionnaire apparaît comme une succession de couples d’objets comprise entre accolades et séparés par des virgules. La clé et sa valeur sont séparées par le symbole :. Leur création reprend le même format : x = { "cle1":"valeur1", "cle2":"valeur2" } y = { } # crée un dictionnaire vide z = dict () # crée aussi un dictionnaire vide Les indices ne sont plus entiers mais des chaînes de caractères pour cet exemple. Pour associer la valeur à la clé "cle1", il suffit d’écrire : La plupart des fonctions disponibles pour les listes sont interdites pour les dictionnaires comme la concaténation ou l’opération de multiplication (?). Il n’existe plus non plus d’indices entiers pour repérer les éléments, le seul repère est leur clé. La table 2.8 dresse la liste des opérations simples sur les dictionnaires tandis que la table 2.9 dresse la liste des méthodes plus complexes. xind | vrai si x est une des clés de d | xnotind | d[i] | retourne l’élément associé à la clé i | len(d) | nombre d’éléments de d | min(d) | plus petite clé | max(d) | plus grande clé | deld[i] | supprime l’élément associé à la clé i | list(d) | retourne une liste contenant toutes les clés du dictionnaire d. | dict(x) | convertit x en un dictionnaire si cela est possible, d est alors égal à dict(d.items()) | Table 2.8 : Opérations disponibles sur les dictionnaires, d est un dictionnaire, x est quant à lui quelconque. d.copy() | Retourne une copie de d. | d.has_key(x) | Retourne True si x est une clé de d. | d.items() | Retourne une liste contenant tous les couples (clé, valeur) inclus dans le dictionnaire. | d.keys() | Retourne une liste contenant toutes les clés du dictionnaire d. | d.values() | Retourne une liste contenant toutes les valeurs du dictionnaire d. | d.iteritems() | Retourne un itérateur sur les couples (clé, valeur). | d.iterkeys() | Retourne un itérateur sur les clés. | d.itervalues() | Retourne un itérateur sur les valeurs. | d.get(k[,x]) | Retourne d[k], si la clé k est manquante, alors la valeur None est retournée à moins que le paramètre optionnel x soit renseigné, auquel cas, ce sera ce paramètre qui sera retourné. | d.clear() | Supprime tous les éléments du dictionnaire. | d.update(d2) | Le dictionnaire d reçoit le contenu de d2. | d.setdefault( k[,x]) | Définit d[k] si la clé k existe, sinon, lui affecte x à d[k]. | d.popitem() | Retourne un élément et le supprime du dictionnaire. | Table 2.9 : Méthodes associées aux dictionnaires, d, d2 sont des dictionnaires, x est quant à lui quelconque. Les itérateurs sont des objets qui permettent de parcourir rapidement un dictionnaire, ils seront décrits en détail au chapitre 4 sur les classes. Un exemple de leur utilisation est présenté dans le paragraphe suivant. 2.3.3.2 Exemples Il n’est pas possible de trier un dictionnaire. L’exemple suivant permet néanmoins d’afficher tous les éléments d’un dictionnaire selon un ordre croissant des clés. Ces exemples font appel aux paragraphes sur les boucles (voir chapitre 3). d = { "un":1, "zéro":0, "deux":2, "trois":3, "quatre":4, "cinq":5, \ "six":6, "sept":1, "huit":8, "neuf":9, "dix":10 } key = d.keys () () for k in key: print k,d [k] | L’exemple suivant montre un exemple d’utilisation des itérateurs. Il s’agit de construire un dictionnaire inversé pour lequel les valeurs seront les clés et récipro- quement. d = { "un":1, "zero":0, "deux":2, "trois":3, "quatre":4, "cinq":5, \ "six":6, "sept":1, "huit":8, "neuf":9, "dix":10 } dinv = { } # création d’un dictionnaire vide, on parcout for key,value in d.items () : # les éléments du dictionnaire comme si # c’était une liste de 2-uple (clé,valeur) dinv [value] = key # on retourne le dictionnaire print dinv # affiche {0: ’zero’, 1: ’un’, 2: ’deux’, # 3: ’trois’, 4: ’quatre’, 5: ’cinq’, 6: ’six’, # 8: ’huit’, 9: ’neuf’, 10: ’dix’} d = { "un":1, "zero":0, "deux":2, "trois":3, "quatre":4, "cinq":5, \ "six":6, "sept":1, "huit":8, "neuf":9, "dix":10 } print d.items () print d.iteritems () | Il a pour résultat : [(’trois’, 3), (’sept’, 1), (’neuf’, 9), (’six’, 6), (’zero’, 0), (’un’, 1), (’dix’, 10), (’deux’, 2), (’huit’, 8), (’quatre’, 4), (’cinq’, 5)] <dictionary-itemiterator object at 0x0115DC40> | Remarque 2.20 : modification d’un dictionnaire dans une boucle D’une manière générale, il faut éviter d’ajouter ou de supprimer un élément dans une liste ou un dictionnaire qu’on est en train de parcourir au sein d’une boucle for ou while. Cela peut marcher mais cela peut aussi aboutir à des résultats imprévisibles surtout avec l’utilisation d’itérateurs (fonction iteritems, itervalues, iterkeys). Il est préférable de terminer le parcours de la liste ou du dictionnaire puis de faire les modifications désirées une fois la boucle terminée. Dans le meilleur des cas, l’erreur suivante survient : File "", line 6, in <module> for k in d : RuntimeError: dictionary changed size during iteration | 2.3.3.3 Copie A l’instar des listes (voir paragraphe 2.3.2), les dictionnaires sont des objets et une affectation n’est pas équivalente à une copie comme le montre le programme suivant. d = {4:4,5:5,6:6} d2 = d print d # affiche {4: 4, 5: 5, 6: 6} print d2 # affiche {4: 4, 5: 5, 6: 6} d2 [5] = "modif" print d # affiche {4: 4, 5: ’modif’, 6: 6} print d2 # affiche {4: 4, 5: ’modif’, 6: 6} | Lorsqu’on affecte une liste à une variable, celle-ci n’est pas recopiée, la liste reçoit seulement un nom de variable. L’affectation est en fait l’association d’un nom avec un objet (voir paragraphe 4.6). Pour copier une liste, il faut utiliser la fonction copy du module copy. d = {4:4,5:5,6:6} import copy d2 = (l) d2 [5] = "modif" print d # affiche {4: 4, 5: 5, 6: 6} print d2 # affiche {4: 4, 5: ’modif’, 6: 6} | Le mot-clé is a la même signification pour les dictionnaires que pour les listes, l’exemple du paragraphe 2.3.2 (page 47) est aussi valable pour les dictionnaires. Il en est de même pour la remarque concernant la fonction deepcopy. Cette fonction recopie les listes et les dictionnaires. 2.3.3.4 Clés de type modifiable Ce paragraphe concerne davantage des utilisateurs avertis qui souhaitent malgré tout utiliser des clés de type modifiable. Dans l’exemple qui suit, la clé d’un dictionnaire est également un dictionnaire et cela provoque une erreur. Il en serait de même si la variable k utilisée comme clé était une liste. k = { 1:1} d = { } d [k] = 0 | | Traceback (most recent call last): File "", line 3, in <module> d [k] = 0 TypeError: dict objects are unhashable | | Cela ne veut pas dire qu’il faille renoncer à utiliser un dictionnaire ou une liste comme clé. La fonction id permet d’obtenir un entier qui identifie de manière unique tout objet. Le code suivant est parfaitement correct. k = { 1:1} d = { } d [id (k)] = 0 | Toutefois, ce n’est pas parce que deux dictionnaires auront des contenus identiques que leurs identifiants retournés par la fonction id seront égaux. C’est ce qui explique l’erreur que provoque la dernière ligne du programme suivant. k = {1:1} d = { } d [id (k)] = 0 b = k print d [id(b)] # affiche bien zéro c = {1:1} print d [id(c)] # provoque une erreur car même si k et c ont des contenus égaux, # ils sont distincts, la clé id(c) n’existe pas dans d class A : pass k = A () d = { } d [k] = 0 print d # affiche {<__main__.A object at 0x0120DB90>: 0} print id (k), hex(id(k)) # affiche 18930576, 0x120db90 print d [id(k)] # provoque une erreur | La fonction hex convertit un entier en notation hexadécimale. Les nombres affichés changent à chaque exécution. Pour conclure, ce dernier exemple montre comment se passer de la fonction id dans le cas d’une clé de type dictionnaire. class A (dict): def __hash__(self): return id(self) k = A () k ["t"]= 4 d = { } d [k] = 0 print d # affiche {{’t’: 4}: 0} | 2.4 Extensions 2.4.1 Mot-clé print, repr, conversion en chaîne de caractères Le mot-clé print est déjà apparu dans les exemples présentés ci-dessus, il permet d’afficher une ou plusieurs variables préalablement définies, séparées par des virgules. Les paragraphes qui suivent donnent quelques exemples d’utilisation. La fonction print permet d’afficher n’importe quelle variable ou objet à l’écran, cet affichage suppose la conversion de cette variable ou objet en une chaîne de caractères. Deux fonctions permettent d’effectuer cette étape sans toutefois afficher le résultat à l’écran. La fonction str (voir paragraphe 2.2.4.2) permet de convertir toute variable en chaîne de caractères. Il existe cependant une autre fonction repr, qui effectue cette conversion. Dans ce cas, le résultat peut être interprété par la fonction eval (voir paragraphe 2.4.2) qui se charge de la conversion inverse. Pour les types simples comme ceux présentés dans ce chapitre, ces deux fonctions retournent des résultats identiques. Pour l’exemple, x désigne n’importe quelle variable. x == eval (repr(x)) # est toujours vrai (True) x == eval (str (x)) # n’est pas toujours vrai 2.4.2 Fonction eval x = 32 y = 9 op = "+ - * / % // & | and or << >>".split () for o in op : s = str (x) + " " + o + " " + str (y) print s, " = ", eval (s) | Ceci aboutit au résultat suivant : 32 + 9 = 41 32 - 9 = 23 32 * 9 = 288 32 / 9 = 3 32 % 9 = 5 32 // 9 = 3 32 & 9 = 0 32 | 9 = 41 32 and 9 = 9 32 or 9 = 32 32 << 9 = 16384 32 >> 9 = 0 | Le programme va créer une chaîne de caractères pour chacune des opérations et celleci sera évaluée grâce à la fonction eval comme si c’était une expression numérique. Il faut bien sûr que les variables que l’expression mentionne existent durant son évaluation. 2.4.3 Informations fournies par Python Bien que les fonctions ne soient définies que plus tard (paragraphe 3.4), il peut être intéressant de mentionner la fonction dir qui retourne la liste de toutes les variables créées et accessibles à cet instant du programme. L’exemple suivant : Retourne le résultat suivant : [’__builtins__’, ’__doc__’, ’__file__’, ’__name__’, ’x’] Certaines variables - des chaînes des caractères - existent déjà avant même la première instruction. Elles contiennent différentes informations concernant l’environnement dans lequel est exécuté le programme Python : __builtins__ | Ce module contient tous les éléments présents dès le début d’un programme Python, il contient entre autres les types présentés dans ce chapitre et des fonctions simples comme range. | __doc__ | C’est une chaîne commentant le fichier, c’est une chaîne de caractères insérée aux premières lignes du fichiers et souvent entourée des symboles ””” (voir chapitre 6). | __file__ | Contient le nom du fichier où est écrit ce programme. | __name__ | Contient le nom du module. Remarque 2.21 : fonction dir La fonction dir appelée sans argument donne la liste des fonctions et variables définies à cet endroit du programme. Ce résultat peut varier selon qu’on se trouver dans une fonction, une méthode de classe ou à l’extérieur du programme. L’instruction dir([]) donne la liste des méthodes qui s’appliquent à une liste. De la même manière, la fonction type retourne une information concernant le type d’une variable. x = 3 print x, type(x) | # affiche 3 <type ’int’> | x = 3.5 print x, type(x) | # affiche 3.5 <type ’float’> | 2.4.4 Affectations multiples Il est possible d’effectuer en Python plusieurs affectations simultanément. x = 5 # affecte 5 à x y = 6 # affecte 6 à y x,y = 5,6 # affecte en une seule instruction 5 à x et 6 à y | Cette particularité reviendra lorsque les fonctions seront décrites puisqu’il est possible qu’une fonction retourne plusieurs résultats comme la fonction divmod illustrée par le programme suivant. x,y = divmod (17,5) print x,y # affiche 3 2 print "17 / 5 = 5 * ", x, " + ",y # affiche 17 / 5 = 5 * 3 + 2 Le langage Python offre la possibilité d’effectuer plusieurs affectations sur la même ligne. Dans l’exemple qui suit, le couple (5,5) est affecté à la variable point, puis le couple x, y reçoit les deux valeurs du T-uple point. 2.4.5 Type long Le type long permet d’écrire des entiers aussi grands que l’on veut. Le langage Python passe automatiquement du type int à long lorsque le nombre considéré devient trop grand. Ils se comportent de la même manière excepté que les opérations sur des types long sont plus longues en temps d’exécution . | | <type ’int’> 536870912 <type ’int’> 1073741824 <type ’long’> 2147483648 <type ’long’> 4294967296 | 2.4.6 Ensemble Le langage Python définit également ce qu’on appelle un ensemble. Il est défini par les classes set de type modifiable et la classe frozenset de type immuable. Ils n’acceptent que des types identiques et offrent la plupart des opérations liées aux ensembles comme l’intersection, l’union. D’un usage moins fréquent, ils ne seront pas plus détaillés . print set ( (1,2,3) ) & set ( (2,3,5) ) # construit l’intersection qui est set([2, 3]) | Ce chapitre a présenté les différents types de variables définis par le langage Python pour manipuler des données ainsi que les opérations possibles avec ces types de données. Le chapitre suivant va présenter les tests, les boucles et les fonctions qui permettent de réaliser la plupart des programmes informatiques.
Chapitre 3 Syntaxe du langage Python (boucles, tests, fonctions) Avec les variables, les boucles et les fonctions, on connaît suffisamment d’éléments pour écrire des programmes. Le plus difficile n’est pas forcément de les comprendre mais plutôt d’arriver à découper un algorithme complexe en utilisant ces briques élémentaires. C’est l’objectif des chapitres centrés autour des exercices. Toutefois, même si ce chapitre présente les composants élémentaires du langage, l’aisance qu’on peut acquérir en programmation vient à la fois de la connaissance du langage mais aussi de la connaissance d’algorithmes standards comme celui du tri ou d’une recherche dichotomique. C’est cette connaissance tout autant que la maîtrise d’un langage de programmation qui constitue l’expérience en programmation. 3.1 Les trois concepts des algorithmes la séquence | enchaînement des instructions les unes à la suite des autres : passage d’une instruction à la suivante | le saut | passage d’une instruction à une autre qui n’est pas forcément la suivante (c’est une rupture de séquence) | le test | choix entre deux instructions | Le saut n’apparaît plus de manière explicite dans les langages évolués car il est une source fréquente d’erreurs. Il intervient dorénavant de manière implicite au travers des boucles qui combinent un saut et un test comme le montre l’exemple suivant : Version avec boucles : initialisation de la variable moy à 0 faire pour i allant de 1 à N moy reçoit moy + ni moy reçoit moy / N Version équivalente avec sauts : ligne 1 : initialisation de la variable moy à 0 ligne 2 : initialisation de la variable i à 1 ligne 3 : moy reçoit moy + ni ligne 4 : i reçoit i + 1 ligne 5 : si i est inférieur ou égal à N alors aller à la ligne 3 ligne 6 : moy reçoit moy / N Tout programme peut se résumer à ces trois concepts. Chaque langage les met en place avec sa propre syntaxe et parfois quelques nuances mais il est souvent facile de les reconnaître même dans des langages inconnus. Le calcul d’une somme décrit plus haut et écrit en Python correspond à l’exemple suivant : moy = 0 for i in range(1,N+1): moy += n [i] moy /= N | # de 1 à N+1 exclu --> de 1 à N inclus | Le premier élément de cette syntaxe est constitué de ses mots-clés (for et in) (voir également table 3.1) et des symboles (=, +=, /=, [, ], (, ), :) (voir table 3.2) La fonction iskeyword du module keyword permet de savoir si un mot-clé donné fait partie du langage Python. import keyword print keyword.iskeyword("for") | # affiche True | print keyword.iskeyword("until") | # affiche False | and | as | assert | break | class | continue | def | del | elif | else | except | exec | finally | for | global | if | import | in | is | lambda | not | or | pass | print | raise | return | try | while | with | yield | | | | | | | | | | | Table 3.1 : Mots-clés du langage Python, voir également la page Table 3.2 : Symbole du langage Python, certains ont plusieurs usages comme : qui est utilisé à chaque test ou boucle et qui permet aussi de déterminer une plage d’indices dans un tableau. L’opérateur<>est identique à ! = mais est déclaré obsolète (deprecated) : il est susceptible de disparaître dans une prochaine version de Python. Remarque 3.1 : syntaxe et espaces Les espaces entre les mots-clés et les symboles ont peu d’importance, il peut n’y en avoir aucun comme dix. Les espaces servent à séparer un mot-clé, un nombre d’une variable. 3.2 Tests 3.2.1 Définition et syntaxe Définition 3.2 : test Les tests permettent d’exécuter des instructions différentes selon la valeur d’une condition logique. syntaxe 3.3 : test if condition1 : instruction1 instruction2 else : instruction3 instruction4 La clause else est facultative. Lorsque la condition condition1 est fausse et qu’il n’y a aucune instruction à exécuter dans ce cas, la clause else est inutile. La syntaxe du test devient : if condition1 : instruction1 instruction2 S’il est nécessaire d’enchaîner plusieurs tests d’affilée, il est possible de condenser l’écriture avec le mot-clé elif : if condition1 : instruction1 instruction2 elif condition2 : instruction3 instruction4 elif condition3 : instruction5 instruction6 else : instruction7 instruction8 Remarque 3.4 : indentation x = 1 if x > 0 : signe = 1 print "le nombre est positif" else : signe = -1 print "le nombre est négatif" # ligne mal indentée print "signe = ", signe | Une ligne est mal indentée (print "le nombre est négatif"). Elle ne devrait être exécutée que si la condition x > 0 n’est pas vérifiée. Le fait qu’elle soit alignée avec les premières instructions du programme fait que son exécution n’a plus rien à voir avec cette condition. La programme répond de manière erronée : le nombre est positif le nombre est négatif signe = 1 Dans certains cas, l’interpréteur Python ne sait pas à quel bloc attacher une instruction, c’est le cas de l’exemple suivant, la même ligne a été décalée de deux espaces, ce qui est différent de la ligne qui précède et de la ligne qui suit. x = 1 if x > 0 : signe = 1 print "le nombre est positif" else : signe = -1 print "le nombre est négatif" print "signe = ", signe | # ligne mal indentée | L’interpréteur retourne l’erreur suivante : File "", line 7 print "le nombre est négatif" ^ IndentationError: unindent does not match any outer indentation level | 3.2.2 Comparaisons possibles Les comparaisons possibles entre deux entités sont avant tout numériques mais ces opérateurs peuvent être définis pour tout type (voir chapitre 4), notamment sur les chaînes de caractères pour lesquelles les opérateurs de comparaison transcrivent l’ordre alphabétique. | | is , notis | xisy vérifie que x et y sont égaux, notis, différents, l’opérateur is est différent de l’opérateur ==, il est expliqué au paragraphe 2.3.2 (page 47). | in, notin | appartient, n’appartient pas | 3.2.3 Opérateurs logiques Il existe trois opérateurs logiques qui combinent entre eux les conditions. not | négation | and | et logique | or | ou logique | Remarque 3.5 : priorité des opérations 3.2.4 Ecriture condensée Il existe deux écritures condensées de tests. La première consiste à écrire un test et l’unique instruction qui en dépend sur une seule ligne. if condition : instruction1 else : instruction2 | Ce code peut tenir en deux lignes : | if condition : instruction1 else : instruction2 | Le second cas d’écriture condensée concerne les comparaisons enchaînées. Le test if3 < xandx < 5 : instruction peut être condensé par if3 < x < 5 : instruction. Il est ainsi possible de juxtaposer autant de comparaisons que nécessaire : if3 < x < y < 5 : instruction. Le mot-clé in permet également de condenser certains tests lorsque la variable à tester est entière. ifx == 1orx == 6orx == 50 : peut être résumé simplement par ifxin (1,6,50) :. 3.2.5 Exemple L’exemple suivant associe à la variable signe le signe de la variable x. x = -5 if x < 0 : signe = -1 elif x == 0 : signe = 0 else : signe = 1 | Son écriture condensée lorsqu’il n’y a qu’une instruction à exécuter : | x = -5 if x < 0 : signe = -1 elif x == 0 : signe = 0 else : signe = 1 | Le programme suivant saisit une ligne au clavier et dit si c’est "oui" ou "non" qui a été saisi. s = raw_input ("dites oui : ") # voir remarque suivante if s == "oui" or s [0:1] == "o" or s [0:1] == "O" or s == "1" : print "oui" else : print "non" | Remarque 3.6 : fonction raw_input import Tkinter def question (legende) : reponse = [""] root = () root.title ("pseudo raw_input") Tkinter.Label (text = legende).pack (side = ) s = Tkinter.Entry (text= "def", width=80) s.pack (side = ) def rget () : reponse [0] = s.get () root.destroy () Tkinter.Button (text = "ok", command = rget).pack (side = ) root.mainloop () return reponse [0] print "reponse ", question ("texte de la question") | Figure 3.1 : Fenêtre apparaissant lors de l’exécution du programme page 62 proposant une version graphique de la fonction raw_input. Remarque 3.7 : ifNone, if0 L’écriture de certains tests peut encore être réduite lorsqu’on cherche à comparer une variable entière, booléenne ou None comme le précise le tableau 3.2.5. type | test | test équivalent | bool | if v : | if v == True : | bool | if not v : | if v == False : | int | if v : | if v!= 0 : | int | if not v : | if v == 0 : | float | if v : | if v!= 0.0 : | float | if not v : | if v == 0.0 : | None | if v : | if v!= None : | None | if not v : | if v == None : | Table 3.3 : Ce tableau précise le sens de certains tests lorsque leur écriture est tronquée. On suppose que v est une variable. 3.2.6 Passer, instruction pass Dans certains cas, aucune instruction ne doit être exécutée même si un test est validé. En Python, le corps d’un test ne peut être vide, il faut utiliser l’instruction pass. Lorsque celle-ci est manquante, Python affiche un message d’erreur. signe = 0 x = 0 if x < 0 : signe = -1 elif x == 0: pass # signe est déjà égal à 0 else : signe = 1 | Dans ce cas précis, si l’instruction pass est oubliée, l’interpréteur Python génère l’erreur suivante : File "", line 6 else : ^ IndentationError: expected an indented block | 3.3 Boucles Les boucles permettent de répéter une séquence d’instructions tant qu’une certaine condition est vérifiée. Le langage Python propose deux types de boucles. La boucle while suit scrupuleusement la définition 3.8. La boucle for est une boucle while déguisée (voir paragraphe 3.3.2), elle propose une écriture simplifiée pour répéter la même séquence d’instructions pour tous les éléments d’un ensemble. 3.3.1 Boucle while 3.3.1.1 Syntaxe et exemples L’implémentation d’une boucle de type while suit le schéma d’écriture suivant : syntaxe 3.9 : boucle while while cond : instruction 1 instruction n Où cond est une condition qui détermine la poursuite de la répétition des instructions incluses dans la boucle. Tant que celle-ci est vraie, les instructions 1 à n sont exécutées. Tout comme les tests, la remarque 3.4 concernant l’indentation reste vraie. Le décalage des lignes d’un cran vers la droite par rapport à l’instruction while permet de les inclure dans la boucle comme le montre l’exemple suivant. n = 0 while n < 3: print "à l’intérieur ", n n += 1 print "à l’extérieur ", n | Résultat : | à l’intérieur 0 à l’intérieur 1 à l’intérieur 2 à l’extérieur 3 | 3.3.1.2 Condition Les conditions suivent la même syntaxe que celles définies lors des tests (voir paragraphes 3.2.2 et 3.2.3). A moins d’inclure l’instruction break qui permet de sortir prématurément d’une boucle, la condition qui régit cette boucle doit nécessairement être modifiée à l’intérieur de celle-ci. Dans le cas contraire, on appelle une telle boucle une boucle infinie puisqu’il est impossible d’en sortir. L’exemple suivant contient une boucle infinie car le symbole = est manquant dans la dernière instruction. La variable n n’est jamais modifiée et la condition n < 3 toujours vraie. n = 0 while n < 3 : print n | # n n’est jamais modifié, l’instruction correcte serait n += 1 | 3.3.2 Boucle for 3.3.2.1 Syntaxe et exemples L’implémentation d’une boucle de type for suit le schéma d’écriture suivant : syntaxe 3.10 : boucle for for x in set : instruction 1 instruction n Où x est un élément de l’ensemble set. Les instructions 1 à n sont exécutées pour chaque élément x de l’ensemble set. Cet ensemble peut être une chaîne de caractères, un T-uple, une liste, un dictionnaire ou tout autre type incluant des itérateurs qui sont présentés au chapitre 4 concernant les classes (page 106). Tout comme les tests, la remarque 3.4 concernant l’indentation reste vraie. L’exemple suivant affiche tous les éléments d’un T-uple à l’aide d’une boucle for. t = (1,2,3,4) for x in t: | # affiche les nombres 1,2,3,4 | print x | # chacun sur une ligne différente | Lors de l’affichage d’un dictionnaire, les éléments n’apparaissent pas triés ni dans l’ordre dans lequel ils y ont été insérés. L’exemple suivant montre comment afficher les clés et valeurs d’un dictionnaire dans l’ordre croissant des clés. d = { 1:2, 3:4, 5:6, 7:-1, 8:-2 } print d # affiche le dictionnaire {8: -2, 1: 2, 3: 4, 5: 6, 7: -1} k = d.keys () print k # affiche les clés [8, 1, 3, 5, 7] k.sort () print k # affiche les clés triées [1, 3, 5, 7, 8] for x in k: # affiche les éléments du dictionnaire print x,":",d [x] # triés par clés croissantes | Le langage Python propose néanmoins la fonction sorted qui réduit l’exemple suivant en trois lignes : d = { 1:2, 3:4, 5:6, 7:-1, 8:-2 } for x in sorted(d): # pour les clés dans l’ordre croissant print x,":",d [x] sum = 0 N = 10 for n in range(0,N): | # ou for n in xrange(0,N): (plus rapide) | sum += n | # additionne tous les entiers compris entre 0 et N-1 | Ou encore pour une liste quelconque : l = [ 4, 5, 3, -6, 7, 9] sum = 0 for n in range(0,len (l)): # ou for n in xrange(0,len (l)) : (plus rapide) sum += l [n] # additionne tous les éléments de l 3.3.2.2 Listes et boucle for Le paragraphe 2.3.1.4 a montré comment le mot-clé for peut être utilisé pour simplifier la création d’une liste à partir d’une autre. La syntaxe suit le schéma suivant : syntaxe 3.11 : boucle for [ expression for x in ensemble ] Où expression est une expression numérique incluant ou non x, la variable de la boucle, ensemble est un ensemble d’éléments, T-uple, liste, dictionnaire. Cette syntaxe permet de résumer en une ligne la création de la séquence y du programme suivant. y = list () for i in range(0,5) : y.append (i+1) print y | # affiche [1,2,3,4,5] | Les trois lignes du milieu sont maintenant résumées en une seule : y = [ i+1 for i in range(0,5)] # résume trois lignes du programme précédent print y # affiche [1,2,3,4,5] Un autre exemple de cette syntaxe réduite a été présenté au paragraphe 2.3.1.4 page 46. Cette écriture condensée est bien souvent plus lisible même si tout dépend des préférences de celui qui programme. 3.3.2.3 Itérateurs Toute boucle for peut s’appliquer sur un objet muni d’un itérateur tels que les chaînes de caractères, T-uples, les listes, les dictionnaires. d = ["un", "deux", "trois"] for x in d: print x # affichage de tous les éléments de d d = ["un", "deux", "trois"] it = iter (d) | # obtient un itérateur sur d | while True: try: x = () | # obtient l’élément suivant, s’il n’existe pas | except StopIteration: break | # déclenche une exception | print x | # affichage de tous les éléments de d | 3.3.2.4 Plusieurs variables de boucles Jusqu’à présent, la boucle for n’a été utilisée qu’avec une seule variable de boucle, comme dans l’exemple suivant où on parcourt une liste de T-uple pour les afficher. d = [ (1,0,0), (0,1,0), (0,0,1) ] for v in d: print v Lorsque les éléments d’un ensemble sont des T-uples, des listes ou des dictionnaires composés de taille fixe, il est possible d’utiliser une notation qui rappelle les affectations multiples (voir paragraphe 2.4.4). L’exemple précédent devient dans ce cas : d = [ (1,0,0), (0,1,0), (0,0,1) ] for x,y,z in d: print x,y,z Cette écriture n’est valable que parce que chaque élément de la liste d est un T-uple composé de trois nombres. Lorsqu’un des éléments est de taille différente à celle des autres, comme dans l’exemple suivant, une erreur survient. d = [ (1,0,0), (0,1,0,6), (0,0,1) ] # un élément de taille quatre for x,y,z in d: print x,y,z Ce programme génère l’erreur suivante : Traceback (most recent call last): File "c:\temp\", line 2, in -toplevelfor x,y,z in d: print x,y,z ValueError: unpack tuple of wrong size | Cette syntaxe est très pratique associée à la fonction zip (voir paragraphe 2.3.1.5). Il est alors possible de parcourir plusieurs séquences (T-uple, liste, dictionnaire) simultanément. a = range(0,5) b = [x**2 for x in a] for x,y in zip (a,b): print y, " est le carré de ", x # affichage à droite | | 0 est le carré de 0 1 est le carré de 1 4 est le carré de 2 9 est le carré de 3 16 est le carré de 4 | 3.3.3 Ecriture condensée d = ["un", "deux", "trois"] for x in d: print x | # une seule instruction | Il existe peu de cas où la boucle while s’écrit sur une ligne car elle inclut nécessairement une instruction permettant de modifier la condition d’arrêt. d = ["un", "deux", "trois"] i = 0 while d [i] != "trois" : i += 1 print "trois a pour position ", i 3.3.4 Pilotage d’une boucle 3.3.4.1 Passer à l’itération suivante : continue Pour certains éléments d’une boucle, lorsqu’il n’est pas nécessaire d’exécuter toutes les instructions, il est possible de passer directement à l’élément suivant ou l’itération suivante. Le programme suivant utilise la crible d’Eratosthène pour dénicher tous les nombres premiers compris entre 1 et 99. d = dict () for i in range(1,100): # d [i] est vrai si i est un nombre premier d [i] = True # au début, comme on ne sait pas, on suppose # que tous les nombres sont premiers for i in range(2,100): # si d [i] est faux, if not d [i]: continue # les multiples de i ont déjà été cochés # et peut passer à l’entier suivant for j in range (2,100): if i*j < 100: d [i*j] = False # d [i*j] est faux pour tous les multiples de i # inférieurs à 100 print "liste des nombres premiers" for i in d: if d [i]: print i | Ce programme est équivalent au suivant : d = dict () for i in range(1,100): d [i] = True for i in range(2,100): if d [i]: for j in range (2,100): if i*j < 100 : d [i*j] = False print "liste des nombres premiers" for i in d: if d [i]: print i 3.3.4.2 Sortie prématurée : break Lors de l’écriture d’une boucle while, il n’est pas toujours adéquat de résumer en une seule condition toutes les raisons pour lesquelles il est nécessaire d’arrêter l’exécution de cette boucle. De même, pour une boucle for, il n’est pas toujours utile de visiter tous les éléments de l’ensemble à parcourir. C’est le cas par exemple lorsqu’on recherche un élément, une fois qu’il a été trouvé, il n’est pas nécessaire d’aller plus loin. L’instruction break permet de quitter l’exécution d’une boucle. l = [6,7,5,4,3] n = 0 c = 5 for x in l: if x == c: break # l’élément a été trouvé, on sort de la boucle n += 1 # si l’élément a été trouvé, cette instruction # n’est pas exécutée print "l’élément ",c, " est en position ", print n # affiche l’élément 5 est en position 2 | Si deux boucles sont imbriquées, l’instruction break ne sort que de la boucle dans laquelle elle est insérée. L’exemple suivant vérifie si un entier est la somme des carrés de deux entiers compris entre 1 et 20. set = range (1,21) n = 53 for x in set: for y in set: c = x*x + y*y if c == n: break if c == n: break # cette seconde instruction break est nécessaire # pour sortir de la seconde boucle # lorsque la solution a été trouvée if c == n: # le symbole \ permet de passer à la ligne sans changer d’instruction print n, " est la somme des carrés de deux entiers :", \ x, "*", x, "+", y, "*", y, "=", n else: print n, " n’est pas la somme des carrés de deux entiers" | Le programme affiche : 53 est la somme des carrés de deux entiers : 2 * 2 + 7 * 7 = 53 3.3.4.3 Fin normale d’une boucle : else L = [6,7,5,4,3] n = 0 c = 1 for x in L : if x == c : print "l’élément ", c, " est en position ", n | break n += 1 else: print "aucun élément ", c, " trouvé" # affiche aucun élément 1 trouvé | Les lignes dépendant de la clause else seront exécutées dans tous les cas où l’exécution de la boucle n’est pas interrompue par une instruction break ou une instruction return ou par la levée d’une exception . 3.3.4.4 Suppression ou ajout d’éléments lors d’une boucle En parcourant la liste en se servant des indices, il est possible de supprimer une partie de cette liste. Il faut néanmoins faire attention à ce que le code ne produise pas d’erreur comme c’est le cas pour le suivant. La boucle for parcourt la liste range(0,len(li)) qui n’est pas modifiée en même temps que l’instruction delli[i : i + 2]. li = range (0,10) print li # affiche [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] for i in range (0, len (li)): if i == 5 : del li [i:i+2] print li [i] # affiche successivement 0, 1, 2, 3, 4, 7, 8, 9 et # produit une erreur print li | Le programme suivant marche parfaitement puisque cette fois-ci la boucle parcourt la liste li. En revanche, pour la suppression d’une partie de celle-ci, il est nécessaire de conserver en mémoire l’indice de l’élément visité. C’est le rôle de la variable i. li = range (0,10) print li # affiche [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] i = 0 for t in li : if i == 5 : del li [i:i+2] i = i+1 print t # affiche successivement 0, 1, 2, 3, 4, 5, 8, 9 print li # affiche [0, 1, 2, 3, 4, 7, 8, 9] li = range (0,10) print li | # affiche [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] | for i in li : if i == 5 : del i print li | # affiche [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] | On pourrait construire des exemples similaires dans le cadre de l’ajout d’un élément à la liste. Il est en règle générale déconseillé de modifier une liste, un dictionnaire pendant qu’on le parcourt. Malgré tout, si cela s’avérait indispensable, il convient de faire plus attention dans ce genre de situations. 3.4 Fonctions Les fonctions sont des petits programmes qui effectuent des tâches plus précises que le programme entier. On peut effectivement écrire un programme sans fonction mais ils sont en général illisibles. Utiliser des fonctions implique de découper un algorithme en tâches élémentaires. Le programme final est ainsi plus facile à comprendre. Un autre avantage est de pouvoir plus facilement isoler une erreur s’il s’en produit une : il suffit de tester une à une les fonctions pour déterminer laquelle retourne un mauvais résultat. L’avantage le plus important intervient lorsqu’on doit effectuer la même chose à deux endroits différentes d’un programme : une seule fonction suffit et elle sera appelée à ces deux endroits . 3.4.1 Définition, syntaxe Définition 3.12 : fonction Une fonction est une partie d’un programme - ou sous-programme - qui fonctionne indépendamment du reste du programme. Elle reçoit une liste de paramètres et retourne un résultat. Le corps de la fonction désigne toute instruction du programme qui est exécutée si la fonction est appelée. Lorsqu’on écrit ses premiers programme, on écrit souvent des fonctions plutôt longues avant de s’apercevoir que certains parties sont identiques ailleurs. On extrait donc la partie répétée pour en faire une fonction. Avec l’habitude, on finit par écrire des fonctions plus petites et réutilisables. def fonction_nom (par_1, , par_n) : instruction_1 instruction_n return res_1, , res_n fonction_nom est le nom de la fonction, il suit les mêmes règles que le nom des variables. par_1 à par_n sont les noms des paramètres et res_1 à res_n sont les résultats retournés par la fonction. Les instructions associées à une fonction doivent être indentées par rapport au mot-clé def. S’il n’y a aucun résultat, l’instruction return est facultative ou peut être utilisée seule sans être suivie par une valeur ou une variable. Cette instruction peut apparaître plusieurs fois dans le code de la fonction mais une seule d’entre elles sera exécutée. A partir de ce moment, toute autre instruction de la fonction sera ignorée. Pour exécuter une fonction ainsi définie, il suffit de suivre la syntaxe suivante : syntaxe 3.14 : fonction, appel x_1, , x_n = fonction_nom (valeur_1, valeur_2, , valeur_n) Où fonction_nom est le nom de la fonction, valeur_1 à valeur_n sont les noms des paramètres, x_1 à x_n reçoivent les résultats retournés par la fonction. Cette affectation est facultative. Si on ne souhaite pas conserver les résultats, on peut donc appeler la fonction comme suit : fonction_nom (valeur_1, valeur_2, , valeur_n) Lorsqu’on commence à programmer, il arrive parfois qu’on confonde le rôle des motsclés print et return. Dans ce cas, il faut se reporter à la remarque 10.3 page 255. 3.4.2 Exemple Le programme suivant utilise deux fonctions. La première convertit des coordonnées cartésiennes en coordonnées polaires. Elle prend deux réels en paramètres et retourne deux autres réels. La seconde fonction affiche les résultats de la première pour tout couple de valeurs (x,y). Elle ne retourne aucun résultat. import math def coordonnees_polaires (x,y): # compte des signes de x et y return rho, theta def affichage (x,y): r,t = coordonnees_polaires(x,y) print "cartésien (%f,%f) --> polaire (%f,%f degrés)" \ % (x,y,r,math.degrees(t)) affichage (1,1) affichage (0.5,1) affichage (-0.5,1) affichage (-0.5,-1) affichage (0.5,-1) | Le programme affiche les lignes suivantes : cartésien (1.000000,1.000000) --> polaire (1.414214,45.000000 degrés) cartésien (0.500000,1.000000) --> polaire (1.118034,63.434949 degrés) cartésien (-0.500000,1.000000) --> polaire (1.118034,116.565051 degrés) cartésien (-0.500000,-1.000000) --> polaire (1.118034,-116.565051 degrés) cartésien (0.500000,-1.000000) --> polaire (1.118034,-63.434949 degrés) 3.4.3 Paramètres avec des valeurs par défaut Lorsqu’une fonction est souvent appelée avec les mêmes valeurs pour ses paramètres, il est possible de spécifier pour ceux-ci une valeur par défaut. syntaxe 3.15 : fonction, valeur par défaut def fonction_nom (param_1, param_2 = valeur_2, , param_n = valeur_n): Où fonction_nom est le nom de la fonction. param_1 à param_n sont les noms des paramètres, valeur_2 à valeur_n sont les valeurs par défaut des paramètres param_2 à param_n. La seule contrainte lors de cette définition est que si une valeur par défaut est spécifiée pour un paramètre, alors tous ceux qui suivent devront eux aussi avoir une valeur par défaut. Exemple : def commander_carte_orange (nom, prenom, paiement = "carte", nombre = 1, zone = 2): print "nom : ", nom print "prénom : ", prenom print "paiement : ", paiement print "nombre : ", nombre print "zone :", zone commander_carte_orange ("Dupré", "Xavier", "chèque") # les autres paramètres nombre et zone auront pour valeur # leurs valeurs par défaut def commander_carte_orange (nom, prenom, paiement = "carte", nombre = 1, zone): print "nom : ", nom # | Il déclenche l’erreur suivante : File "", line 1 def commander_carte_orange (nom, prenom, paiement = "carte", nombre = 1, zone): SyntaxError: non-default argument follows default argument | Remarque 3.16 : valeurs par défaut de type modifiable Les valeurs par défaut de type modifiable (liste, dictionnaire, classes) peuvent introduire des erreurs inattendues dans les programmes comme le montre l’exemple suivant : def fonction (l = [0,0]) : l [0] += 1 return l print fonction () # affiche [1,0] : résultat attendu print fonction () # affiche [2,0] : résultat surprenant print fonction ( [0,0]) # affiche [1,0] : résultat attendu | L’explication provient du fait que la valeur par défaut est une liste qui n’est pas recréée à chaque appel : c’est la même liste à chaque fois que la fonction est appelée sans paramètre. Pour remédier à cela, il faudrait écrire : import copy def fonction (l = [0,0]) : l = (l) l [0] += 1 return l | L’exercice 12.4.7 (page 316) propose un exemple plus complet, voire retors. 3.4.4 Ordre des paramètres Le paragraphe 3.4.1 a présenté la syntaxe d’appel a une fonction. Lors de l’appel, le nom des paramètres n’intervient plus, supposant que chaque paramètre reçoit pour valeur celle qui a la même position que lui lors de l’appel à la fonction. Il est toutefois possible de changer cet ordre en précisant quel paramètre doit recevoir quelle valeur. syntaxe 3.17 : fonction, ordre des paramètres x_1, , x_n = fonction_nom (param_1 = valeur_1, , param_n = valeur_n) Exemple : def identite (nom, prenom): print "nom : ", nom, " prénom : ", prenom identite("Xavier", "Dupré") # nom : Xavier prénom : Dupré identite(prenom = "Xavier", nom = "Dupré") # nom : Dupré prénom : Xavier | Cette possibilité est intéressante surtout lorsqu’il y a de nombreux paramètres par défaut et que seule la valeur d’un des derniers paramètres doit être changée. def commander_carte_orange (paiement = "carte", nombre = 1, zone = 2): print "paiement : ", paiement print "nombre : ", nombre print "zone :", zone commander_carte_orange (zone = 5) # seule la valeur par défaut # du paramètre zone sera changée | 3.4.5 Surcharge de fonction Contrairement à d’autres langages, Python n’autorise pas la surcharge de fonction. Autrement dit, il n’est pas possible que plusieurs fonctions portent le même nom même si chacune d’entre elles a un nombre différent de paramètres. def fonction (a,b): return a + b def fonction (a,b,c): return a + b + c print fonction (5,6) print fonction (5,6,7) Le petit programme précédent est syntaxiquement correct mais son exécution génère une erreur parce que la seconde définition de la fonction fonction efface la première. Traceback (most recent call last): File "", line 7, in ? print fonction (5,6) TypeError: fonction() takes exactly 3 arguments (2 given) | 3.4.6 Commentaires Le langage Python propose une fonction help qui retourne pour chaque fonction un commentaire ou mode d’emploi qui indique comment se servir de cette fonction. L’exemple suivant affiche le commentaire associé à la fonction round. Help on built-in function round: round( ) round(number[, ndigits]) -> floating point number This always returns a floating point number. Precision may be negative. | Lorsqu’on utilise cette fonction help sur la fonction coordonnees_polaires définie dans l’exemple du paragraphe précédent 3.4.2, le message affiché n’est pas des plus explicites. >>> help (coordonnees_polaires) Help on function coordonnees_polaires in module __main__: coordonnees_polaires(x, y) Pour changer ce message, il suffit d’ajouter en première ligne du code de la fonction une chaîne de caractères. import math def coordonnees_polaires (x,y): """convertit des coordonnées cartésiennes en coordonnées polaires (x,y) --> (pho,theta)""" rho = (x*x+y*y) theta = math.atan2 (y,x) return rho, theta help (coordonnees_polaires) | Le programme affiche alors le message d’aide suivant nettement plus explicite : Help on function coordonnees_polaires in module __main__: coordonnees_polaires(x, y) convertit des coordonnées cartésiennes en coordonnées polaires (x,y) --> (pho,theta) | Il est conseillé d’écrire ce commentaire pour toute nouvelle fonction avant même que son corps ne soit écrit. L’expérience montre qu’on oublie souvent de l’écrire après. 3.4.7 Paramètres modifiables Les paramètres de types immuables et modifiables se comportent de manières différentes à l’intérieur d’une fonction. Ces paramètres sont manipulés dans le corps de la fonction, voire modifiés parfois. Selon le type du paramètre, ces modifications ont des répercussions à l’extérieur de la fonction. L’exemple suivant utilise une fonction somme_n_premier_terme qui modifie ces deux paramètres. Le premier, n, est immuable, sa modification n’a aucune incidence sur la variable nb. En revanche, le premier élément du paramètre liste reçoit la valeur 0. Le premier élément de la liste l n’a plus la même valeur après l’appel de la fonction somme_n_premier_terme que celle qu’il avait avant. def somme_n_premier_terme(n,liste): """calcul la somme des n premiers termes d’une liste""" somme = 0 for i in liste: somme += i n -= 1 # modification de n (type immuable) if n <= 0: break liste[0] = 0 # modification de liste (type modifiable) return somme | l = [1,2,3,4] nb = 3 print "avant la fonction ",nb,l # affiche avant la fonction 3 [1, 2, 3, 4] s = somme_n_premier_terme (nb,l) print "après la fonction ",nb,l # affiche après la fonction 3 [0, 2, 3, 4] print "somme : ", s # affiche somme : 6 La liste l est modifiée à l’intérieur de la fonction somme_n_premier_terme comme l’affichage suivant le montre. En fait, à l’intérieur de la fonction, la liste l est désignée par l’identificateur liste, c’est la même liste. La variable nb est d’un type immuable. Sa valeur a été recopiée dans le paramètre n de la fonction somme_n_premier_terme. Toute modification de n à l’intérieur de cette fonction n’a aucune répercussion à l’extérieur de la fonction. Remarque 3.18 : passage par adresse Dans l’exemple précédent, il faut faire distinguer le fait que la liste passée en paramètre ne soit que modifiée et non changée. L’exemple suivant inclut une fonction qui affecte une nouvelle valeur au paramètre liste sans pour autant modifier la liste envoyée en paramètre. def fonction (liste): liste = [] liste = [0,1,2] print liste # affiche [0,1,2] fonction (liste) | Il faut considérer dans ce programme que la fonction fonction reçoit un paramètre appelé liste mais utilise tout de suite cet identificateur pour l’associer à un contenu différent. L’identificateur liste est en quelque sorte passé du statut de paramètre à celui de variable locale. La fonction associe une valeur à liste - ici, une liste vide sans toucher à la valeur que cet identificateur désignait précédemment. Le programme qui suit est différent du précédent mais produit les mêmes effets. Ceci s’explique par le fait que le mot-clé del ne supprime pas le contenu d’une variable mais seulement son identificateur. Le langage Python détecte ensuite qu’un objet n’est plus désigné par aucun identificateur pour le supprimer. Cette remarque est à rapprocher de celles du paragraphe 4.6. def fonction (liste): del liste liste = [0,1,2] print liste # affiche [0,1,2] fonction (liste) print liste # affiche [0,1,2] | Le programme qui suit permet cette fois-ci de vider la liste liste passée en paramètre à la fonction fonction. La seule instruction de cette fonction modifie vraiment le contenu désigné par l’identificateur liste et cela se vérifie après l’exécution de cette fonction. def fonction (liste): del liste[0:len(liste)] # on peut aussi écrire : liste[:] = [] | liste = [0,1,2] print liste | # affiche [0,1,2] | fonction (liste) print liste | # affiche [] | 3.4.8 Fonction récursive Définition 3.19 : fonction récursive Une fonction récursive est une fonction qui s’appelle elle-même. La fonction récursive la plus fréquemment citée en exemple est la fonction factorielle. Celle-ci met en évidence les deux composantes d’une fonction récursive, la récursion proprement dite et la condition d’arrêt. def factorielle(n): if n == 0 : return 1 | La dernière ligne de la fonction factorielle est la récursion tandis que la précédente est la condition d’arrêt, sans laquelle la fonction ne cesserait de s’appeler, empêchant le programme de terminer son exécution. Si celle-ci est mal spécifiée ou absente, l’interpréteur Python affiche une suite ininterrompue de messages. Python n’autorise pas plus de 1000 appels récursifs : factorielle(999) provoque nécessairement une erreur d’exécution même si la condition d’arrêt est bien spécifiée. Traceback (most recent call last): File "", line 5, in <module> factorielle(999) File "", line 3, in factorielle else : return n * factorielle(n-1) File "", line 3, in factorielle else : return n * factorielle(n-1) | La liste des messages d’erreurs est aussi longue qu’il y a eu d’appels à la fonction récursive. Dans ce cas, il faut transformer cette fonction en une fonction non récursive équivalente, ce qui est toujours possible. def factorielle_non_recursive (n) : r = 1 for i in range (2, n+1) : r *= i return r | 3.4.9 Portée 3.4.9.1 Portée des variables, des paramètres Lorsqu’on définit une variable, elle n’est pas utilisable partout dans le programme. Par exemple, elle n’est pas utilisable avant d’avoir été déclarée au moyen d’une affectation. Le court programme suivant déclenche une erreur. print x # déclenche une erreur Traceback (most recent call last): File "", line 1, in <module> print x NameError: name ’x’ is not defined | Il est également impossible d’utiliser une variable à l’extérieur d’une fonction où elle a été déclarée. Plusieurs fonctions peuvent ainsi utiliser le même nom de variable sans qu’à aucun moment, il n’y ait confusion. Le programme suivant déclenche une erreur identique à celle reproduite ci-dessus. def portee_variable(x): var = x print var portee_variable(3) # la fonction portee_variable | Définition 3.20 : portée d’une variable La portée d’une variable associée à un identificateur recouvre la portion du programme à l’intérieur de laquelle ce même identificateur la désigne. Ceci implique que, dans cette portion de code, aucune autre variable, aucune autre fonction, aucune autre classe, ne peut porter le même identificateur. Une variable n’a donc d’existence que dans la fonction dans laquelle elle est déclarée. On appelle ce type de variable une variable locale. Par défaut, toute variable utilisée dans une fonction est une variable locale. Définition 3.21 : variable locale Une variable locale est une variable dont la portée est réduite à une fonction. Par opposition aux variables locales, on définit les variables globales qui sont déclarées à l’extérieur de toute fonction. Définition 3.22 : variable globale Une variable globale est une variable dont la portée est l’ensemble du programme. L’exemple suivant mélange variable locale et variable globale. L’identificateur n est utilisé à la fois pour désigner une variable globale égale à 1 et une variable locale égale à 1. A l’intérieur de la fonction, n désigne la variable locale égale à 2. A l’extérieur de la fonction, n désigne la variable globale égale à 1. n = 1 | # déclaration d’une variable globale | def locale_globale(): n = 2 | # déclaration d’une variable locale | print n | # affiche le contenu de la variable locale | print n | # affiche 1 | locale_globale() | # affiche 2 | print n | # affiche 1 | Il est possible de faire référence aux variables globales dans une fonction par l’intermédiaire du mot-clé global. Celui-ci indique à la fonction que l’identificateur n n’est plus une variable locale mais désigne une variable globale déjà déclarée. n = 1 | # déclaration d’une variable globale | def locale_globale(): | # cette ligne indique que n désigne la variable globale | n = 2 | # change le contenu de la variable globale | print n | # affiche le contenu de la variable globale | print n | # affiche 1 | locale_globale() | # affiche 2 | print n | # affiche 2 | Cette possibilité est à éviter le plus possible car on peut considérer que locale_globale est en fait une fonction avec un paramètre caché. La fonction locale_globale n’est plus indépendante des autres fonctions puisqu’elle modifie une des données du programme. 3.4.9.2 Portée des fonctions Le langage Python considère les fonctions également comme des variables d’un type particulier. La portée des fonctions obéit aux mêmes règles que celles des variables. Une fonction ne peut être appelée que si elle a été définie avant son appel. print type(factorielle) # affiche <type ’function’> Comme il est possible de déclarer des variables locales, il est également possible de définir des fonctions locales ou fonctions imbriquées. Une fonction locale n’est appelable qu’à l’intérieur de la fonction dans laquelle elle est définie. Dans l’exemple suivant, la fonction affiche_pair inclut une fonction locale qui n’est appelable que par cette fonction affiche_pair. def affiche_pair(): def fonction_locale(i): if i % 2 == 0 : return True else : return False for i in range(0,10): if fonction_locale(i): | # fonction locale ou imbriquée | print i affiche_pair() fonction_locale(5) | # l’appel à cette fonction locale # déclenche une erreur d’exécution | | | | A l’intérieur d’une fonction locale, le mot-clé global désigne toujours les variables globales du programme et non les variables de la fonction dans laquelle cette sousfonction est définie. 3.4.10 Nombre de paramètres variable Où fonction est un nom de fonction, param_1 à param_n sont des paramètres de la fonction, liste est le nom de la liste qui doit recevoir la liste des valeurs seules envoyées à la fonction et qui suivent les paramètres (plus précisément, c’est un tuple), dictionnaire reçoit la liste des couples (identificateur, valeur). L’appel à cette fonction suit quant à lui la syntaxe suivante : syntaxe 3.24 : fonction, nombre variable de paramètres (appel) fonction (valeur_1, , valeur_n, \ liste_valeur_1, , liste_valeur_p, \ nom_1 = v_1, , nom_q = v_q) Où fonction est un nom de fonction, valeur_1 à valeur_n sont les valeurs associées aux paramètres param_1 à param_n, liste_valeur_1 à liste_valeur_p formeront la liste liste, les couples nom_1 : v_1 à nom_q : v_q formeront le dictionnaire dictionnaire. Exemple : def fonction(p,*l,**d): print "p = ",p print "liste (tuple) l :", l print "dictionnaire d :", d fonction (1,2,3,a=5,b=6) # 1 est associé au paramètre p # 2 et 3 sont insérés dans la liste l # a=5 et b=6 sont insérés dans le dictionnaire d | Ce programme affiche : p = 1 liste l : (2, 3) dictionnaire d : {’a’: 5, ’b’: 6} A l’instar des paramètres par défaut, la seule contrainte de cette écriture est la nécessité de respecter l’ordre dans lequel les informations doivent apparaître. Lors de l’appel, les valeurs sans précision de nom de paramètre seront placées dans une liste (ici le tuple l). Les valeurs associées à un nom de paramètre seront placées dans un dictionnaire (ici d). Les valeurs par défaut sont obligatoirement placées après les paramètres non nommés explicitement. Une fonction qui accepte des paramètres en nombre variable peut à son tour appeler une autre fonction acceptant des paramètres en nombre variable. Il faut pour cela se servir du symbole ? afin de transmettre à fonction les valeurs reçues par fonction2. def fonction2 (p, *l, **d) : l += (4,) # on ajoute une valeur au tuple d ["c"] = 5 # on ajoute un couple (paramètre,valeur) fonction (p, *l, **d) # ne pas oublier le symbole * fonction2 (1,2,3,a=5,b=6) | Le programme affiche : p = 1 liste l : (2, 3, 4) dictionnaire d : {’a’: 5, ’c’: 5, ’b’: 6} 3.4.11 Ecriture simplifiée pour des fonctions simples Lorsque le code d’une fonction tient en une ligne et est le résultat d’une expression, il est possible de condenser son écriture à l’aide du mot-clé lambda. syntaxe 3.25 : fonction lambda nom_fonction = lambda param_1, , param_n : expression nom_fonction est le nom de la fonction, param_1 à param_n sont les paramètres de cette fonction (ils peuvent également recevoir des valeurs par défaut), expression est l’expression retournée par la fonction. L’exemple suivant utilise cette écriture pour définir la fonction min retournant le plus petit entre deux nombres positifs. min = lambda x,y : (abs (x+y) - abs (x-y))/2 print min (1,2) | # affiche 1 | print min (5,4) | # affiche 4 | Cette écriture correspond à l’écriture non condensée suivante : def min(x,y): return (abs (x+y) - abs (x-y))/2 print min (1,2) # affiche 1 print min (5,4) # affiche 4 | 3.4.12 Fonctions générateur def fonction_yield(n): i = 0 while i < n-1: print "yield 1" # affichage : pour voir ce que fait le programme yield i # arrête la fonction qui reprendra i = i+1 # à la ligne suivante lors du prochain appel print "yield 2" # affichage : pour voir ce que fait le programme yield i # arrête la fonction qui ne reprendra pas # lors du prochain appel car le code de la fonction # prend fin ici for a in fonction_yield(2): print a # affiche tous les éléments que retourne la # fonction fonction_yield, elle simule la liste # [0,1] print "-----------------------------------------------" for a in fonction_yield(3): print a # nouvel appel, l’exécution reprend # au début de la fonction, # affiche tous les éléments que retourne la # fonction fonction_yield, elle simule la liste # [0,1,2] | Le programme affiche tous les entiers compris entre 0 et 4 inclus ainsi que le texte ”yield1” ou ”yield2” selon l’instruction yield qui a retourné le résultat. Lorsque la fonction a finalement terminé son exécution, le prochain appel agit comme si c’était la première fois qu’on l’appelait. yield 1 0 yield 2 1 ----------------------------------------------- | yield 1 0 yield 1 1 yield 2 2 3.4.13 Identificateur appelable La fonction callable retourne un booléen permettant de savoir si un identificateur est une fonction (voir chapitre 4), de savoir par conséquent si tel identificateur est appelable comme une fonction. x = 5 def y () : return None print callable (x) # affiche False car x est une variable print callable (y) # affiche True car y est une fonction 3.4.14.1 Fonction eval Cette fonction a déjà été abordée lors des paragraphes 2.4.1 (paragraphe 2.4.2, page 54). Un exemple a déjà été présenté page 54. Elle évalue toute chaîne de caractères contenant une expression écrite avec la syntaxe du langage Python. Cette expression peut utiliser toute variable ou toute fonction accessible au moment où est appelée la fonction eval. x = 3 y = 4 print eval ("x*x+y*y+2*x*y") # affiche 49 print (x+y)**2 # affiche 49 | Si l’expression envoyée à la fonction eval inclut une variable non définie, l’interpréteur Python génère une erreur comme le montre l’exemple suivant. x = 3 y = 4 print eval ("x*x+y*y+2*x*y+z") | La variable z n’est pas définie et l’expression n’est pas évaluable. Traceback (most recent call last): File "c:\temp\", line 3, in -toplevelprint eval ("x*x+y*y+2*x*y+z") File "<string>", line 0, in -toplevel- NameError: name ’z’ is not defined | L’erreur se produit dans une chaîne de caractères traduite en programme informatique, c’est pourquoi l’interpréteur ne peut pas situer l’erreur dans un fichier. L’erreur ne se produit dans aucun fichier, cette chaîne de caractères pourrait être définie dans un autre. 3.4.14.2 Fonctions compile, exec Plus complète que la fonction eval, la fonction compile permet d’ajouter une ou plusieurs fonctions au programme, celle-ci étant définie par une chaîne de caractères. Le code est d’abord compilé (fonction compile) puis incorporé au programme (fonction exec) comme le montre l’exemple suivant. import math str = """def coordonnees_polaires (x,y): rho = (x*x+y*y) theta = math.atan2 (y,x) return rho, theta""" # fonction définie par une chaîne de caractères obj = compile(str,"","exec") # fonction compilée print coordonnees_polaires(1,1)# affiche (1.4142135623730951, 0.78539816339744828) | La fonction compile prend en fait trois arguments. Le premier est la chaîne de caractères contenant le code à compiler. Le second paramètre ("" dans l’exemple) contient un nom de fichier dans lequel seront placées les erreurs de compilation. Le troisième paramètre est une chaîne de caractères à choisir parmi "exec" ou "eval". Selon ce choix, ce sera la fonction exec ou eval qui devra être utilisée pour agréger le résultat de la fonction compile au programme. L’exemple suivant donne un exemple d’utilisation de la fonction compile avec la fonction eval. import math str = """(x*x+y*y)""" # expression définie par une chaîne de caractères obj = compile(str,"","eval") # expression compilée x = 1 y = 2 print eval (obj) # résultat de l’expression | 3.5 Indentation L’indentation est synonyme de décalage. Pour toute boucle, test, fonction, et plus tard, toute définition de classe, le fait d’indenter ou décaler les lignes permet de définir une dépendance d’un bloc de lignes par rapport à un autre. Les lignes indentées par rapport à une boucle for dépendent de celle-ci puisqu’elle seront exécutées à chaque passage dans la boucle. Les lignes indentées par rapport au mot-clé def sont considérées comme faisant partie du corps de la fonction. La remarque 3.4 page 59 précise l’erreur que l’interpréteur Python retourne en cas de mauvaise indentation. 3.6 Fonctions usuelles Certaines fonctions sont communes aux dictionnaires et aux listes, elles sont également définis pour de nombreux objets présents dans les extensions du langages. Quelque soit le contexte, le résultat attendu à la même signification. Les plus courantes sont présentées par la table 3.4. La fonction map permet d’écrire des boucles de façon simplifiée. Elle est utile dans le cas où on souhaite appliquer la même fonction à tous les éléments d’un ensemble. Par exemple les deux dernières lignes du programme suivant sont équivalentes. l = [0,3,4,4,5,6] print [ est_pair (i) for i in l ] # affiche [0, 1, 0, 0, 1, 0] print map (est_pair, l) # affiche [0, 1, 0, 0, 1, 0] Elle peut aider à simplifier l’écriture lorsque plusieurs listes sont impliquées. Ici encore, les deux dernières lignes sont équivalentes. def addition (x,y) : return x + y l = [0,3,4,4,5,6] m = [1,3,4,5,6,8] print [ addition (l [i], m [i]) for i in range (0, len (l)) ] print map (addition, l, m) # affiche [1, 6, 8, 9, 11, 14] Il est possible de substituer None à la fonction f pour obtenir l’équivalent de la fonction zip. print map (None, l,m) # affiche [(0, 1), (3, 3), (4, 4), (4, 5), (5, 6), (6, 8)] print zip (l,m) # affiche [(0, 1), (3, 3), (4, 4), (4, 5), (5, 6), (6, 8)] Comme pour les dictionnaires, la fonction sorted permet de parcourir les éléments d’une liste de façon ordonnée. Les deux exemples qui suivent sont presque équivalents. Dans le second, la liste l demeure inchangée alors qu’elle est triée dans le premier programme. l = [ 4, 5, 3, -6, 7, 9] l.sort () for n in l : print n | | l = [ 4, 5, 3, -6, 7, 9] for n in sorted (l) : print n | l = [ 4, 5, 3, -6, 7, 9] for i in xrange (0, len (l)) : print i, l [i] | | l = [ 4, 5, 3, -6, 7, 9] for i,v in enumerate (l) : print i, v | abs(x) | Retourne la valeur absolue de x. | | callable(x) | Dit si la variable x peut être appelée. | | chr(i) | Retourne le caractère associé au code numérique i. | | cmp(x,y) | Compare x et y, retourne -1 si x < y, 0 en cas d’égalité, 1 sinon. | | dir(x) | Retourne l’ensemble des méthodes associées à x qui peut être un objet, un module, un variable, | | enumerate(x) | parcourt un ensemble itérable (voir paragraphe 3.6) | | help(x) | Retourne l’aide associée à x. | | id(x) | Retourne un identifiant unique associé à l’objet x. Le mot-clé is est relié à cet identifiant. | | isinstance( x,classe | Dit si l’objet x est de type classe, voir le chapitre 4 sur les classes. | | issubclass( cl1,cl2) | Dit si la classe cl1 hérite de la classe cl2, voir le chapitre 4 sur les classes. | | len(l) | Retourne la longueur de l. | | map(f,l1,l2, ) | Applique la fonction f sur les listes l1, l2 Voir l’exemple page 86. | | max(l) | Retourne le plus grand élément de l. | | min(l) | Retourne le plus petit élément de l. | | ord(s) | Fonction réciproque de chr. | | range(i,j[,k] | Construit la liste des entiers de i à j. Si k est précisé, va de k en k à partir de i. | | reload(module) | Recharge un module (voir paragraphe 6). | | repr(o) | Retourne une chaîne de caractères qui représente l’objet o. | | round(x[,n]) | Arrondi x à n décimales près ou aucune si n n’est pas précisé. | | sorted(x [,cmp[,key[, reverse]]]) | tri un ensemble itérable (voir paragraphe 3.6) | | str(o) | Retourne une chaîne de caractères qui représente l’objet o. | | sum(l) | Retourne la somme de l’ensemble l. | | type(o) | | xrange(i,j[,k] | Plus rapide que la fonction range mais utilisable que dans une boucle for. | | zip(l1,l2, ) | Construit une liste de tuples au lieu d’un tuple de listes. | | | | | | | | Table 3.4 : Liste non exhaustive de fonctions définis par le langage Python sans qu’aucune extension ne soit nécessaire. Elles ne sont pas toujours applicables même si la syntaxe d’appel est correcte, elles produisent une erreur dans ce cas. 3.7 Constructions classiques Les paragraphes qui suivent décrivent des schémas qu’on retrouve dans les programmes dans de nombreuses situations. Ce sont des combinaisons simples d’une ou deux boucles, d’un test, d’une liste, d’un dictionnaire. 3.7.1 Recherche d’un élément Rechercher un élément consiste à parcourir un ensemble jusqu’à ce qu’on le trouve : le résultat souhaité est sa position ou un résultat indiquant qu’il n’a pas été trouvé. Cette recherche est souvent insérée dans une fonction dont voici le schéma : def recherche (li, c) : for i,v in enumerate (li) : if v == c : return i return -1 li = [ 45, 32, 43, 56 ] print recherche (li, 43) # affiche 2 | La méthode index permet de réduire ce programme. Néanmoins, la fonction recherche peut être adaptée pour rechercher la première valeur trouvée parmi deux possibles, ce qu’il n’est pas possible de faire avec la méthode index. def recherche (li, c,d) : for i,v in enumerate (li) : if v in [c,d] : return i return -1 li = [ 45, 32, 43, 56 ] print recherche (li, 43, 32) # affiche 1 3.7.2 Maximum, minimum La recherche d’un extremum comme le calcul d’une somme fait toujours intervenir une boucle. La différence est qu’on souhaite parfois obtenir la position du maximum. li = [ 0, 434, 43, 6436, 5 ] m = li [0] # initialisation for l in li : # boucle if m < l : m = l # m est le maximum | li = [ 0, 434, 43, 6436, 5 ] m = 0 for i in xrange (0, len (li)) : if li [m] < li [i] : m = i | Ce code est équivalent à l’instruction max(li). Pour récupérer la position du maximum, il faut itérer sur les positions et non sur les éléments. Pour éviter la boucle, on peut utiliser l’astuce décrite au paragraphe 3.7.7 qui consiste à former une liste de couples avec la position initiale : k = [ (v,i) for i,v in enumerate (li) ] m = max (k) [1] Il arrive fréquemment qu’on ne doive pas chercher le maximum d’une liste mais le maximum au sein d’un sous-ensemble de cette liste. Dans l’exemple qui suit, on cherche le tuple dont la première valeur est maximale et dont la seconde valeur est égale à 1. li = [ (0,0), (434,0), (43,1), (6436,1), (5,0) ] m = -1 # -1 car le premier élément peut ne pas faire partie du sous-ensemble for i in range (0, len (li)) : if li [i][1] == 0 and (m == -1 or li [m][0] < li [i][0]) : m = i | 3.7.3 Recherche dichotomique La recherche dichotomique est plus rapide qu’une recherche classique mais elle suppose que celle-ci s’effectue dans un ensemble trié. L’idée est de couper en deux l’intervalle de recherche à chaque itération. Comme l’ensemble est trié, en comparant l’élément cherché à l’élément central, on peut éliminer une partie de l’ensemble : la moitié inférieure ou supérieure. def recherche_dichotomique (li, c) : a,b = 0, len (li)-1 while a <= b : return -1 # élément non trouvé li = range (0,100,2) print recherche_dichotomique (li, 48) # affiche 24 print recherche_dichotomique (li, 49) # affiche -1 | 3.7.4 Décomposition en matrice Les quelques lignes qui suivent permettent de décomposer une chaîne de caractères en matrice. Chaque ligne et chaque colonne sont séparées par des séparateurs différents. Ce procédé intervient souvent lorsqu’on récupère des informations depuis un fichier texte lui-même provenant d’un tableur. s = "case11;case12;case13|case21;case22;case23" # décomposition en matrice ligne = s.split ("|") # lignes mat = [ l.split (";") for l in ligne ] # colonnes L’opération inverse : ligne = [ ";".join (l) for l in mat ] | # colonnes | s = "|".join (ligne) | # lignes | 3.7.5 Somme Le calcul d’une somme fait toujours intervenir une boucle car le langage Python ne peut faire des additions qu’avec deux nombres. Le schéma est toujours le même : initialisation et boucle. li = [ 0, 434, 43, 6456 ] s = 0 | # initialisation | for l in li : | # boucle | s += l | # addition | Ce code est équivalent à l’instruction sum(li). Dans ce cas où la somme intègre le résultat d’une fonction et non les éléments d’une liste, il faudrait écrire : def fonction (x) : return x*x s = 0 for l in li : s += fonction (l) s = sum ( [fonction (l) for l in li] ) s = sum ( map (fonction, li) ) | Et ces deux lignes pourraient être résumées en une seule grâce à l’une de ces deux instructions. 3.7.6 Tri Le tri est une opération fréquente. On n’a pas toujours le temps de programmer le tri le plus efficace comme un tri quicksort et un tri plus simple suffit la plupart du temps. Le tri suivant consiste à recherche le plus petit élément puis à échanger sa place avec le premier élément du tableau du tableau. On recommence la même procédure à partir de la seconde position, puis la troisième et ainsi de suite jusqu’à la fin du tableau. for i in xrange (0, len (li)) : # recherche du minimum entre i et len (li) exclu pos = i for j in xrange (i+1, len (li)) : if li [j] < li [pos] : pos = j # échange ech = li [pos] li [pos] = li [i] li [i] = ech | La méthode sort tri également une liste mais selon un algorithme plus efficace que celui-ci dont la logique est présentée par l’exercice 4, page 261. Ceci explique pourquoi on hésite toujours à programmer un tri quitte à avoir recours à une astuce telle que celle présentées au paragraphe suivant. 3.7.7 Tri et position initiale Il arrive parfois qu’on souhaite trier un tableau puis appliquer la même transformation à un second tableau. Pour cela, il faut récupérer la position initiale des éléments dans le tableau trié. Une possibilité consiste à trier non pas le tableau mais une liste de couples (valeur, position) comme le montre l’exemple suivant : tab = ["zéro", "un", "deux"] # tableau à trier pos = [ (tab [i],i) for i in range (0, len (tab)) ] # tableau de couples () # tri La liste [p[1] forpinpos] correspond à l’ensemble des positions initiales des éléments de la liste tab. Les deux lignes qui suivent permettent d’en déduire la position de chaque élément dans l’ensemble trié. ordre[i] est le rang de l’élément d’indice i avant le tri. ordre = range (0, len (pos)) for i in xrange (0, len (pos)) : ordre [pos [i][1]] = i 3.7.8 Comptage On souhaite ici compter le nombre d’occurrences de chaque élément d’un tableau. Par exemple, on pourrait connaître par ce moyen la popularité d’un mot dans un discours politique ou l’étendue du vocabulaire utilisé. L’exemple suivant compte les mots d’une liste de mots. li = ["un", "deux", "un", "trois"] d = { } for l in li : if l not in d : d [l] = 1 else : d [l] += 1 print d # affiche {’un’: 2, ’trois’: 1, ’deux’: 1} | La structure la plus appropriée ici est un dictionnaire puisqu’on cherche à associer une valeur à un élément d’une liste qui peut être de tout type. Si la liste contient des éléments de type modifiable comme une liste, il faudrait convertir ceux-ci en un type immuable comme une chaîne de caractères. L’exemple suivant illustre ce cas en comptant les occurrences des lignes d’une matrice. mat = [ [1,1,1], [2,2,2], [1,1,1]] d = { } for l in mat : k = str (l) # k = tuple (l) lorsque cela est possible if k not in d : d [k] = 1 else : d [k] += 1 print d # affiche {’[1, 1, 1]’: 2, ’[2, 2, 2]’: 1} | On peut également vouloir non pas compter le nombre d’occurrence mais mémoriser les positions des éléments tous identiques. On doit utiliser un dictionnaire de listes : li = ["un", "deux", "un", "trois"] d = { } for i,v in enumerate (li) : if v not in d : d [v] = [ i ] else : d [v].append (i) print d # affiche {’un’: [0, 2], ’trois’: [3], ’deux’: [1]} | 3.7.9 Matrice en un vecteur mat = [[0,1,2],[3,4,5]] lin = [ i * len (mat [i]) + j \ for i in range (0, len (mat)) \ for j in range (0, len (mat [i])) ] Le programme suivant fait l’inverse. Il faut faire attention à la position des crochets et l’ordre des boucles for. nc = len (mat [0]) mat = [ [ lin [i * nc + j] for j in range (0, len (mat [i])) ] \ for i in range (0, len (mat)) ] 3.7.10 Fonctions et variables Une fonction peut aussi recevoir en paramètre une autre fonction. L’exemple suivant inclut la fonction calcul_n_valeur qui prend comme paramètres l et f. Cette fonction calcule pour toutes les valeurs x de la liste l la valeur f(x). fonction_carre ou fonction_cube sont passées en paramètres à la fonction calcul_n_valeur qui les exécute. Le paragraphe 4.10.1 page 134 présente le même programme mais rédigé avec des classes. def fonction_carre(x) : return x*x def fonction_cube (x) : return x*x*x def calcul_n_valeur (l,f): res = [ f(i) for i in l ] return res l = [0,1,2,3] print l # affiche [0, 1, 2, 3] l1 = calcul_n_valeur (l, fonction_carre) print l1 # affiche [0, 1, 4, 9] l2 = calcul_n_valeur (l, fonction_cube) print l2 # affiche [0, 1, 8, 27] |
Chapitre 4 Classes Imaginons qu’une banque détienne un fichier contenant des informations sur ses clients et qu’il soit impossible pour un client d’avoir accès directement à ces informations. Toutefois, il lui est en théorie possible de demander à son banquier quelles sont les informations le concernant détenues par sa banque. Il est en théorie également possible de les rectifier s’il estime qu’elles sont incorrectes. . On peut comparer cette banque à un objet qui possède des informations et des moyens permettant de lire ces informations et de les modifier. Vu de l’extérieur, cette banque cache son fonctionnement interne et les informations dont elle dispose, mais propose des services à ses utilisateurs. 4.1 Présentation des classes : méthodes et attributs 4.1.1 Définition, déclaration Définition 4.1 : classe Une classe est un ensemble incluant des variables ou attributs et des fonctions ou méthodes. Les attributs sont des variables accessibles depuis toute méthode de la classe où elles sont définies. En Python, les classes sont des types modifiables. syntaxe 4.2 : classe, définition class nom_classe : # corps de la classe # Le corps d’une classe peut être vide, inclure des variables ou attributs, des fonctions ou méthodes. Il est en tout cas indenté de façon à indiquer à l’interpréteur Python les lignes qui forment le corps de la classe. Les classes sont l’unique moyen en langage Python de définir de nouveaux types propres à celui qui programme. Il n’existe pas de type "matrice" ou de type "graphe" en langage Python qui soit prédéfini . Il est néanmoins possible de les définir au moyen des classes. Une matrice est par exemple un objet qui inclut les attributs suivant : le nombre de lignes, le nombre de colonnes, les coefficients de la matrice. Cette matrice inclut aussi des méthodes comme des opérations entre deux matrices telles que l’addition, la soustraction, la multiplication ou des opérations sur ellemême comme l’inversion, la transposition, la diagonalisation. Cette liste n’est pas exhaustive, elle illustre ce que peut être une classe "matrice" - représentation informatique d’un objet "matrice" -, un type complexe incluant des informations de types variés (entier pour les dimensions, réels pour les coefficients), et des méthodes propres à cet objet, capables de manipuler ces informations. La création d’une variable de type objet est identique à celle des types standards du langage Python : elle passe par une simple affectation. On appelle aussi cl une instance de la classe nom_classe. Cette syntaxe est identique à la syntaxe d’appel d’une fonction. La création d’une instance peut également faire intervenir des paramètres (voir paragraphe 4.4). Le terme instance va de paire avec le terme classe : Définition 4.4 : instance Une instance d’une classe C désigne une variable de type C. Le terme instance ne s’applique qu’aux variables dont le type est une classe. L’exemple suivant permet de définir une classe vide. Le mot-clé pass permet de préciser que le corps de la classe ne contient rien. Il est tout de même possible de définir une instance de la classe classe_vide simplement par l’instruction suivante : class classe_vide: pass cl = classe_vide () Remarque 4.5 : type d’une instance Dans l’exemple précédent, la variable nb n’est pas de type exemple_classe mais de type instance comme le montre la ligne suivante : print type (cl) | # affiche <type ’instance’> | Pour savoir si une variable est une instance d’une classe donnée, il faut utiliser la fonction isinstance : isinstance (cl,classe_vide) | # affiche True | 4.1.2 Méthodes Définition 4.6 : méthode Les méthodes sont des fonctions qui sont associées de manière explicite à une classe. Elles ont comme particularité un accès privilégié aux données de la classe elle-même. Ces données ou attributs sont définis plus loin. Les méthodes sont en fait des fonctions pour lesquelles la liste des paramètres contient obligatoirement un paramètre explicite qui est l’instance de la classe à laquelle cette méthode est associée. Ce paramètre est le moyen d’accéder aux données de la classe. syntaxe 4.7 : classe, méthode, définition def nom_methode (self, param_1, , param_n) : # corps de la méthode A part le premier paramètre qui doit de préférence s’appeler self, la syntaxe de définition d’une méthode ressemble en tout point à celle d’une fonction. Le corps de la méthode est indenté par rapport à la déclaration de la méthode, elle-même indentée par rapport à la déclaration de la classe. L’appel à cette méthode obéit à la syntaxe qui suit : syntaxe 4.8 : classe, méthode, appel cl = nom_classe () # variable de type nom_classe t = cl.nom_methode (valeur_1, , valeur_n) L’appel d’une méthode nécessite tout d’abord la création d’une variable. Une fois cette variable créée, il suffit d’ajouter le symbole "." pour exécuter la méthode. Le paramètre self est ici implicitement remplacé par cl lors de l’appel. L’exemple suivant simule le tirage de nombres aléatoires à partir d’une suite définie par récurrence un+1 = (un ?A) mod B où A et B sont des entiers très grands. Cette suite n’est pas aléatoire mais son comportement imite celui d’une suite aléatoire. Le terme un est dans cet exemple contenu dans la variable globale rnd. rnd = 42 class exemple_classe: def methode1(self,n): """simule la génération d’un nombre aléatoire compris entre 0 et n-1 inclus""" global rnd rnd = 397204094 * rnd % 2147483647 return int (rnd % n) nb = exemple_classe () l = [ nb.methode1(100) for i in range(0,10) ] print l # affiche [19, 46, 26, 88, 44, 56, 56, 26, 0, 8] | nb2 = exemple_classe () l2 = [ nb2.methode1(100) for i in range(0,10) ] print l2 # affiche [46, 42, 89, 66, 48, 12, 61, 84, 71, 41] Remarque 4.9 : méthodes et fonctions Les méthodes sont des fonctions insérées à l’intérieur d’une classe. La syntaxe de la déclaration d’une méthode est identique à celle d’une fonction en tenant compte du premier paramètre qui doit impérativement être self. Les paramètres par défaut, l’ordre des paramètres, les nombres variables de paramètres présentés au paragraphe 3.4 sont des extensions tout autant applicables aux méthodes qu’aux fonctions. 4.1.3 Attributs Définition 4.10 : attribut Les attributs sont des variables qui sont associées de manière explicite à une classe. Les attributs de la classe se comportent comme des variables globales pour toutes les méthodes de cette classe. Une classe permet en quelque sorte de regrouper ensemble des informations liées. Elles n’ont de sens qu’ensemble et les méthodes manipulent ces données liées. C’est le cas pour un segment qui est toujours défini par ces deux extrémités qui ne vont pas l’une sans l’autre. syntaxe 4.11 : classe, attribut, définition class nom_classe : def nom_methode (self, param_1, , param_n) : self.nom_attribut = valeur Le paramètre self n’est pas un mot-clé même si le premier paramètre est le plus souvent appelé self. Il désigne l’instance de la classe sur laquelle va s’appliquer la méthode. La déclaration d’une méthode inclut toujours un paramètre self de sorte que self.nom_attribut désigne un attribut de la classe. nom_attribut seul désignerait une variable locale sans aucun rapport avec un attribut portant le même nom. Les attributs peuvent être déclarés à l’intérieur de n’importe quelle méthode, voire à l’extérieur de la classe elle-même. L’endroit où est déclaré un attribut a peu d’importance pourvu qu’il le soit avant sa première utilisation. Dans l’exemple qui suit, la méthode methode1 utilise l’attribut rnd sans qu’il ait été créé. def methode1(self,n): """simule la génération d’un nombre aléatoire compris entre 0 et n-1 inclus""" | = 397204094 * % 2147483647 return int ( % n) nb = exemple_classe () l = [ nb.methode1(100) for i in range(0,10) ] print l | Cet exemple déclenche donc une erreur (ou exception) signifiant que l’attribut rnd n’a pas été créé. Traceback (most recent call last): File "", line 8, in -toplevell = [ nb.methode1(100) for i in range(0,10) ] File "", line 4, in methode1 = 397204094 * % 2147483647 AttributeError: exemple_classe instance has no attribute ’rnd’ | Pour remédier à ce problème, il existe plusieurs endroits où il est possible de créer l’attribut rnd. Il est possible de créer l’attribut à l’intérieur de la méthode methode1. Mais le programme n’a plus le même sens puisqu’à chaque appel de la méthode methode1, l’attribut rnd reçoit la valeur 42. La liste de nombres aléatoires contient dix fois la même valeur. class exemple_classe: def methode1(self,n): """simule la génération d’un nombre aléatoire compris entre 0 et n-1 inclus""" = 42 # déclaration à l’intérieur de la méthode, # doit être précédé du mot-clé self = 397204094 * % 2147483647 return int ( % n) nb = exemple_classe () l = [ nb.methode1(100) for i in range(0,10) ] print l # affiche [19, 19, 19, 19, 19, 19, 19, 19, 19, 19] | Il est possible de créer l’attribut rnd à l’extérieur de la classe. Cette écriture devrait toutefois être évitée puisque la méthode methode1 ne peut pas être appelée sans que l’attribut rnd ait été ajouté. class exemple_classe: def methode1(self,n): """simule la génération d’un nombre aléatoire compris entre 0 et n-1 inclus""" = 397204094 * % 2147483647 return int ( % n) nb = exemple_classe () # indispensable pour utiliser la méthode methode1 l = [ nb.methode1(100) for i in range(0,10) ] print l # affiche [19, 46, 26, 88, 44, 56, 56, 26, 0, 8] | 4.2 Constructeur L’endroit le plus approprié pour déclarer un attribut est à l’intérieur d’une méthode appelée le constructeur. S’il est défini, il est implicitement exécuté lors de la création de chaque instance. Le constructeur d’une classe se présente comme une méthode et suit la même syntaxe à ceci près que son nom est imposé : __init__. Hormis le premier paramètre, invariablement self, il n’existe pas de contrainte concernant la liste des paramètres excepté que le constructeur ne doit pas retourner de résultat. syntaxe 4.12 : classe, constructeur class nom_classe : def __init__(self, param_1, , param_n): # code du constructeur nom_classe est une classe, __init__ est son constructeur, sa syntaxe est la même que celle d’une méthode sauf que le constructeur ne peut employer l’instruction return. La modification des paramètres du constructeur implique également la modification de la syntaxe de création d’une instance de cette classe. syntaxe 4.13 : classe, instance x = nom_classe (valeur_1, ,valeur_n) nom_classe est une classe, valeur_1 à valeur_n sont les valeurs associées aux paramètres param_1 à param_n du constructeur. L’exemple suivant montre deux classes pour lesquelles un constructeur a été défini. La première n’ajoute aucun paramètre, la création d’une instance ne nécessite pas de paramètre supplémentaire. La seconde classe ajoute deux paramètres a et b. Lors de la création d’une instance de la classe classe2, il faut ajouter deux valeurs. class classe1: def __init__(self): # pas de paramètre supplémentaire print "constructeur de la classe classe1" self.n = 1 # ajout de l’attribut n class classe2: def __init__(self,a,b): # deux paramètres supplémentaires print "constructeur de la classe classe2" self.n = (a+b)/2 # ajout de l’attribut n x = classe2 (5,9) # affiche constructeur de la classe classe2 print x.n # affiche 7 | Remarque 4.14 : constructeur et fonction Le constructeur autorise autant de paramètres qu’on souhaite lors de la création d’une instance et celle-ci suit la même syntaxe qu’une fonction. La création d’une instance pourrait être considérée comme l’appel à une fonction à ceci près que le type du résultat est une instance de classe. En utilisant un constructeur, l’exemple du paragraphe précédent simulant une suite de variable aléatoire permet d’obtenir une classe autonome qui ne fait pas appel à une variable globale ni à une déclaration d’attribut extérieur à la classe. class exemple_classe: def __init__ (self) : # constructeur = 42 # on crée l’attribut rnd, identique pour chaque instance # --> les suites générées auront toutes le même début def methode1(self,n): = 397204094 * % 2147483647 return int ( % n) nb = exemple_classe () l = [ nb.methode1(100) for i in range(0,10) ] print l # affiche [19, 46, 26, 88, 44, 56, 56, 26, 0, 8] nb2 = exemple_classe () l2 = [ nb2.methode1(100) for i in range(0,10) ] print l2 # affiche [19, 46, 26, 88, 44, 56, 56, 26, 0, 8] | De la même manière qu’il existe un constructeur exécuté à chaque création d’instance, il existe un destructeur exécuté à chaque destruction d’instance. Il suffit pour cela de redéfinir la méthode __del__. A l’inverse d’autres langages comme le C++, cet opérateur est peu utilisé car le Python nettoie automatiquement les objets qui ne sont plus utilisés ou plus référencés par une variable. Un exemple est donné page 115. 4.3 Apport du langage Python Chaque attribut d’une instance de classe est inséré dans un dictionnaire appelé __dict__, attribut implicitement présent dès la création d’une instance. class exemple_classe: def __init__ (self) : = 42 def methode1(self,n): = 397204094 * % 2147483647 return int ( % n) nb = exemple_classe () print nb.__dict__ # affiche {’rnd’: 42} | Ce dictionnaire offre aussi la possibilité de tester si un attribut existe ou non. Dans un des exemples du paragraphe précédent, l’attribut rnd était créé dans la méthode methode1, sa valeur était alors initialisée à chaque appel et la fonction retournait sans cesse la même valeur. En testant l’existence de l’attribut rnd, il est possible de le créer dans la méthode methode1 au premier appel sans que les appels suivants ne réinitialisent sa valeur à 42. class exemple_classe: def methode1(self,n): if "rnd" not in self.__dict__ : # l’attribut existe-t-il ? = 42 # création de l’attribut self.__dict__ ["rnd"] = 42 # autre écriture possible = 397204094 * % 2147483647 return int ( % n) nb = exemple_classe () l = [ nb.methode1(100) for i in range(0,10) ] print l # affiche [19, 46, 26, 88, 44, 56, 56, 26, 0, 8] | 4.3.2 Attributs implicites Certains attributs sont créés de manière implicite lors de la création d’une instance. Ils contiennent des informations sur l’instance. __module__ | Contient le nom du module dans lequel est incluse la classe (voir chapitre 6). | __class__ | Contient le nom de la classe de l’instance. Ce nom est précédé du nom du module suivi d’un point. | __dict__ | __doc__ | Contient un commentaire associé à la classe (voir paragraphe 4.3.3). | L’attribut __class__ contient lui même d’autres d’attributs : __doc__ | Contient un commentaire associé à la classe (voir paragraphe 4.3.3). | __dict__ | Contient la liste des attributs statiques (définis hors d’une méthode) et des méthodes (voir paragraphe 4.5.2). | __name__ | Contient le nom de l’instance. | __bases__ | Contient les classes dont la classe de l’instance hérite (voir paragraphe 4.8). | class classe_vide: pass cl = classe_vide () print cl.__module__ | # affiche __main__ | print cl.__class__ | # affiche __main__.classe_vide () | print cl.__dict__ | # affiche {} | print cl.__doc__ | # affiche None (voir paragraphe suivant) | print cl.__class__.__doc__ | # affiche None | print cl.__class__.__dict__ | # affiche {’__module__’: ’__main__’, # ’__doc__’: None} | print cl.__class__.__name__ | # affiche classe_vide | print cl.__class__.__bases__ | # affiche () | | | | 4.3.3 Commentaires, aide Comme les fonctions et les méthodes, des commentaires peuvent être associés à une classe, ils sont affichés grâce à la fonction help. Cette dernière présente le commentaire associé à la classe, la liste des méthodes ainsi que chacun des commentaires qui leur sont associés. Ce commentaire est affecté à l’attribut implicite __doc__. L’appel à la fonction help rassemble le commentaire de toutes les méthodes, le résultat suit le programme ci-dessous. class exemple_classe: """simule une suite de nombres aléatoires""" def __init__ (self) : """constructeur : initialisation de la première valeur""" = 42 def methode1(self,n): | class exemple_classe | simule une suite de nombres aléatoires | | Methods defined here: | | __init__(self) | constructeur : initialisation de la première valeur | | methode1(self, n) | simule la génération d’un nombre aléatoire | compris entre 0 et n-1 inclus | Pour obtenir seulement le commentaire associé à la classe, il suffit d’écrire l’une des trois lignes suivantes : print exemple_classe.__doc__ # affiche simule une suite de nombres aléatoires print nb.__doc__ # affiche simule une suite de nombres aléatoires print nb.__class__.__doc__ # affiche simule une suite de nombres aléatoires La fonction help permet d’accéder à l’aide associée à une fonction, une classe. Il existe des outils qui permettent de collecter tous ces commentaires pour construire une documentation au format HTML à l’aide d’outils comme pydoc ou Doxygen . Ces outils sont souvent assez simples d’utilisation. La fonction dir permet aussi d’obtenir des informations sur la classe. Cette fonction appliquée à la classe ou à une instance retourne l’ensemble de la liste des attributs et des méthodes. L’exemple suivant utilise la fonction dir avant et après l’appel de la méthode meth. Etant donné que cette méthode ajoute un attribut, la fonction dir retourne une liste plus longue après l’appel. class essai_class: def meth(self): x = 6 self.y = 7 a = essai_class() print dir (a) | # affiche [’__doc__’, ’__module__’, ’meth’] | a.meth () print dir (a) | # affiche [’__doc__’, ’__module__’, ’meth’, ’y’] | print dir (essai_class) | # affiche [’__doc__’, ’__module__’, ’meth’] 4.3.4 Classe incluse Parfois, il arrive qu’une classe soit exclusivement utilisée en couple avec une autre, c’est par exemple le cas des itérateurs (voir paragraphe 4.4.2). Il est alors possible d’inclure dans la déclaration d’une classe celle d’une sous-classe. L’exemple qui suit contient la classe ensemble_element. C’est un ensemble de points en trois dimensions (classe element) qui n’est utilisé que par cette classe. Déclarer la classe element à l’intérieur de la classe ensemble_element est un moyen de signifier ce lien. class ensemble_element : class element : def __init__ (self) : self.x, self.y, self.z = 0,0,0 def __init__ (self) : = [ ensemble_element.element () for i in xrange (0,3) ] def barycentre (self) : b = ensemble_element.element () for el in : b.x += el.x b.y += el.y b.z += el.z b.x /= len () b.y /= len () b.z /= len () return b f = ensemble_element () f.all [0].x, f.all [0].y, f.all [0].z = 4.5,1.5,1.5 b = f.barycentre () print b.x,b.y,b.z # affiche 1.5 0.5 0.5 | Pour créer une instance de la classe element, il faut faire précéder son nom de la classe où elle est déclarée : b = ensemble_element.element() comme c’est le cas dans la méthode barycentre par exemple. 4.4 Opérateurs, itérateurs Les opérateurs sont des symboles du langages comme +, -, +=, Au travers des opérateurs, il est possible de donner un sens à une syntaxe comme celle de l’exemple suivant : class nouvelle_classe: pass x = nouvelle_classe () + nouvelle_classe () | L’addition n’est pas le seul symbole concerné, le langage Python permet de donner un sens à tous les opérateurs numériques et d’autres reliés à des fonctions du langage comme len ou max. 4.4.1 Opérateurs Le programme suivant contient une classe définissant un nombre complexe. La méthode ajoute définit ce qu’est une addition entre nombres complexes. def __init__ (self, a = 0, b= 0) : self.a, self.b = a,b def get_module (self) : return (self.a * self.a + self.b * self.b) def ajoute (self,c): return nombre_complexe (self.a + c.a, self.b + c.b) c1 = nombre_complexe (0,1) c2 = nombre_complexe (1,0) c = c1.ajoute (c2) # c = c1 + c2 print c.a, c.b | Toutefois, on aimerait bien écrire simplement c = c1 + c2 au lieu de c = c1.ajoute(c2) car cette syntaxe est plus facile à lire et surtout plus intuitive. Le langage Python offre cette possibilité. Il existe en effet des méthodes clés dont l’implémentation définit ce qui doit être fait dans le cas d’une addition, d’une comparaison, d’un affichage, A l’instar du constructeur, toutes ces méthodes clés, qu’on appelle des opérateurs, sont encadrées par deux blancs soulignés, leur déclaration suit invariablement le même schéma. Voici celui de l’opérateur __add__ qui décrit ce qu’il faut faire pour une addition. syntaxe 4.15 : classe, opérateur __add__ class nom_class : def __add__ (self, autre) : # corps de l’opérateur return # nom_classe nom_classe est une classe. L’opérateur __add__ définit l’addition entre l’instance self et l’instance autre et retourne une instance de la classe nom_classe. Le programme suivant reprend le précédent de manière à ce que l’addition de deux nombres complexes soit dorénavant une syntaxe correcte. class nombre_complexe: def __init__ (self, a = 0, b= 0) : self.a, self.b = a,b def get_module (self) : return (self.a * self.a + self.b * self.b) def __add__(self, c): return nombre_complexe (self.a + c.a, self.b + c.b) c1 = nombre_complexe (0,1) c2 = nombre_complexe (1,0) c = c1 + c2 # cette expression est maintenant syntaxiquement correcte c = c1.__add__ (c2) # même ligne que la précédente mais écrite explicitement print c.a, c.b syntaxe 4.16 : classe, opérateur __iadd__ class nom_class : def __iadd__ (self, autre) : # corps de l’opérateur return self nom_classe est une classe. L’opérateur __iadd__ définit l’addition entre l’instance self et l’instance autre. L’instance self est modifiée pour recevoir le résultat. L’opérateur retourne invariablement l’instance modifiée self. On étoffe la classe nombre_complexe à l’aide de l’opérateur __iadd__. class nombre_complexe: def __init__ (self, a = 0, b= 0) : self.a, self.b = a,b def get_module (self) : return (self.a * self.a + self.b * self.b) def __add__(self, c) : return nombre_complexe (self.a + c.a, self.b + c.b) def __iadd__(self, c) : self.a += c.a self.b += c.b return self c1 = nombre_complexe (0,1) c2 = nombre_complexe (1,0) c1 += c2 # utilisation de l’opérateur += c1.__iadd__ (c2) # c’est la transcription explicite de la ligne précédente print c1.a, c1.b | Un autre opérateur souvent utilisé est __str__ qui permet de redéfinir l’affichage d’un objet lors d’un appel à l’instruction print. syntaxe 4.17 : classe, opérateur __str__ class nom_class : def __str__ (self) : # corps de l’opérateur return nom_classe est une classe. L’opérateur __str__ construit une chaîne de caractères qu’il retourne comme résultat de façon à être affiché. L’exemple suivant reprend la classe nombre_complexe pour que l’instruction print affiche un nombre complexe sous la forme a + ib. class nombre_complexe: def __init__ (self, a = 0, b= 0) : self.a, self.b = a,b def __add__(self, c) : return nombre_complexe (self.a + c.a, self.b + c.b) def __str__ (self) : if self.b == 0 : return "%f" % (self.a) elif self.b > 0 : return "%f + %f i" % (self.a, self.b) else : return "%f - %f i" % (self.a, -self.b) | Il existe de nombreux opérateurs qu’il est possible de définir. La table 4.1 (page 137) présente les plus utilisés. Parmi ceux-là, on peut s’attarder sur les opérateurs __getitem__ et __setitem__, ils redéfinissent l’opérateur [] permettant d’accéder à un élément d’une liste ou d’un dictionnaire. Le premier, __getitem__ est utilisé lors d’un calcul, un affichage. Le second, __setitem__, est utilisé pour affecter une valeur. L’exemple suivant définit un point de l’espace avec trois coordonnées. Il redéfinit ou surcharge les opérateurs __getitem__ et __setitem__ de manière à pouvoir accéder aux coordonnées de la classe point_espace qui définit un point dans l’espace. En règle générale, lorsque les indices ne sont pas corrects, ces deux opérateurs lèvent l’exception IndexError (voir le chapitre 5). class point_espace: def __init__ (self, x,y,z): self._x, self._y, self._z = x,y,z def __getitem__(self,i): if i == 0 : return self._x if i == 1 : return self._y if i == 2 : return self._z # pour tous les autres cas --> erreur raise IndexError ("indice impossible, 0,1,2 autorisés") def __setitem__(self,i,x): if i == 0 : self._x = x elif i == 1 : self._y = y elif i == 2 : self._z = z # pour tous les autres cas --> erreur raise IndexError ("indice impossible, 0,1,2 autorisés") | def __str__(self): return "(%f,%f,%f)" % (self._x, self._y, self._z) a = point_espace (1,-2,3) print a # affiche (1.000000,-2.000000,3.000000) a [1] = -3 # (__setitem__) affecte -3 à a.y print "abscisse : ", a [0] # (__getitem__) affiche abscisse : 1 print "ordonnée : ", a [1] # (__getitem__) affiche ordonnée : -3 print "altitude : ", a [2] # (__getitem__) affiche altitude : 3 Traceback (most recent call last): File "", line 31, in ? print a [4] File "", line 13, in __getitem__ raise IndexError, "indice impossible, 0,1,2 autorisés" IndexError: indice impossible, 0,1,2 autorisés | 4.4.2 Itérateurs L’opérateur __iter__ permet de définir ce qu’on appelle un itérateur. C’est un objet qui permet d’en explorer un autre, comme une liste ou un dictionnaire. Un itérateur est un objet qui désigne un élément d’un ensemble à parcourir et qui connaît l’élément suivant à visiter. Il doit pour cela contenir une référence à l’objet qu’il doit explorer et inclure une méthode next qui retourne l’élément suivant ou lève une exception si l’élément actuel est le dernier. Par exemple, on cherche à explorer tous les éléments d’un objet de type point_espace défini au paragraphe précédent. Cette exploration doit s’effectuer au moyen d’une boucle for. a = point_espace (1,-2,3) for x in a: print x # affiche successivement 1,-2,3 | Cette boucle cache en fait l’utilisation d’un itérateur qui apparaît explicitement dans l’exemple suivant équivalent au précédent (voir paragraphe 3.3.2.3, page 66). a = point_espace (1,-2,3) it = iter (a) while True: try : print () except StopIteration : break | Afin que cet extrait de programme fonctionne, il faut définir un itérateur pour la classe point_espace. Cet itérateur doit inclure la méthode next. La classe point_espace doit quant à elle définir l’opérateur __iter__ pour retourner l’itérateur qui permettra de l’explorer. class point_espace: def __init__ (self, x,y,z): self._x, self._y, self._z = x,y,z def __str__(self): return "(%f,%f,%f)" % (self._x, self._y, self._z) def __getitem__(self,i): if i == 0 : return self._x if i == 1 : return self._y if i == 2 : return self._z class class_iter: """cette classe définit un itérateur pour point_espace""" def __init__ (self,ins): """initialisation, self._ins permet de savoir quelle instance de point_espace on explore, self._n mémorise l’indice de l’élément exploré""" self._n = 0 self._ins = ins def __iter__ (self) : # le langage impose cette méthode return self # dans certaines configurations def next (self): """retourne l’élément d’indice self._n et passe à l’élément suivant""" if self._n <= 2: v = self._ins [self._n] self._n += 1 return v else : # si cet élément n’existe pas, lève une exception raise StopIteration def __iter__(self): """opérateur de la classe point_espace, retourne un itérateur permettant de l’explorer""" return point_espace.class_iter (self) a = point_espace (1,-2,3) for x in a: print x # affiche successivement 1,-2,3 | Cette syntaxe peut paraître fastidieuse mais elle montre de manière explicite le fonctionnement des itérateurs. Cette construction est plus proche de ce que d’autres langages objets proposent. Python offre néanmoins une syntaxe plus courte avec le mot-clé yield qui permet d’éviter la création de la classe class_iter. Le code de la méthode __iter__ change mais les dernières lignes du programme précédent qui affichent successivement les éléments de point_espace sont toujours valides. class point_espace: def __init__ (self, x,y,z): self._x, self._y, self._z = x,y,z def __str__(self): return "(%f,%f,%f)" % (self._x, self._y, self._z) def __getitem__(self,i): if i == 0 : return self._x | if i == 1 : return self._y if i == 2 : return self._z # pour tous les autres cas --> erreur raise IndexError ("indice impossible, 0,1,2 autorisés") def __iter__(self): """itérateur avec yield (ou générateur)""" _n = 0 while _n <= 2 : yield self.__getitem__ (_n) _n += 1 print x # affiche successivement 1,-2,3 | 4.5 Méthodes, attributs statiques et ajout de méthodes 4.5.1 Méthode statique Définition 4.18 : méthode statique Les méthodes statiques sont des méthodes qui peuvent être appelées même si aucune instance de la classe où elles sont définies n’a été créée. L’exemple suivant définit une classe avec une seule méthode. Comme toutes les méthodes présentées jusqu’à présent, elle inclut le paramètre self qui correspond à l’instance pour laquelle elle est appelée. class essai_class: def methode (self): print "méthode non statique" x = essai_class () x.methode () | Une méthode statique ne nécessite pas qu’une instance soit créée pour être appelée. C’est donc une méthode n’ayant pas besoin du paramètre self. syntaxe 4.19 : classe, méthode statique class nom_class : def nom_methode(params, ) : # corps de la méthode nom_methode = staticmethod (nom_methode) nom_classe est une classe, nom_methode est une méthode statique. Il faut pourtant ajouter la ligne suivante pour indiquer à la classe que cette méthode est bien statique à l’aide du mot-clé staticmethod. Le programme précédent est modifié pour inclure une méthode statique. La méthode methode ne nécessite aucune création d’instance pour être appelée. class essai_class: def methode (): print "méthode statique" methode = staticmethod(methode) essai_class.methode () | Remarque 4.20 : déclaration d’une méthode statique def methode (): print "méthode statique" class essai_class: pass essai_class.methode = staticmethod(methode) essai_class.methode () | Toutefois, il est conseillé de placer l’instruction qui contient staticmethod à l’intérieur de la classe. Elle n’y sera exécutée qu’une seule fois comme le montre l’exemple suivant : class essai_class: print "création d’une instance de la classe essai_class" methode = staticmethod(methode) cl = classe_vide () # affiche création d’une instance de la classe essai_class ck = classe_vide () # n’affiche rien | Les méthodes statiques sont souvent employées pour créer des instances spécifiques d’une classe. class Couleur : def __init__ (self, r, v, b) : self.r, self.v, self.b = r, v, b def __str__ (self) : return str ( (self.r,self.v,self.b)) def blanc () : return Couleur (255,255,255) def noir () : return Couleur (0,0,0) blanc = staticmethod (blanc) noir = staticmethod (noir) c = Couleur.blanc () print c # affiche (255, 255, 255) c = () print c # affiche (0, 0, 0) | 4.5.2 Attributs statiques Définition 4.21 : attribut statique Les attributs statiques sont des attributs qui peuvent être utilisés même si aucune instance de la classe où ils sont définis n’a été créée. Ces attributs sont partagés par toutes les instances. syntaxe 4.22 : classe, attribut statique class nom_class : attribut_statique = valeur def nom_methode (self,params, ): nom_class.attribut_statique2 = valeur2 def nom_methode_st (params, ) : nom_class.attribut_statique3 = valeur3 nom_methode_st = staticmethod (nom_methode_st) Pour le programme suivant, la méthode meth n’utilise pas self.x mais essai_class.x. L’attribut x est alors un attribut statique, partagé par toutes les instances. C’est pourquoi dans l’exemple qui suit l’instruction z.meth() affiche la valeur 6 puisque l’appel y.meth() a incrémenté la variable statique x. class essai_class: x = 5 def meth(self): print essai_class.x essai_class.x += 1 y = essai_class () z = essai_class () y.meth() # affiche 5 z.meth() # affiche 6 | Remarque 4.23 : ambiguïté Même si un attribut est statique, il peut être utilisé avec la syntaxe self.attribut_statique dans une méthode non statique à condition qu’un attribut non statique ne porte pas le même nom. Si tel est pourtant le cas, certaines confusions peuvent apparaître : class exemple_classe: rnd = 42 def incremente_rnd (self): += 1 return cl = exemple_classe() | print cl.__dict__ # affiche {} print cl.__class__.__dict__ ["rnd"] # affiche 42 cl.incremente_rnd () print cl.__dict__ # affiche {’rnd’: 43} print cl.__class__.__dict__ ["rnd"] # affiche 42 | Dans ce cas, ce sont en fait deux attributs qui sont créés. Le premier est un attribut statique créé avec la seconde ligne de l’exemple rnd = 42. Le second attribut n’est pas statique et apparaît dès la première exécution de l’instruction +=1 comme le montre son apparition dans l’attribut __dict__ qui ne recense pas les attributs statiques. 4.5.3 Ajout de méthodes syntaxe 4.24 : classe, classmethod def nom_methode (cls) : # code de la fonction} class nom_classe : # code de la classe nom_methode = classmethod (nom_methode) # syntaxe 1 nom_classe.nom_methode = classmethod (nom_methode) # syntaxe 2 nom_classe est une classe, nom_methode est une méthode, nom_methode est une fonction qui est par la suite considérée comme une méthode de la classe nom_methode grâce à l’une ou l’autre des deux instructions incluant le mot-clé classmethod. Dans l’exemple qui suit, cette syntaxe est utilisée pour inclure trois méthodes à la classe essai_class selon que la méthode est déclarée et affectée à cette classe à l’intérieur ou à l’extérieur du corps de essai_class. def meth3 (cls): print "ok meth3", cls.x def meth4 (cls): print "ok meth4", cls.x class essai_classe: x = 5 def meth(self): print "ok meth", self.x def meth2(cls): print "ok meth2", cls.x meth3 = classmethod (meth3) x = essai_classe () x.meth () # affiche ok meth 5 x.meth2 () # affiche ok meth2 5 x.meth3 () # affiche ok meth3 5 | essai_classe.meth4 = classmethod (meth4) x.meth4 () | # affiche ok meth4 5 | 4.5.4 Propriétés syntaxe 4.25 : classe, propriété class nom_classe : # code de la classe nom_propriete = property (fget, fset, fdel, doc) Au sein de ces trois lignes, nom_classe est une classe, nom_propriete est le nom de la propriété, fget est la méthode qui doit retourner la valeur du pseudo-attribut nom_propriete, fset est la méthode qui doit modifier la valeur du pseudo-attribut nom_propriete, fdel est la méthode qui doit détruire le pseudo-attribut nom_propriete, doc est un commentaire qui apparaîtra lors de l’appel de la fonction help(nom_class) ou help(nom_class.nom_propriete). Pour illustrer l’utilisation des propriétés, on part d’une classe nombre_complexe qui ne contient que les parties réelle et imaginaire. Lorsqu’on cherche à obtenir le module 4, on fait appel à une méthode qui calcule ce module. Lorsqu’on cherche à modifier ce module, on fait appel à une autre méthode qui multiplie les parties réelle et imaginaire par un nombre réel positif de manière à ce que le nombre complexe ait le module demandé. On procède de même pour la propriété arg. La propriété conj retourne quant à elle le conjugué du nombre complexe mais la réciproque n’est pas prévue. On ne peut affecter une valeur à conj. import math class nombre_complexe(object): # voir remarque après l’exemple def __init__ (self, a = 0, b= 0): self.a = a self.b = b def __str__ (self) : if self.b == 0 : return "%f" % (self.a) elif self.b > 0 : return "%f + %f i" % (self.a, self.b) else : return "%f - %f i" % (self.a, -self.b) def get_module (self): return (self.a * self.a + self.b * self.b) | 4. Le module désigne ici le module d’un nombre complexe qui est égal à sa norme. On le note? |a + ib| = a2 + b2. def set_module (self,m): r = self.get_module () if r == 0: self.a = m self.b = 0 else : self.a *= d self.b *= d def get_argument (self) : r = self.get_module () if r == 0 : return 0 else : return math.atan2 (self.b / r, self.a / r) def set_argument (self,arg) : m = self.get_module () self.a = m * (arg) self.b = m * (arg) def get_conjugue (self): return nombre_complexe (self.a,-self.b) module = property (fget = get_module, fset = set_module, doc = "module") arg = property (fget = get_argument, fset = set_argument, doc = "argument") conj = property (fget = get_conjugue, doc = "conjugué") c = nombre_complexe (0.5, (3)/2) print "c = ", c # affiche c = 0.500000 + 0.866025 i print "module = ", c.module # affiche module = 1.0 print "argument = ", c.arg # affiche argument = 1.0471975512 c = nombre_complexe () c.module = 1 c.arg = * 2 / 3 print "c = ", c # affiche c = -0.500000 + 0.866025 i print "module = ", c.module # affiche module = 1.0 print "argument = ", c.arg # affiche argument = 2.09439510239 print "conjugué = ", c.conj # affiche conjugué = -0.500000 - 0.866025 i | La propriété conj ne possède pas de fonction qui permet de la modifier. Par conséquent, l’instruction c.conj = nombre_complexe(0,0) produit l’erreur suivante : Traceback (most recent call last): File "", line 53, in ? c.conj = nombre_complexe (0,0) AttributeError: can’t set attribute | Etant donné qu’une propriété porte déjà le nom de conj, aucun attribut du même nom ne peut être ajouté à la classe nombre_complexe. Remarque 4.26 : propriété et héritage 4.6 Copie d’instances 4.6.1 Copie d’instance de classe simple Aussi étrange que cela puisse paraître, le signe = ne permet pas de recopier une instance de classe. Il permet d’obtenir deux noms différents pour désigner le même objet. Dans l’exemple qui suit, la ligne nb2 = nb ne fait pas de copie de l’instance nb, elle permet d’obtenir un second nom nb2 pour l’instance nb. Vu de l’extérieur, la ligne nb2.rnd = 0 paraît modifier à la fois les objets nb et nb2 puisque les lignes printnb.rnd et printnb2.rnd affichent la même chose. En réalité, nb et nb2 désignent le même objet. class exemple_classe: def __init__ (self) : = 42 def methode1(self,n): = 397204094 * % 2147483647 return int ( % n) nb = exemple_classe () nb2 = nb print # affiche 42 print # affiche 42 = 0 print # affiche 0, comme prévu print # affiche 0, si nb et nb2 étaient des objets différents, # cette ligne devrait afficher 42 | Pour créer une copie de l’instance nb, il faut le dire explicitement en utilisant la fonction copy du module copy (voir le chapitre 6). syntaxe 4.27 : fonction copy import copy nom_copy = (nom_instance) nom_instance est une instance à copier, nom_copy est le nom désignant la copie. L’exemple suivant applique cette copie sur la classe exemple_classe générant des nombres aléatoires. class exemple_classe: def __init__ (self) : = 42 def methode1(self,n): = 397204094 * % 2147483647 return int ( % n) nb = exemple_classe () import copy # pour utiliser le module copy nb2 = (nb) # copie explicite print # affiche 42 print # affiche 42 | = 0 print # affiche 0 print # affiche 42 m = [ 0, 1 ] m2 = m del m2 # supprime l’identificateur mais pas la liste print m # affiche [0, 1] | La suppression d’un objet n’est effective que s’il ne reste aucune variable le référençant. L’exemple suivant le montre. class CreationDestruction (object) : def __init__ (self) : print "constructeur" def __new__ (self) : print "__new__" return object.__new__ (self) def __del__ (self) : print "__del__" print "a" m = CreationDestruction () print "b" m2 = m print "c" del m print "d" del m2 print "e" | La sortie de ce programme est la suivante : a __new__ constructeur b c d __del__ e Le destructeur est appelé autant de fois que le constructeur. Il est appelé lorsque plus aucun identificateur n’est relié à l’objet. Cette configuration survient lors de l’exemple précédent car le mot-clé del a détruit tous les identificateurs m et m2 qui étaient reliés au même objet. 4.6.2 Copie d’instance de classes incluant d’autres classes La fonction copy n’est pas suffisante lorsqu’une classe inclut des attributs qui sont eux-mêmes des classes incluant des attributs. Dans l’exemple qui suit, la classe exemple_classe inclut un attribut de type classe_incluse qui contient un attribut attr. Lors de la copie à l’aide de l’instruction nb2 = copy.copy(nb), l’attribut inclus n’est pas copié, c’est l’instruction nb2.inclus = nb.inclus qui est exécutée. On se retrouve donc avec deux noms qui désignent encore le même objet. class classe_incluse: def __init__ (self) : = 3 class exemple_classe: def __init__ (self) : self.inclus = classe_incluse () = 42 nb = exemple_classe () import copy # pour utiliser le module copy nb2 = (nb) # copie explicite print # affiche 3 print # affiche 3 = 0 print # affiche 0 (on voudrait 3 ici) print # affiche 0 syntaxe 4.29 : classe, opérateur __copy__ class nom_classe : def __copy__ () : copie = nom_classe ( ) # return copie nom_classe est le nom d’une classe. La méthode __copy__ doit retourner une instance de la classe nom_classe, dans cet exemple, cette instance a pour nom copie. L’exemple suivant montre un exemple d’implémentation de la classe __copy__. Cette méthode crée d’abord une autre instance copie de la classe exemple_classe puis initialise un par un ses membres. L’attribut rnd est recopié grâce à une affectation car c’est un nombre. L’attribut inclus est recopié grâce à la fonction copy du module copy car c’est une instance de classe. Après la copie, on vérifie bien que modifier l’attribut inclus.attr de l’instance nb ne modifie pas l’attribut inclus.attr de l’instance nb2. import copy class classe_incluse: def __init__ (self) : = 3 class exemple_classe: def __init__ (self) : self.inclus = classe_incluse () = 42 def __copy__ (self): copie = exemple_classe () = copie.inclus = (self.inclus) return copie nb = exemple_classe () nb2 = (nb) # copie explicite, # utilise l’opérateur __copy__, # cette ligne est équivalente à # nb2 = nb.__copy__() print # affiche 42 print # affiche 42 print # affiche 3 print # affiche 3 = 0 = 1 print # affiche 1 print # affiche 42 print # affiche 0 print # affiche 3 (c’est le résultat souhaité) | Remarque 4.30 : affectation et copie def fonction_liste (): return range (4,7) # retourne la liste [4,5,6] l = fonction_liste () # la liste [4,5,6] n’est pas recopiée, # l’identificateur l lui est affecté Lorsqu’une fonction retourne un résultat mais que celui-ci n’est pas attribué à un nom de variable. Le langage Python détecte automatiquement que ce résultat n’est plus lié à aucune variable. Il est détruit automatiquement. def fonction_liste (): return range (4,7) | fonction_liste () # la liste [4,5,6] n’est pas recopiée, # elle n’est pas non plus attribuée à une variable, # elle est alors détruite automatiquement par le langage Python | 4.6.3 Listes et dictionnaires Les listes et les dictionnaires sont des types modifiables et aussi des classes. Par conséquent, l’affectation et la copie ont un comportement identique à celui des classes. l = [4,5,6] l2 = l print l | # affiche [4, 5, 6] | print l2 | # affiche [4, 5, 6] | l2 [1] = 10 print l | # affiche [4, 10, 6] | print l2 | # affiche [4, 10, 6] | Pour effectuer une copie, il faut écrire le code suivant : l = [4,5,6] import copy l2 = (l) print l # affiche [4, 5, 6] print l2 # affiche [4, 5, 6] l2 [1] = 10 print l # affiche [4, 5, 6] print l2 # affiche [4, 10, 6] | La fonction copy ne suffit pourtant pas lorsque l’objet à copier est par exemple une liste incluant d’autres objets. Elle copiera la liste et ne fera pas de copie des objets eux-mêmes. import copy l = [ [i] for i in range(0,3)] ll = (l) print l, " - ", ll # affiche [[0], [1], [2]] | - [[0], [1], [2]] | ll [0][0] = 6 print l, " - ", ll # affiche [[6], [1], [2]] | - [[6], [1], [2]] import copy l = [ [i] for i in range(0,3)] ll = copy.deepcopy (l) print l, " - ", ll # affiche [[0], [1], [2]] | - [[0], [1], [2]] | ll [0][0] = 6 print l, " - ", ll # affiche [[0], [1], [2]] | - [[6], [1], [2]] | 4.6.4 copy et deepcopy La fonction copy effectue une copie d’un objet, la fonction deepcopy effectue une copie d’un objet et de ceux qu’il contient. La fonction copy est associée à la méthode __copy__ tandis que la fonction deepcopy est associée à la méthode __deepcopy__. Il est rare que l’une de ces deux méthodes doivent être redéfinies. L’intérêt de ce paragraphe est plus de montrer le mécanisme que cache la fonction deepcopy qui est la raison pour laquelle il existe deux fonctions de copie et non une seule. syntaxe 4.31 : fonction deepcopy import copy memo = { } nom_copy = copy.deepcopy (nom_instance [,memo]) nom_instance est une instance à copier, nom_copy est le nom désignant la copie. memo est un paramètre facultatif : s’il est envoyé à la fonction deepcopy, il contiendra alors la liste de toutes les copies d’objet effectuées lors de cet appel. syntaxe 4.32 : classe, opérateur __deepcopy__ class nom_classe : def __deepcopy__ (self,memo) : copie = (self) # return copie nom_classe est le nom d’une classe. La méthode __deepcopy__ doit retourner une instance de la classe nom_classe, dans cet exemple, cette instance a pour nom copie. Le paramètre memo permet de conserver la liste des copies effectuées à condition d’appeler deepcopy avec un dictionnaire en paramètre. import copy class classe_incluse: def __init__ (self) : = 3 class exemple_classe: def __init__ (self) : self.inclus = classe_incluse () = 42 def __copy__ (self): copie = exemple_classe () = return copie def __deepcopy__ (self,memo): if self in memo : return memo [self] copie = (self) memo [self] = copie # mémorise la copie de self qui est copie | copie.inclus = copy.deepcopy (self.inclus,memo) return copie nb = exemple_classe () nb2 = copy.deepcopy (nb) # copie explicite à tous niveaux, # utilise l’opérateur __copy__, # cette ligne est équivalente à # nb2 = nb.__deepcopy__() print # affiche 42 print # affiche 42 print # affiche 3 print # affiche 3 = 0 = 1 print # affiche 1 print # affiche 42 print # affiche 0 print # affiche 3 # résultat souhaité | On peut se demander quel est l’intérêt de la méthode __deepcopy__ et surtout du paramètre memo modifié par la ligne memo[self] = copie. Ce détail est important lorsqu’un objet inclut un attribut égal à lui-même ou inclut un objet qui fait référence à l’objet de départ comme dans l’exemple qui suit. import copy class Objet1 : def __init__ (self, i) : self.i = i def __str__ (self) : return "o1 " + str (self.i) + " : " + str (self.o2.i) class Objet2 : def __init__ (self, i, o) : self.i = i self.o1 = o o.o2 = self def __str__ (self) : return "o2 " + str (self.i) + " : " + str (self.o1.i) def __deepcopy__ (self,memo) : return Objet2 (self.i, self.o1) o1 = Objet1 (1) o2 = Objet2 (2, o1) print o1 # affiche o1 1 : 2 print o2 # affiche o2 2 : 1 o3 = copy.deepcopy (o2) o3.i = 4 print o1 # affiche o1 1 : 4 --> on voudrait 2 print o2 # affiche o2 2 : 1 print o3 # affiche o2 4 : 1 import copy class Objet1 : def __init__ (self, i) : self.i = i def __str__ (self) : return "o1 " + str (self.i) + " : " + str (self.o2.i) def __deepcopy__ (self,memo={}) : if self in memo : return memo [self] r = Objet1 (self.i) memo [self] = r r.o2 = copy.deepcopy (self.o2, memo) return r class Objet2 : def __init__ (self, i, o) : self.i = i self.o1 = o o.o2 = self def __str__ (self) : return "o2 " + str (self.i) + " : " + str (self.o1.i) def __deepcopy__ (self,memo = {}) : if self in memo : return memo [self] r = Objet2 (self.i, self.o1) memo [self] = r r.o1 = copy.deepcopy (self.o1, memo) return r o1 = Objet1 (1) o2 = Objet2 (2, o1) print o1 # affiche o1 1 : 2 print o2 # affiche o2 2 : 1 o3 = copy.deepcopy (o2) o3.i = 4 print o1 # affiche o1 1 : 2 --> on a 2 cette fois-ci print o2 # affiche o2 2 : 1 print o3 # affiche o2 4 : 1 | Ces problématiques se rencontrent souvent lorsqu’on aborde le problème de la sérialisation d’un objet qui consiste à enregistrer tout objet dans un fichier, même si cet objet inclut des références à des objets qui font référence à lui-même. C’est ce qu’on appelle des références circulaires. L’enregistrement d’un tel objet avec des références circulaires et sa relecture depuis un fichier se résolvent avec les mêmes artifices que ceux proposés ici pour la copie. L’utilisation des opérateurs __copy__ et __deepcopy__ est peu fréquente. Les fonctions copy et deepcopy du module copy suffisent dans la plupart des cas. 4.7 Attributs figés syntaxe 4.33 : classe, __slots__ class nom_classe (object) : __slots__ = "attribut_1", , "attribut_n" nom_classe est le nom de la classe, elle doit hériter de object ou d’une classe qui en hérite elle-même (voir paragraphe 4.8). Il faut ensuite ajouter au début du corps de la classe la ligne __slots__ = ”attribut_1”, ,”attribut_n” où attribut_1 à attribut_n sont les noms des attributs de la classe. Aucun autre ne sera accepté. L’exemple suivant utilise cette syntaxe pour définir un point avec seulement trois attributs _x, _y, _z. class point_espace(object): __slots__ = "_x", "_y", "_z" def __init__ (self, x,y,z): self._x, self._y, self._z = x,y,z def __str__(self): return "(%f,%f,%f)" % (self._x, self._y, self._z) a = point_espace (1,-2,3) print a | Etant donné que la liste des attributs est figée, l’instruction a.j = 6 qui ajoute un attribut j à l’instance a déclenche une exception (voir paragraphe 5). La même erreur se déclenche si on cherche à ajouter cet attribut depuis une méthode (self.j = 6). Traceback (most recent call last): File "", line 15, in ? a.j = 6 AttributeError: ’point_espace’ object has no attribute ’j’ | L’attribut __dict__ n’existe pas non plus, par conséquent, l’expression a.__dict__ génère la même exception. La présence de l’instruction __slots__ = n’a aucun incidence sur les attributs statiques. 4.8 Héritage L’héritage est un des grands avantages de la programmation objet. Il permet de créer une classe à partir d’une autre en ajoutant des attributs, en modifiant ou en ajoutant des méthodes. En quelque sorte, on peut modifier des méthodes d’une classe tout en conservant la possibilité d’utiliser les anciennes versions. 4.8.1 Exemple autour de pièces de monnaie import random # extension interne incluant des fonctions # simulant des nombres aléatoires, # random.randint (a,b) --> retourne un nombre entier entre a et b # cette ligne doit être ajoutée à tous les exemples suivant # même si elle n’y figure plus def cent_tirages () : s = 0 for i in range (0,100) : s += random.randint (0,1) return s print cent_tirages () | On désire maintenant réaliser cette même expérience pour une pièce truquée pour laquelle la face pile sort avec une probabilité de 0,7. Une solution consiste à réécrire la fonction cent_tirages pour la pièce truquée. def cent_tirages () : s = 0 for i in range (0,100) : t = random.randint (0,10) if t >= 3 : s += 1 return s print cent_tirages () | Toutefois cette solution n’est pas satisfaisante car il faudrait réécrire cette fonction pour chaque pièce différente pour laquelle on voudrait réaliser cette expérience. Une autre solution consiste donc à passer en paramètre de la fonction cent_tirages une fonction qui reproduit le comportement d’une pièce, qu’elle soit normale ou truquée. def piece_normale () : return random.randint (0,1) def piece_truquee () : t = random.randint (0,10) if t >= 3 : return 1 else : return 0 def cent_tirages (piece) : s = 0 for i in range (0,100) : s += piece () return s print cent_tirages (piece_normale) print cent_tirages (piece_truquee) | Mais cette solution possède toujours un inconvénient car les fonctions associées à chaque pièce n’acceptent aucun paramètre. Il n’est pas possible de définir une pièce qui est normale si la face pile vient de sortir et qui devient truquée si la face face vient de sortir 5. On choisit alors de représenter une pièce normale par une classe. class piece_normale : def tirage (self) : return random.randint (0,1) def cent_tirages (self) : s = 0 for i in range (0,100) : s += self.tirage () return s | On peut aisément recopier et adapter ce code pour la pièce truquée. class piece_normale : def tirage (self) : return random.randint (0,1) def cent_tirages (self) : s = 0 for i in range (0,100) : s += self.tirage () return s class piece_truquee : def tirage (self) : t = random.randint (0,10) if t >= 3 : return 1 else : return 0 def cent_tirages (self) : s = 0 for i in range (0,100) : s += self.tirage () return s p = piece_normale () print p.cent_tirages () p2 = piece_truquee () print p2.cent_tirages () | Toutefois, pour les deux classes piece_normale et piece_truquee, la méthode cent_tirage est exactement la même. Il serait préférable de ne pas répéter ce code puisque si nous devions modifier la première - un nombre de tirages différent par 5. Il faudrait pour cela créer une variable globale. avant = 0 defpiece_tres_truquee() : globalavant ifavant == 0 : avant = piece_truquee() else : avant = piece_normale() returnavant C’est comme si la fonction piece_tres_truquee avait un paramètre caché. Cette solution n’est pas conseillée car c’est le genre de détail qu’on oublie par la suite. exemple -, il faudrait également modifier la seconde. La solution passe par l’héritage. On va définir la classe piece_truquee à partir de la classe piece_normale en remplaçant seulement la méthode tirage puisqu’elle est la seule à changer. On indique à la classe piece_truquee qu’elle hérite - ou dérive - de la classe piece_normale en mettant piece_normale entre parenthèses sur la ligne de la déclaration de la classe piece_truquee. Comme la méthode cent_tirages ne change pas, elle n’a pas besoin d’apparaître dans la définition de la nouvelle classe même si cette méthode est aussi applicable à une instance de la classe piece_truquee. class piece_normale : def tirage (self) : return random.randint (0,1) def cent_tirages (self) : s = 0 class piece_truquee (piece_normale) : def tirage (self) : t = random.randint (0,10) if t >= 3 : return 1 else : return 0 p = piece_normale () print p.cent_tirages () p2 = piece_truquee () print p2.cent_tirages () | Enfin, on peut définir une pièce très truquée qui devient truquée si face vient de sortir et qui redevient normale si pile vient de sortir. Cette pièce très truquée sera implémentée par la classe piece_tres_truquee. Elle doit contenir un attribut avant qui conserve la valeur du précédent tirage. Elle doit redéfinir la méthode tirage pour être une pièce normale ou truquée selon la valeur de l’attribut avant. Pour éviter de réécrire des méthodes déjà écrites, la méthode tirage de la classe piece_tres_truquee doit appeler la méthode tirage de la classe piece_truquee ou celle de la classe piece_normale selon la valeur de l’attribut avant. class piece_normale : def tirage (self) : return random.randint (0,1) def cent_tirages (self) : s = 0 for i in range (0,100) : s += self.tirage () return s class piece_truquee (piece_normale) : def tirage (self) : t = random.randint (0,10) if t >= 3 : return 1 else : return 0 class piece_tres_truquee (piece_truquee) : def __init__(self) : | # création de l’attribut avant self.avant = 0 def tirage (self) : if self.avant == 0 : # appel de la méthode tirage de la classe piece_truquee self.avant = piece_truquee.tirage (self) else : # appel de la méthode tirage de la classe piece_normale self.avant = piece_normale.tirage (self) return self.avant p = piece_normale () print "normale ", p.cent_tirages () p2 = piece_truquee () print "truquee ", p2.cent_tirages () p3 = piece_tres_truquee () print "tres truquee ", p3.cent_tirages () | L’héritage propose donc une manière élégante d’organiser un programme. Il rend possible la modification des classes d’un programme sans pour autant les altérer. Définition 4.34 : héritage La surcharge est un autre concept qui va de pair avec l’héritage. Elle consiste à redéfinir des méthodes déjà définies chez l’ancêtre. Cela permet de modifier le comportement de méthodes bien que celles-ci soient utilisées par d’autres méthodes dont la définition reste inchangée. Définition 4.35 : surcharge Lorsqu’une classe B hérite de la classe A et redéfinit une méthode de la classe A portant le même nom, on dit qu’elle surcharge cette méthode. S’il n’est pas explicitement précisé qu’on fait appel à une méthode d’une classe donnée, c’est toujours la méthode surchargée qui est exécutée. 4.8.2 Syntaxe syntaxe 4.36 : classe, héritage class nom_classe (nom_ancetre) : # corps de la classe # nom_classe désigne le nom d’une classe qui hérite ou dérive d’une autre classe nom_ancetre. Celle-ci nom_ancetre doit être une classe déjà définie. L’utilisation de la fonction help permet de connaître tous les ancêtres d’une classe. On applique cette fonction à la classe piece_tres_truquee définie au paragraphe précédent. help (piece_tres_truquee) On obtient le résultat suivant : Help on class piece_tres_truquee in module __main__: class piece_tres_truquee(piece_truquee) | Method resolution order: | piece_tres_truquee | piece_truquee | piece_normale | | Methods defined here: | | __init__(self) | | tirage(self) | | ---------------------------------------------------------------------| Methods inherited from piece_normale: | | cent_tirages(self) | Remarque 4.37 : Method resolution order L’attribut __bases__ d’une classe (voir paragraphe 4.3.2) contient le (ou les ancêtres, voir paragraphe 4.8.4). Il suffit d’interroger cet attribut pour savoir si une classe hérite d’une autre comme le montre l’exemple suivant. for l in piece_tres_truquee.__bases__ : print l # affiche __main__.piece_truquee print piece_normale in piece_tres_truquee.__bases__ # affiche False print piece_truquee in piece_tres_truquee.__bases__ # affiche True | La fonction issubclass permet d’obtenir un résultat équivalent. issubclass(A,B) indique si la classe A hérite directement ou indirectement de la classe B. Le paragraphe 4.8.5 revient sur cette fonction. print issubclass (piece_tres_truquee, piece_normale) # affiche True print issubclass (piece_truquee, piece_normale) # affiche True Dans les exemples précédents, piece_normale ne dérive d’aucune autre classe. Toutefois, le langage Python propose une classe d’objets dont héritent toutes les autres classes définies par le langage : c’est la classe object. Les paragraphes précédents ont montré qu’elle offrait certains avantages (voir paragraphe 4.5.4 sur les propriétés ou encore paragraphe 4.7 sur les attributs non liés). Le paragraphe précédent a montré qu’il était parfois utile d’appeler dans une méthode une autre méthode appartenant explicitement à l’ancêtre direct de cette classe ou à un de ses ancêtres. La syntaxe est la suivante. syntaxe 4.38 : classe, appel d’une méthode de l’ancêtre class nom_classe (nom_ancetre) : def nom_autre_methode (self, ) : # def nom_methode (self, ) : nom_ancetre.nom_methode (self, ) # appel de la méthode définie chez l’ancêtre nom_ancetre.nom_autre_methode (self, ) # appel d’une autre méthode définie chez l’ancêtre self.nom_autre_methode ( ) # appel d’une méthode surchargée Ces appels sont très fréquents en ce qui concerne les constructeurs qui appellent le constructeur de l’ancêtre. Il est même conseillé de le faire à chaque fois. class A : def __init__ (self) : self.x = 0 class B (A) : def __init__ (self) : A.__init__ (self) self.y = 0 | Remarque 4.39 : surcharge d’attributs Contrairement aux méthodes, la surcharge d’attributs n’est pas possible. Si un ancêtre possède un attribut d’identificateur a, les classes dérivées le possèdent aussi et ne peuvent en déclarer un autre du même nom. Cela tient au fait que quelque soit la méthode utilisée, celle de l’ancêtre ou celle d’une classe dérivée, c’est le même dictionnaire d’attributs __dict__ qui est utilisé. En revanche, si la classe ancêtre déclare un attribut dans son constructeur, il ne faut pas oublier de l’appeler dans le constructeur de la classe fille afin que cette attribut existe pour la classe fille. class ancetre : def __init__(self) : self.a = 5 def __str__ (self) : return "a = " + str (self.a) class fille (ancetre) : def __init__(self) : ancetre.__init__(self) # cette ligne est importante | # car sans elle, l’attribut a n’existe pas self.a += 1 def __str__ (self) : s = "a = " + str (self.a) return s x = ancetre () print x # affiche a = 5 y = fille () print y # affiche a = 6 | 4.8.3 Sens de l’héritage Il n’est pas toujours évident de concevoir le sens d’un héritage. En mathématique, le carré est un rectangle dont les côtés sont égaux. A priori, une classe carre doit dériver d’une classe rectangle. class rectangle : def __init__(self,a,b) : self.a,self.b = a,b class carre (rectangle) : def __init__( self, a) : rectangle.__init__ (self, a,a) r = rectangle (3,4) print r # affiche rectangle 3 x 4 c = carre (5) print c # affiche rectangle 5 x 5 | Toutefois, on peut aussi considérer que la classe carre contient une information redondante puisqu’elle possède deux attributs qui seront toujours égaux. On peut se demander s’il n’est pas préférable que la classe rectangle hérite de la classe carre. class carre : def __init__( self, a) : self.a = a def __str__ (self) : return "carre " + str (self.a) class rectangle (carre): def __init__(self,a,b) : carre.__init__(self, a) self.b = b def __str__ (self) : return "rectangle " + str (self.a) + " x " + str (self.b) r = rectangle (3,4) print r # affiche rectangle 3 x 4 c = carre (5) print c # affiche carre 5 | Cette seconde version minimise l’information à mémoriser puisque la classe carre ne contient qu’un seul attribut et non deux comme dans l’exemple précédent. Néanmoins, il a fallu surcharger l’opérateur __str__ afin d’afficher la nouvelle information. Il n’y a pas de meilleur choix parmi ces deux solutions proposées. La première solution va dans le sens des propriétés croissantes, les méthodes implémentées pour les classes de bases restent vraies pour les suivantes. La seconde solution va dans le sens des attributs croissants, des méthodes implémentées pour les classes de bases doivent souvent être adaptées pour les héritiers. En contrepartie, il n’y a pas d’information redondante. Ce problème d’héritage ne se pose pas à chaque fois. Dans l’exemple du paragraphe 4.8.1 autour des pièces truquées, il y a moins d’ambiguïté sur le sens de l’héritage. Celui-ci est guidé par le problème à résoudre qui s’avère plus simple à concevoir dans le sens d’un héritage d’une pièce normale vers une pièce truquée. 4.8.4 Héritage multiple Jusqu’à présent, tous les exemples d’héritages entre classes n’ont fait intervenir que deux classes, la classe ancêtre dont hérite la classe descendante. L’héritage multiple part du principe qu’il peut y avoir plusieurs ancêtres pour une même classe. La classe descendante hérite dans ce cas de tous les attributs et méthodes de tous ses ancêtres. Dans l’exemple qui suit, la classe C hérite des classes A et B. Elle hérite donc des méthodes de carre et cube. Chacune des classes A et B contient un constructeur qui initialise l’attribut a. Le constructeur de la classe C appelle le constructeur de la classe A pour initialiser cet attribut. class A : def __init__ (self) : self.a = 5 def carre (self) : return self.a ** 2 class B : def __init__ (self) : self.a = 6 def cube (self) : return self.a ** 3 class C (A,B) : def __init__ (self): A.__init__ (self) x = C () print x.carre () # affiche 25 print x.cube () # affiche 125 | Mais ces héritages multiples peuvent parfois apporter quelques ambiguïtés comme le cas où au moins deux ancêtres possèdent une méthode du même nom. Dans l’exemple qui suit, la classe C hérite toujours des classes A et B. Ces deux classes possèdent une méthode calcul. La classe C, qui hérite des deux, possède aussi une méthode calcul qui, par défaut, sera celle de la classe A. class A : def __init__ (self) : self.a = 5 def calcul (self) : return self.a ** 2 class B : def __init__ (self) : self.a = 6 def calcul (self) : return self.a ** 3 class C (A,B) : def __init__ (self): A.__init__ (self) x = C () print x.calcul () # affiche 25 | Cette information est disponible via la fonction help appliquée à la classe C. C’est dans ce genre de situations que l’information apportée par la section Methodresolutionorder est importante (voir remarque 4.37 page 127). | Method resolution order: | C | A | B | | Methods defined here: | | __init__(self) | | calcul(self) Pour préciser que la méthode calcul de la classe C doit appeler la méthode calcul de la classe B et non A, il faut l’écrire explicitement en surchargeant cette méthode. class A : def __init__ (self) : self.a = 5 def calcul (self) : return self.a ** 2 class B : def __init__ (self) : self.a = 6 def calcul (self) : return self.a ** 3 class C (A,B) : def __init__ (self): A.__init__ (self) def calcul (self) : return B.calcul (self) x = C () print x.calcul () # affiche 125 | Remarque 4.40 : héritage multiple et constructeur L’exemple précédent est un cas particulier où il n’est pas utile d’appeler les constructeurs des deux classes dont la classe C hérite mais c’est un cas particulier. Le constructeur de la classe C devrait être ainsi : class C (A,B) : def __init__ (self): A.__init__ (self) B.__init__ (self) | 4.8.5 Fonctions issubclass et isinstance La fonction issubclass permet de savoir si une classe hérite d’une autre. syntaxe 4.41 : fonction isubclass issubclass (B,A) Le résultat de cette fonction est vrai si la classe B hérite de la classe A, le résultat est faux dans tous les autres cas. La fonction prend comme argument des classes et non des instances de classes. L’exemple qui suit utilise cette fonction dont le résultat est vrai même pour des classes qui n’héritent pas directement l’une de l’autre. class A (object) : pass class B (A) : pass class C (B) : pass print issubclass (A, B) | # affiche False | print issubclass (B, A) | # affiche True | print issubclass (A, C) | # affiche False | print issubclass (C, A) | # affiche True a = A () b = B () print issubclass (a.__class__, B) | # affiche False | print issubclass (b.__class__, A) | # affiche True | print issubclass (a.__class__, A) | # affiche True | La fonction isinstance permet de savoir si une instance de classe est d’une type donné. Elle est équivalente à la fonction issubclass à ceci près qu’elle prend comme argument une instance et une classe. L’exemple précédent devient avec la fonction isinstance : a = A () b = B () print isinstance (a, B) | # affiche False | print isinstance (b, A) | # affiche True | print isinstance (a, A) | # affiche True | L’utilisation des fonctions issubclass et isinstance n’est pas très fréquente mais elle permet par exemple d’écrire une fonction qui peut prendre en entrée des types variables. def fonction_somme_list (ens) : r = "list " for e in ens : r += e return r def fonction_somme_dict (ens) : r = "dict " for k,v in ens.items () : r += v return r def fonction_somme (ens) : if isinstance (ens, dict) : return fonction_somme_dict (ens) elif isinstance (ens, list) : return fonction_somme_list (ens) else : return "erreur" li = ["un", "deux", "trois"] di = {1:"un", 2:"deux", 3:"trois"} tu = ("un", "deux", "trois") print fonction_somme (li) # affiche list undeuxtrois print fonction_somme (di) # affiche dict undeuxtrois print fonction_somme (tu) # affiche erreur | L’avantage est d’avoir une seule fonction capable de s’adapter à différents type de variables, y compris des types créés par un programmeur en utilisant les classes. 4.9 Compilation de classes s = """class carre : def __init__( self, a) : self.a = a def __str__ (self) : return "carre " + str (self.a) class rectangle (carre): def __init__(self,a,b) : carre.__init__(self, a) self.b = b def __str__ (self) : return "rectangle " + str (self.a) + " x " + str (self.b)""" obj = compile(s,"","exec") # code à compiler exec (obj) # classes incorporées au programme r = rectangle (3,4) print r # affiche rectangle 3 x 4 | c = carre (5) print c # affiche carre 5 Comme toute fonction, la fonction compile génère une exception lorsque la chaîne de caractères contient une erreur. Le programme qui suit essaye de compiler une chaîne de caractères confondant self et seilf. # coding: latin-1 """erreur de compilation incluses dans le code inséré dans la chaîne de caractère s""" s = """class carre : def __init__( self, a) : seilf.a = a # erreur de compilation def __str__ (self) : return "carre " + str (self.a) """ obj = compile(s,"variable s","exec") # code à compiler exec (obj) # classes incorporées au programme c = carre (5) print c # affiche carre 5 | L’exécution de ce programme affiche le message suivant : Traceback (most recent call last): File "C:\temp\", line 14, in -toplevelc = carre (5) File "variable s", line 3, in __init__ NameError: global name ’seilf’ is not defined 4.10 Constructions classiques 4.10.1 Héritage Le premier exemple est classique puisqu’il reprend le programme du paragraphe 3.7.10 de la page 92 pour le réécrire avec des classes et éviter de passer des fonctions comme paramètre d’une autre fonction. La première classe définit le module des suivantes. La méthode calcul n’accepte qu’un seul paramètre x mais pourrait également prendre en compte des constantes si celles-ci sont renseignées via le constructeur de la classe. C’est l’avantage de cette solution déjà illustrée par les pièces normales et truquées. class Fonction : def calcul (self, x) : pass def calcul_n_valeur (self, l) : res = [ self.calcul (i) for i in l ] return res | class Carre (Fonction) : def calcul (self, x) : return x*x class Cube (Fonction) : def calcul (self, x) : return x*x*x l = [0,1,2,3] print l # affiche [0, 1, 2, 3] l1 = Carre ().calcul_n_valeur (l) # l1 vaut [0, 1, 4, 9] l2 = Cube () .calcul_n_valeur (l) # l2 vaut [0, 1, 8, 27] | La version suivante mélange héritage et méthodes envoyées comme paramètre à une fonction. Il est préférable d’éviter cette construction même si elle est très utilisée par les interfaces graphiques. Elle n’est pas toujours transposable dans tous les langages de programmation tandis que le programme précédent aura un équivalent dans tous les langages objet. class Fonction : def calcul (self, x) : pass class Carre (Fonction) : def calcul (self, x) : return x*x class Cube (Fonction) : def calcul (self, x) : return x*x*x def calcul_n_valeur (l,f): res = [ f(i) for i in l ] return res l = [0,1,2,3] l1 = calcul_n_valeur (l, Carre ().calcul) # l1 vaut [0, 1, 4, 9] l2 = calcul_n_valeur (l, Cube ().calcul) # l2 vaut [0, 1, 8, 27] | 4.10.2 Deux lignées d’héritages La question est comment modéliser ces deux structures et ces deux multiplications sachant que les quatre paires structure - algorithme doivent fonctionner. On pourrait simplement créer deux classes faisant référence aux deux structures différentes et à l’intérieur de chacune d’entre elles, avoir deux méthodes de multiplication. Néanmoins, si une nouvelle structure ou un nouvel algorithme apparaît, la mise à jour peut être fastidieuse. Il est conseillé dans ce cas d’avoir quatre classes et de définir une interface d’échanges communes. L’algorithme de multiplication ne doit pas savoir quelle structure il manipule : il doit y accéder par des méthodes. De cette manière, c’est la classe qui indique l’algorithme choisi et non une méthode. Ajouter un troisième algorithme ou une troisième structure revient à ajouter une classe : l’interface d’échange ne change pas. Le programme pourrait suivre le schéma qui suit. class Matrice : def __init__ (self,lin,col,coef): , = lin, col # interface d’échange def get_lin () : return def get_col () : return def __getitem__(self,i,j): pass def __setitem__(self,i,j,v): pass def get_submat(self, i1,j1,i2,j2): pass def set_submat(self, i1,j1,mat): pass # fin de l’interface d’échange def trace (self) : t = 0 for i in xrange (0, ): t += self (i,i) return t class MatriceList (Matrice) : def __init__ (self,lin,col,coef): Matrice.__init__ (self, \ lin, col, coef) # def __getitem__ (self, i,j) : # def __setitem__ (self, i,j, v) : # def get_submat(self, i1,j1,i2,j2): # def set_submat(self, i1,j1,mat): # class MatriceDict (Matrice) : def __init__ (self,lin,col,coef): Matrice.__init__ (self, \ lin, col, coef) # def __getitem__ (self, i,j) : # def __setitem__ (self, i,j, v) : # def get_submat(self, i1,j1,i2,j2): # def set_submat(self, i1,j1,mat): # | | class Produit : def calcul (self, mat1, mat2): pass class ProduitClassique (Produit) : # return class ProduitStrassen (Produit) : def calcul (self, mat1,mat2): # return | | Cette construction autorise même la multiplication de matrices de structures différentes. Très répandue, cette architecture est souvent plus coûteuse au moment de la conception car il faut bien penser l’interface d’échange mais elle l’est beaucoup moins par la suite. Il existe d’autres assemblages de classes assez fréquents, regroupés sous le terme de Design Patterns. Pour peu que ceux-ci soient connus de celui qui conçoit un programme, sa relecture et sa compréhension en sont facilitées si les commentaires font mention du pattern utilisé. __cmp__(self,x) | Retourne un entier égale à -1, 0, 1, chacune de ces valeurs étant associés respectivement à : self < x, self == x, self > x. Cet opérateur est appelé par la fonction cmp. | __str__(self) | Convertit un objet en une chaîne de caractère qui sera affichée par la fonction print ou obtenu avec la fonction str. | __contains__(self,x) | Retourne True ou False selon que x appartient à self. Le mot-clé in renvoie à cet opérateur. En d’autres termes, if x in obj : appelle obj.__contains__(x). | __len__(self) | Retourne le nombre d’élément de self. Cet opérateur est appelé par la fonction len. | __abs__(self) | Cet opérateur est appelé par la fonction abs. | __getitem__(self,i) | Cet opérateur est appelé lorsqu’on cherche à accéder à un élément de l’objet self d’indice i comme si c’était une liste. Si l’indice i est incorrect, l’exception IndexError doit être levée. | __setitem__(self,i,v) | Cet opérateur est appelé lorsqu’on cherche à affecter une valeur v à un élément de l’objet self d’indice i comme si c’était une liste ou un dictionnaire. Si l’indice i est incorrect, l’exception IndexError doit être levée. | __delitem__(self,i) | __int__(self) __float__(self) __complex__(self) | Ces opérateurs implémente la conversion de l’instance self en entier, réel ou complexe. | __add__(self,x) __div__(self,x) __mul__(self,x) __sub__(self,x) __pow__(self,x) __lshift__(self,x) __rshift__(self,x) | Opérateurs appelés pour les opérations +, /, *, -, **, <<, >> | __iadd__(self,x) __idiv__(self,x) __imul__(self,x) __isub__(self,x) __ipow__(self,x) __ilshift__(self,x) __irshift__(self,x) | Opérateurs appelés pour les opérations +=, /=, *=, -=, **=, «=, »= | Table 4.1 : Opérateurs ou fonctions spéciales les plus utilisées. La page(voir égalementrecense toutes les fonctions spéciales qu’il est possible de redéfinir pour une classe.
Chapitre 5 Exceptions Le petit programme suivant déclenche une erreur parce qu’il effectue une division par zéro. Il déclenche une erreur ou ce qu’on appelle une exception. Traceback (most recent call last): File "", line 2, in ? y = 1.0 / x ZeroDivisionError: float division | Le mécanisme des exceptions permet au programme de "rattraper" les erreurs, de détecter qu’une erreur s’est produite et d’agir en conséquence afin que le programme ne s’arrête pas. 5.1 Principe des exceptions 5.1.1 Attraper toutes les erreurs Définition 5.1 : exception Une exception est un objet qui indique que le programme ne peut continuer son exécution. On décide par exemple qu’on veut rattraper toutes les erreurs du programme et afficher un message d’erreur. Le programme suivant appelle la fonction inverse qui retourne l’inverse d’un nombre. def inverse (x): y = 1.0 / x return y a = inverse (2) print a b = inverse (0) print b Traceback (most recent call last): File "", line 8, in ? b = inverse (0) File "", line 3, in inverse y = 1.0 / x ZeroDivisionError: float division | Afin de rattraper l’erreur, on insère le code susceptible de produire une erreur entre les mots clés try et except. def inverse (x): y = 1.0 / x return y try : a = inverse (2) print a b = inverse (0) # déclenche une exception print b except : print "le programme a déclenché une erreur" | Le programme essaye d’exécuter les quatre instructions incluses entre les instructions try et except. Si une erreur se produit, le programme exécute alors les lignes qui suivent l’instruction except. L’erreur se produit en fait à l’intérieur de la fonction inverse mais celle-ci est appelée à l’intérieur d’un code "protégé" contre les erreurs. Le programme précédent affiche les deux lignes suivantes. 0.5 le programme a déclenché une erreur Il est aussi possible d’ajouter une clause else qui sert de préfixe à une liste d’instructions qui ne sera exécutée que si aucune exception n’est déclenchée. def inverse (x): y = 1.0 / x return y try : print inverse (2) # pas d’erreur print inverse (1) # pas d’erreur non plus except : print "le programme a déclenché une erreur" else : print "tout s’est bien passé" | Ce dernier programme ne déclenche aucune exception et affiche les lignes suivantes. 0.5 1.0 tout s’est bien passé Pour résumer, la syntaxe suivante permet d’attraper toutes les erreurs qui se produisent pendant l’exécution d’une partie du programme. Cette syntaxe permet en quelque sorte de protéger cette partie du programme contre les erreurs. syntaxe 5.2 : exception try : # instructions à protéger except : # que faire en cas d’erreur else : # que faire lorsque aucune erreur n’est apparue Remarque 5.3 : plusieurs erreurs Lorsqu’une section de code est protégée contre les exceptions, si elle contient plusieurs erreurs, son exécution s’arrête à la première des erreurs découvertes. Par exemple, dès la première erreur qui correspond au calcul d’une puissance non entière d’un nombre négatif, l’exécution du programme suivant est dirigée vers l’instruction qui suit le mot-clé except. def inverse (x): y = 1.0 / x return y try : print (-2.1) ** 3.1 # première erreur print inverse (2) print inverse (0) # seconde erreur except : print "le programme a déclenché une erreur" | 5.1.2 Obtenir le type d’erreur, attraper un type d’exception Parfois, plusieurs types d’erreurs peuvent être déclenchés à l’intérieur d’une portion de code protégée. Pour avoir une information sur ce type, il est possible de récupérer une variable de type Exception. def inverse (x): y = 1.0 / x return y | try : print inverse (2) print inverse (0) except Exception, exc: print "exception de type ", exc.__class__ # affiche exception de type exceptions.ZeroDivisionError print "message ", exc # affiche le message associé à l’exception | Le programme précédent récupère une exception sous la forme d’une variable appelée exc. Cette variable est en fait une instance d’une classe d’erreur, exc.__class__ correspond au nom de cette classe. A l’aide de la fonction isinstance, il est possible d’exécuter des traitements différents selon le type d’erreur. def inverse (x): y = 1.0 / x return y try : print (-2.1) ** 3.1 # première erreur print inverse (2) print inverse (0) # seconde erreur except Exception, exc: if isinstance (exc, ZeroDivisionError) : print "division par zéro" else : print "erreur insoupçonnée : ", exc.__class__ print "message ", exc erreur insoupçonnée : exceptions.ValueError Une autre syntaxe plus simple permet d’attraper un type d’exception donné en accolant au mot-clé except le type de l’exception qu’on désire attraper. L’exemple précédent est équivalent au suivant mais syntaxiquement différent. def inverse (x): y = 1.0 / x return y try : print (-2.1) ** 3.1 print inverse (2) print inverse (0) except ZeroDivisionError: print "division par zéro" except Exception, exc: print "erreur insoupçonnée : ", exc.__class__ print "message ", exc | Cette syntaxe obéit au schéma qui suit. syntaxe 5.4 : exception d’un type donné try : # instructions à protéger except type_exception_1 : # que faire en cas d’erreur de type type_exception_1 except type_exception_i : # que faire en cas d’erreur de type type_exception_i except type_exception_n : # que faire en cas d’erreur de type type_exception_n except : # que faire en cas d’erreur d’un type différent de tous # les précédents types else : # que faire lorsque une erreur aucune erreur n’est apparue Toute erreur déclenchée alors que le programme exécute les instructions qui suivent le mot-clé try déclenche immédiatement l’exécution des lignes qui suivent un mot-clé except. Le programme compare le type d’exception aux types type_exception_1 à type_exception_n. S’il existe une correspondance alors ce sont les instructions de la clause except associée qui seront exécutées et uniquement ces instructions. La dernière clause except est facultative, elle est utile lorsque aucun type de ceux prévus ne correspond à l’exception générée. La clause else est aussi facultative. Si la dernière clause except n’est pas spécifiée et que l’exception déclenchée ne correspond à aucune de celle listée plus haut, le programme s’arrête sur cette erreur à moins que celle-ci ne soit attrapée plus tard. 5.1.3 Lancer une exception Lorsqu’une fonction détecte une erreur, il lui est possible de déclencher une exception par l’intermédiaire du mot-clé raise. La fonction inverse compare x à 0 et déclenche l’exception ValueError si x est nul. Cette exception est attrapée plus bas. def inverse (x): if x == 0 : raise ValueError y = 1.0 / x return y try : print inverse (0) # erreur except ValueError: print "erreur" | Il est parfois utile d’associer un message à une exception afin que l’utilisateur ne soit pas perdu. Le programme qui suit est identique au précédent à ceci près qu’il associe à l’exception ValueError qui précise l’erreur et mentionne la fonction où elle s’est produite. Le message est ensuite intercepté plus bas. def inverse (x): if x == 0 : raise ValueError ("valeur nulle interdite, fonction inverse") y = 1.0 / x return y try : print inverse (0) # erreur except ValueError, exc: print "erreur, message : ", exc | Le déclenchement d’une exception suit la syntaxe suivante. syntaxe 5.5 : exception, raise raise exception_type (message) : Cette instruction lance l’exception exception_type associée au message message. Le message est facultatif, lorsqu’il n’y en a pas, la syntaxe se résume à raiseexception_type. Et pour attraper cette exception et le message qui lui est associé, il faut utiliser la syntaxe 5.4 décrite au paragraphe précédent. 5.1.4 Héritage et exception L’instruction help(ZeroDivisionError) retourne l’aide associée à l’exception ZeroDivisionError. Celle-ci indique que l’exception ZeroDivisionError est en fait un cas particulier de l’exception ArithmeticError, elle-même un cas particulier de StandardError. class ZeroDivisionError(ArithmeticError) | Second argument to a division or modulo operation was zero. | | Method resolution order: | ZeroDivisionError | StandardError | Exception Toutes les exceptions sont des cas particuliers de l’exception de type Exception. C’est pourquoi l’instruction exceptException,e : attrape toutes les exceptions. L’instruction exceptArithmeticError : attrape toutes les erreurs de type ArithmeticError, ce qui inclut les erreurs de type ZeroDivisionError. Autrement dit, toute exception de type ZeroDivisionError est attrapée par les instructions suivantes : – exceptZeroDivisionError : – exceptArithmeticError : – exceptStandardError : – exceptException : Plus précisément, chaque exception est une classe qui dérive directement ou indirectement de la classe Exception. L’instruction exceptArithmeticError : par exemple attrape toutes les exceptions de type ArithmeticError et toutes celles qui en dérivent comme la classe ZeroDivisionError (voir également le paragraphe 5.2). 5.1.5 Instructions try, except imbriquées Comme pour les boucles, il est possible d’imbriquer les portions protégées de code les unes dans les autres. Dans l’exemple qui suit, la première erreur est l’appel à une fonction non définie, ce qui déclenche l’exception NameError. def inverse (x): y = 1.0 / x return y try : try : print inverses (0) # fonction inexistante --> exception NameError print inverse (0) # division par zéro --> ZeroDivisionError except NameError: print "appel à une fonction non définie" except ZeroDivisionError, exc: print "erreur ", exc | En revanche, dans le second exemple, les deux lignes printinverse(0) et printinverses(0) ont été permutées. La première exception déclenchée est la division par zéro. La première clause except n’interceptera pas cette erreur puisqu’elle n’est pas du type recherché. def inverse (x): y = 1.0 / x return y try : try : except NameError: print "appel à une fonction non définie" except ZeroDivisionError, exc: print "erreur ", exc | Une autre imbrication possible est l’appel à une fonction qui inclut déjà une partie de code protégée. L’exemple suivant appelle la fonction inverse qui intercepte les exceptions de type ZeroDivisionError pour retourner une grande valeur lorsque x = 0. La seconde exception générée survient lors de l’appel à la fonction inverses qui déclenche l’exception NameError, elle aussi interceptée. def inverse (x): try : y = 1.0 / x except ZeroDivisionError, exc: | print "erreur ", exc if x > 0 : return 1000000000 else : return -1000000000 return y try : print inverse (0) # division par zéro --> la fonction inverse sait gérer print inverses (0) # fonction inexistante --> exception NameError except NameError: print "appel à une fonction non définie" | 5.2 Définir ses propres exceptions 5.2.1 Dériver une classe d’exception Pour définir sa propre exception, il faut créer une classe qui dérive d’une classe d’exception existante (voir paragraphe 5.1.4), par exemple, la classe Exception. L’exemple suivant crée une exception AucunChiffre qui est lancée par la fonction conversion lorsque la chaîne de caractères qu’elle doit convertir ne contient pas que des chiffres. class AucunChiffre (Exception) : """chaîne de caractères contenant aussi autre chose que des chiffres""" def conversion (s) : """conversion d’une chaîne de caractères en entier""" if not s.isdigit () : raise AucunChiffre (s) return int (s) try : s = "123a" print s, " = ", conversion (s) except AucunChiffre, exc : # on affiche ici le commentaire associé à la classe d’exception # et le message associé print AucunChiffre.__doc__, " : ", exc | Remarque 5.6 : exception et opérateur __str__ class AucunChiffre (Exception) : """chaîne de caractères contenant aussi autre chose que des chiffres""" def __str__ (self) : return self.__doc__ + " " + Exception.__str__ (self) | 5.2.2 Personnalisation d’une classe d’exception Il est parfois utile qu’une exception contienne davantage d’informations qu’un simple message. L’exemple suivant reprend l’exemple du paragraphe précédent. L’exception AucunChiffre inclut cette fois-ci un paramètre supplémentaire contenant le nom de la fonction où l’erreur a été déclenchée. La classe AucunChiffre possède dorénavant un constructeur qui doit recevoir deux paramètres : une valeur et un nom de fonction. L’exception est levée à l’aide de l’instruction raiseAucunChiffre (s,”conversion”) qui regroupe dans un T-uple les paramètres à envoyer à l’exception. L’opérateur __str__ a été modifié de façon à ajouter ces deux informations dans le message associé à l’exception. Ainsi, l’instruction printexc présente à l’avant dernière ligne de cet exemple affiche un message plus complet. class AucunChiffre (Exception) : """chaîne de caractères contenant aussi autre chose que des chiffres""" def __init__(self, s, f = "") : Exception.__init__(self, s) self.s = s self.f = f def __str__(self) : return """exception AucunChiffre, depuis la fonction """ + self.f + \ " avec le paramètre " + self.s def conversion (s) : """conversion d’une chaîne de caractères en entier""" if not s.isdigit () : raise AucunChiffre (s, "conversion") return int (s) try : s = "123a" i = conversion (s) print s, " = ", i except AucunChiffre, exc : print exc print "fonction : ", exc.f | Etant donné que le programme déclenche une exception dans la section de code protégée, les deux derniers affichages sont les seuls exécutés correctement. Ils produisent les deux lignes qui suivent. 5.3 Exemples d’utilisation des exceptions 5.3.1 Les itérateurs Les itérateurs sont des outils qui permettent de parcourir des objets qui sont des ensembles, comme une liste, un dictionnaire. Ils fonctionnent toujours de la même manière. L’exemple déjà présenté au paragraphe 4.4.2, page 106) et repris en partie ici définit une classe contenant trois coordonnées, ainsi qu’un itérateur permettant de parcourir ces trois coordonnées. Arrivée à la troisième itération, l’exception StopIteration est déclenchée. Cette exception indique à une boucle for de s’arrêter. class point_espace: # class class_iter: def __init__ (self,ins): self._n = 0 self._ins = ins def __iter__ (self) : return self def next (self): if self._n <= 2: v = self._ins [self._n] self._n += 1 return v else : raise StopIteration def __iter__(self): return point_espace.class_iter (self) # | Cet exemple montre seulement que les exceptions n’interviennent pas seulement lors d’erreurs mais font parfois partie intégrante d’un algorithme. 5.3.2 Exception ou valeur aberrante Sans exception, une solution pour indiquer un cas de mauvaise utilisation d’une fonction est de retourner une valeur aberrante. Retourner -1 pour une fonction dont le résultat est nécessairement positif est une valeur aberrante. Cette convention permet de signifier à celui qui appelle la fonction que son appel n’a pu être traité correctement. Dans l’exemple qui suit, la fonction racine_carree retourne un couple de résultats, True ou False pour savoir si le calcul est possible, suivi du résultat qui n’a un sens que si True est retournée en première valeur. def racine_carree(x) : if x < 0 : return False, 0 else : return True, x ** 0.5 print racine_carree (-1) # (False, 0) print racine_carree (1) # (True, 1.0) def racine_carree(x) : if x < 0 : raise ValueError ("valeur négative") return x ** 0.5 print racine_carree (-1) # déclenche une exception print racine_carree (1) 5.3.3 Le piège des exceptions Ce paragraphe évoque certains problèmes lorsqu’une exception est levée. L’exemple utilise les fichiers décrits au chapitre 7. Lorsqu’une exception est levée à l’intérieur d’une fonction, l’exécution de celle-ci s’interrompt. Si l’exception est attrapée, le programme continue sans problème; les objets momentanément créés seront détruits par le garbage collector. Il faut pourtant faire attention dans le cas par exemple où l’exception est levée alors qu’un fichier est ouvert : il ne sera pas fermé. # coding: latin-1 def ajoute_resultat_division (nom, x, y) : """ajoute le résultat de la division x/y au fichier nom""" f = open (nom, "a") f.write (str (x) + "/" + str (y) + "= ") f.write ( str ((float (x)/y)) + "\n" ) # exception si y == 0 f.close () for i in range (0, 5) : try : print str (i-1) + "/" + str (i-2) ajoute_resultat_division ("", i-1,i-2) except Exception, e : print "erreur avec i = ", i, ",", e | Les écritures dans le fichier se font en mode ajout (”a”), le fichier ”essai.txt” contiendra tout ce qui aura été écrit. -1/-2 0/-1 1/0 erreur avec i = 2 , float division 2/1 3/2 | | -1/-2= 0.5 0/-1= 0.0 2/1= 2.0 3/2= 1.5 1/0= | côté affichage côté fichier Exception | Elle n’est en principe jamais explicitement générée par les fonctions Python mais elle permet d’attraper toutes les exceptions car toutes dérivent de la classe Exception. | AttributeError | Une référence à un attribut inexistant ou une affectation a échoué. | ArithmeticError | Une opération arithmétique a échoué. | FloatingPointError | Une opération avec des nombres réels a échoué. | IOError | Une opération concernant les entrées/sorties (Input/Output) a échoué. Cette erreur survient par exemple lorsqu’on cherche à lire un fichier qui n’existe pas. | ImportError | Cette erreur survient lorsqu’on cherche à importer un module qui n’existe pas (voir chapitre 6). | IndentationError | L’interpréteur ne peut interpréter une partie du programme à cause d’un problème d’indentation. Il n’est pas possible d’exécuter un programme mal indenté mais cette erreur peut se produire lors de l’utilisation de la fonction compile (voir le paragraphe 3.4.14.2, page 85). | IndexError | On utilise un index erroné pour accéder à un élément d’une liste, d’un dictionnaire ou de tout autre tableau. | KeyError | Une clé est utilisée pour accéder à un élément d’un dictionnaire dont elle ne fait pas partie. | NameError | On utilise une variable, une fonction, une classe qui n’existe pas. | OverflowError | Un calcul sur des entiers ou des réels dépasse les capacités de codage des entiers ou des réels. | StopIteration | Cette exception est utilisée pour signifier qu’un itérateur atteint la fin d’un ensemble, un tableau, un dictionnaire. | TypeError | Erreur de type, une fonction est appliquée sur un objet qu’elle n’est pas censée manipuler. | UnicodeError | ValueError | Cette exception survient lorsqu’une valeur est inappropriée pour une certaine opération, par exemple, l’obtention du logarithme d’un nombre négatif. | ZeroDivisionError | Cette exception survient pour une division par zéro. | Table 5.1 : Exceptions standard les plus couramment utilisées. Ces exceptions sont définies par le langage Python. Il n’est pas nécessaire d’inclure un module. La pagerecense toutes les exceptions prédéfinies.
Chapitre 6 Modules Il est souvent préférable de répartir le code d’un grand programme sur plusieurs fichiers. Parmi tous ces fichiers, un seul est considéré comme fichier principal, il contient son point d’entrée, les premières instructions exécutées. Les autres fichiers sont considérés comme des modules, en quelque sorte, des annexes qui contiennent tout ce dont le fichier principal a besoin. 6.1 Modules et fichiers 6.1.1 Exemple Cet exemple montre comment répartir un programme sur deux fichiers. Le premier est appelé module car il n’inclut pas le point d’entrée du programme. Définition 6.1 : point d’entrée du programme Le point d’entrée d’un programme est la première instruction exécutée par l’ordinateur lors de l’exécution de ce programme. Cet exemple de module contient une fonction, une classe et une variable. Ces trois éléments peuvent être utilisés par n’importe quel fichier qui importe ce module. Le nom d’un module correspond au nom du fichier sans son extension. Fichier : module_exemple.py Fichier : exemple.py # coding: latin-1 """exemple de module, aide associée""" exemple_variable = 3 def exemple_fonction () : """exemple de fonction""" return 0 return "exemple_classe" | | import module_exemple c = module_exemple.exemple_classe () print c print module_exemple.exemple_fonction() help (module_exemple) | Pour importer un module, il suffit d’insérer l’instruction importnom_module avant d’utiliser une des choses qu’il définit. Ces importations sont souvent regroupées au début du programme, elles sont de cette façon mises en évidence même s’il est possible de les faire n’importe où. L’exemple ci-dessus à droite importe le module défini à gauche. Les modules commencent le plus souvent par une chaîne de caractères comme dans l’exemple précédent, celle-ci contient l’aide associée à ce module. Elle apparaît avec l’instruction help(module_exemple). Rien ne différencie les deux fichiers module_exemple.py et exemple.py excepté le fait que le second utilise des éléments définis par le premier. Dans un programme composé de plusieurs fichiers, un seul contient le point d’entrée et tous les autres sont des modules. La syntaxe d’appel d’un élément d’un module est identique à celle d’une classe. On peut considérer un module comme une classe avec ses méthodes et ses attributs à la seule différence qu’il ne peut y avoir qu’une seule instance d’un même module. La répétition de l’instruction importmodule_exemple n’aura aucun effet : un module n’est importé que lors de la première instruction importnom_module rencontré lors de l’exécution du programme. Remarque 6.2 : fichier ?.pyc Remarque 6.3 : recharger un module Le module module_exemple contient une variable exemple_variable peut être modifiée au cours de l’exécution du programme. Il est possible de revenir à sa valeur initiale en forçant Python à recharger le module grâce à la fonction reload. import module_exemple module_exemple.exemple_variable = 10 reload (module_exemple) print module_exemple.exemple_variable # affiche 3 6.1.2 Autres syntaxes Il existe trois syntaxes différentes pour importer un module. La première est décrite au paragraphe précédent. Il en existe une autre qui permet d’affecter à un module un identificateur différent du nom du fichier dans lequel il est décrit. En ajoutant l’instruction as suivi d’un autre nom alias, le module sera désigné par la suite par l’identificateur alias comme le montre l’exemple suivant. Fichier : exemple2.py import module_exemple as alias c = alias.exemple_classe () print c print alias.exemple_fonction () help (alias) Une autre syntaxe permet de se passer d’identificateur pour un module en utilisant le mot-clé from. En utilisant la syntaxe frommoduleimport ?, tous les identificateurs (fonctions, classes, variables) sont directement accessibles sans les faire précéder d’un identificateur de module ainsi que le montre l’exemple suivant. Fichier : exemple3.py from module_exemple import * c = exemple_classe () print c print exemple_fonction () Il existe une dernière syntaxe d’importation d’un module qui est utile quand on ne sait pas encore au moment d’écriture du programme le nom du module à importer. Celui-ci sera précisé à l’aide d’une chaîne de caractères au moyen de la fonction __import__. Fichier : exemple4.py alias = __import__ ("module_exemple") c = alias.exemple_classe () print c print alias.exemple_fonction () help (alias) | 6.1.3 Nom d’un module Le nom d’un module est défini par le nom du fichier sous lequel il est enregistré. Dans l’exemple du paragraphe précédent, le module avait pour nom de fichier module_exemple.py, le nom de ce module est donc module_exemple. Néanmoins, ce module peut également être exécuté comme un programme normal. Si tel est le cas, son nom devient __main__. C’est pourquoi, les quelques lignes qui suivent apparaissent souvent. Elles ne sont exécutées que si ce fichier a pour nom __main__. Un seul fichier peut porter ce nom : celui qui contient le point d’entrée. if __name__ == "__main__" : print "ce fichier est le programme principal" | Cette astuce est régulièrement utilisée pour tester les fonctions et classes définies dans un module. Etant donné que cette partie n’est exécutée que si ce fichier est le programme principal, ajouter du code après le test if __name__ == ”__main__” : n’a aucune incidence sur tout programme incluant ce fichier comme module. 6.1.4 Emplacement d’un module Lorsque le module est placé dans le même répertoire que le programme qui l’utilise, l’instruction importnom_module_sans_extension suffit. Cette instruction suffit également si ce module est placé dans le répertoire site ? packages présent dans le répertoire d’installation de Python. Si ce n’est pas le cas, il faut préciser à l’interpréteur Python où il doit chercher ce module : import sys .append ( [0] + "/../common") | La variable sys.path contient les répertoires où Python va chercher les modules. Le premier d’entre eux est le répertoire du programme. Il suffit d’ajouter à cette liste le répertoire désiré, ici, un répertoire appelé common situé au même niveau que le répertoire du programme. A ce sujet, il est conseillé d’utiliser le plus souvent possible des chemins relatifs et non absolus . De cette façon, on peut recopier le programme et ses modules à un autre endroit du disque dur sans altérer leur fonctionnement. 6.1.5 Ajouter un module en cours d’exécution De la même façon que Python est capable d’inclure de nouvelles portions de code en cours d’exécution , il est également capable d’inclure en cours d’exécution des modules dont on ne connaît pas le nom au début de l’exécution. Cela s’effectue grâce à la fonction __import__ déjà présentée ci-dessus. Néanmoins, cette fonction ne peut pas importer un module si celui-ci est désigné par un nom de fichier incluant son répertoire. Il faut d’abord déterminer le répertoire où est le module grâce à la fonction split du module os.path. Le programme suivant illustre cette possibilité en proposant une fonction qui importe un module connaissant le nom du fichier qui le contient. # coding: latin-1 def import_fichier (module) : import import sys if .exists (module) : # on teste l’existence du fichier folder,name = .split (module) # on obtient le répertoire du module if folder not in : .append (folder) # on ajoute le répertoire dans la liste # des répertoires autorisés name = name.replace (".py", "") # on enlève l’extension module = __import__ (name) # on importe le module return module else : | # on importe un module mod = import_fichier (r"D:\Dupre\informatique\programme\") # on affiche l’aide associée help (mod) | 6.1.6 Liste des modules importés Le dictionnaire modules du module sys contient l’ensemble des modules importés. Le programme suivant affiche cette liste. import sys for m in sys.modules : print m, " " * (14 - len(str(m))), sys.modules [m] | os | <module ’os’ from ’c:\python26\lib\’> | | <module ’ntpath’ from ’c:\python26\lib\’> | re | <module ’re’ from ’c:\python26\lib\’> | site | <module ’site’ from ’c:\python26\lib\’> | sys | <module ’sys’ (built-in)> | types | <module ’types’ from ’c:\python26\lib\’> | Lorsque le programme stipule l’import d’un module, Python vérifie s’il n’est pas déjà présent dans cette liste. Dans le cas contraire, il l’importe. Chaque module n’est importé qu’une seule fois. La première instruction importmodule_exemple rencontrée introduit une nouvelle entrée dans le dictionnaire sys.modules : module_exemple <module ’module_exemple’ from ’D:\python_cours\’> Le dictionnaire sys.modules peut être utilisé pour vérifier la présence d’un module ou lui assigner un autre identificateur. Un module est un objet qui n’autorise qu’une seule instance. if "module_exemple" in sys.modules : m = sys.modules ["module_exemple"] m.exemple_fonction () | 6.1.7 Attributs communs à tout module Une fois importés, tous les modules possèdent cinq attributs qui contiennent des informations comme leur nom, le chemin du fichier correspondant, l’aide associée. __all__ | Contient toutes les variables, fonctions, classes du module. | __builtins__ | Ce dictionnaire contient toutes les fonctions et classes inhérentes au langage Python utilisées par le module. | __doc__ | __file__ | Contient le nom du fichier qui définit le module. Son extension est pyc). | __name__ | Cette variable contient a priori le nom du module sauf si le module est le point d’entrée du programme auquel cas cette variable contient ”__main__”. | Ces attributs sont accessibles si le nom du module est utilisé comme préfixe. Sans préfixe, ce sont ceux du module lui-même. import os print os.__name__, os.__doc__ if __name__ == "__main__" : print "ce fichier est le point d’entrée" else : print "ce fichier est importé" 6.1.8 Arborescence de modules, paquetage Lorsque le nombre de modules devient conséquent, il est parfois souhaitable de répartir tous ces fichiers dans plusieurs répertoires. Il faudrait alors inclure tous ces répertoires dans la liste sys.path du module sys ce qui paraît fastidieux. Python propose la définition de paquetage, ce dernier englobe tous les fichiers Python d’un répertoire à condition que celui-ci contienne un fichier __init__.py qui peut être vide. La figure 6.1 présente une telle organisation et l’exemple suivant explicite comment importer chacun de ces fichiers sans avoir à modifier les chemins d’importation. Figure 6.1 : Arborescence de modules, un paquetage est défini par un ensemble de fichiers Python et d’un fichier __init__.py. Les répertoires sont grisées tandis que les fichiers apparaissent avec leur extension. import mesmodules.extension import mesmodules.part1.niveaudeux import mesmodules.part2.niveaudeuxbis Lors de la première instruction importmesmodules.extension, le langage Python ne s’intéresse pas qu’au seul fichier extension.py, il exécute également le contenu du fichier __init__.py. Si cela est nécessaire, c’est ici qu’il faut insérer les instructions à exécuter avant l’import de n’importe quel module du paquetage. 6.2 Modules internes ? Le programme suivant calcule l’intégrale de Monte Carlo de la fonction f(x) = x qui consiste à tirer des nombres aléatoires dans l’intervalle? a,b puis à faire la moyenne des x obtenu. import random # import du module random : simulation du hasard import math # import du module math : fonctions mathématiques def integrale_monte_carlo (a,b,f,n) : somme = 0.0 for i in range (0,n) : x = random.random () * (b-a) + a y = f(x) somme += f(x) return somme / n def racine (x) : return (x) print integrale (0,1,racine,100000) | Le programme suivant utilise le module urllib pour télécharger le contenu d’une page et l’afficher. def get_page_html (url): import urllib d = urllib.urlopen(url) res = d.read () d.close () return res url = "" print get_page_html (url) | 6.3 Modules externes Les modules externes ne sont pas fournis avec Python, ils nécessitent une installation supplémentaire. Il serait impossible de couvrir tous les thèmes abordés par ces extensions. La simplicité d’utilisation du langage Python et son interfaçage facile avec le langage C contribue à sa popularité. Il permet de relier entre eux des projets conçus dans des environnements différents, dans des langages différents. Depuis les versions 2.3, 2.4 du langage Python, la plupart des modules externes sont faciles à installer, faciles à utiliser d’après les exemples que fournissent de plus en plus les sites Internet qui les hébergent. De plus, il s’écoule peu de temps entre la mise à disposition d’une nouvelle version du langage Python et la mise à jour du module pour cette version . Le paragraphe 1.6 (page 24) donne une liste de modules utiles du point de vue d’un ingénieur généraliste. Concernant leur installation, certains modules externes comme SciPy peuvent être installés à l’aide d’un fichier exécutable sous Windows. Il suffit d’exécuter ce fichier pour pouvoir se servir du module par la suite dans un programme Python. Ce mode d’installation est disponible pour la plupart des modules de taille conséquente. D’autres modules apparaissent compressés dans un fichier. Une fois décompressés, ils incluent un fichier setup.py. Le langage Python fournit une procédure d’installation standard : il suffit d’écrire quelques lignes dans une fenêtre de commande ouverte dans le répertoire où a été décompressé le fichier contenant le module à installer. c:\python26\python install Sous Linux et Mac OS X, cette ligne devient : 6.4 Python et les autres langages C’est pour accélérer l’exécution que le langage Python est parfois associé à d’autres langages. Le programme final écrit en Python utilise des fonctionnalités haut niveau codées dans un autre langage plus rapide. 6.4.1 Langage Java Il est possible d’utiliser des classes Java en Python. Cette possibilité ne sera pas plus détaillée ici car le langage Java est également un langage interprété même s’il est nettement plus rapide. Pour cela, il faut utiliser une version de l’interpréteur Python codée lui-même en Java ce qui est le cas de la version Java de Python : Jython . 6.4.2 Langage C Il existe deux façons de construire un programme mêlant Python et langage C. La première méthode est la réalisation de modules Python écrits en C ou C++. A moins de devoir construire un module le plus efficace possible, il est peu conseillé de le faire directement car cela implique de gérer soi-même le référencement des variables créées en C ou C++ et exportées vers Python . Il est préférable d’utiliser des outils comme la librairie SWIG , ou encore la librairie Boost Python qui simplifie le travail d’intégration. C’est cette dernière option qui est détaillée dans ce livre. L’utilisation d’un autre langage est en effet indispensable lorsque l’exigence de rapidité est trop grande. Les modules scientifiques tels que scipy ou numpy ne répondent pas à tous les besoins. Il faut parfois développer soi-même des outils numériques en C++ même si la partie interface est assurée par Python. D’une manière générale, il est préférable de scinder nettement les parties d’un même programme qui s’occupent de calculs numériques de celles qui prennent en charge l’interface graphique. Construire un module écrit en C++ pour gérer les calculs numériques est une façon drastique et efficace d’opérer cette scission. 6.5 Boost Python La librairie Boost est une librairie écrite en C++ qui a l’avantage d’être portable : elle est utilisable sur les systèmes d’exploitation les plus courants. Régulièrement mise à jour, elle propose une extension de la librairie Standard Template Library (STL) incluant notamment les expressions régulières, les graphes, les threads, Boost Python est une partie de cette librairie qui se présente également sous forme de template . Il est préférable d’avoir programmé en C++ et en Python pour pouvoir mettre en œuvre les exemples proposés ci-dessous. La prise en main nécessite un investissement, une certaine habitude des longs messages d’erreurs issus des template mais une fois que cela est fait, la librairie accélère notablement l’écriture d’un module. Il n’est plus utile de se soucier de l’interfaçage avec Python et notamment du référencement déréférencement des objets pour le garbage collector14. Remarque 6.4 : utilisation d’un logiciel de suivi de source Le nombre de fichiers est assez important, il est conseillé de conserver l’historique des modifications grâce à un logiciel de suivi de source tels que TortoiseSVN15. Cela évite de perdre parfois une demi-journée de travail. 6.5.1 Exemple Un point important est l’utilisation de la librairie c : \python26\python26.lib qui explique qu’un module compilé pour une version donnée de Python ne puisse être utilisé avec une autre version : un module compilé avec la version 2.4 ne fonctionnera pas avec la version 2.6. L’exemple proposé contient également différentes fonctions de conversion des structures Python vers des containers de la STL ou Standard Template Library. Il suffit d’ouvrir le fichier pythonsample.sln avec Microsoft Visual C++ pour avoir accès au code source, c’est ce que montre la figure 6.2. Le fichier exesample.sln définit un programme exécutable permettant de tester le module sans Python (voir figure 6.3). Les paragraphes qui suivent présentent quelques éléments de syntaxe et se concluent par un exercice qui consiste à ajouter une fonction au module. 6.5.2 Les grandes lignes Un module construit avec Boost Python se compose de trois éléments, une inamovible DLL boost_python.dll, un fichier <module>.py portant le nom du module, une DLL <module>.dll. Les deux premiers fichiers ne changent pas, le dernier contient les classes et fonctions proposées par le module. L’organisation proposée dans l’exemple du paragraphe 6.5.1 suit le schéma de la figure 6.3. L’utilisation d’un permettent de factoriser le code de ces deux classes. 14. Le langage Python détruit automatique les objets qui ne sont plus utilisés. Cette fonctionnalité s’appuie sur un référencement de tous les objets créés. Alors qu’il est implicite en Python, il est explicite dans un module Python écrit en langage C à moins d’utiliser une librairie comme Boost Python qui prend en charge une partie de cette tâche (voir la page http: #objects-types-and-reference-counts). 15. 16. 17. L’adresse suivante fournit une aide plus complète que ce document : . Figure 6.3 : Schéma général de l’exemple introduit au paragraphe 6.5.1. Le code de la librairie est utilisé selon deux façons, à l’intérieur d’un module Python ce qui est l’objectif recherché et à l’intérieur d’un programme exécutable utilisé pour tester cette librairie sans passer par Python. Une fois le module prêt, il est utilisable dans tout programme Python. Il suffit de l’importer. Lors de cette étape, une fonction va être exécutée pour indiquer au langage Python l’ensemble des classes et des fonctions présentes dans le module. 6.5.2.1 Initialiser le module L’initialisation d’un module correspond à la déclaration au langage Python de l’ensemble des classes et des fonctions à exporter. Cette partie peut contenir des tâches qui doivent être faites la première fois que Python importe le module (initialisation de variables globales par exemple, chargement de données, ). L’objectif de cette initialisation est de construire pour chaque fonction ou classe ou méthode un objet Python correspondant. BOOST_PYTHON_MODULE(PythonSample) { // étapes d’initialisation } ; | 6.5.2.2 Ajout d’une fonction La fonction boost :: python :: def permet de déclarer une fonction. On peut y préciser les arguments, des valeurs par défaut, un message d’aide retourné par l’instruction Python help. int fonction (int r, const char * s, double x = 4) ; BOOST_PYTHON_MODULE(PythonSample) { boost::python::def ("fonction", fonction, (boost::python::arg ("r"), boost::python::arg ("s"), boost::python::arg ("x") = 4), "aide associée à la fonction") ; } ; Les arguments de boost :: python :: def sont les suivants : 1. Le premier argument est le nom de la fonction exportée en Python. Ce nom peut tout à fait être différent de celui de la fonction C++. 2. Le second argument est la fonction elle-même. 4. Le dernier argument est l’aide associée à la fonction. Côté Python, cela donne : import PythonSample print PythonSample.fonction (4, "second") print PythonSample.fonction (4, "second", 5) La surcharge de fonction est possible avec Boost Python mais elle ne sera pas détaillée ici. Le langage Python n’autorise pas la surcharge, la rendre possible revient à créer une fonction qui appellera telle ou telle autre fonction selon le type et le nombre des paramètres qu’elle aura reçus. 6.5.2.3 Ajout d’une classe La déclaration d’une classe suit le même schéma que pour une fonction mais avec le template boost :: python :: class_. Lorsque le constructeur n’a pas d’argument, la déclaration suit le schéma suivant : boost::python::class_<PythonClassSample> obj ( "ClassSample", "help on PythonClassSample") ) ; | Le template class_ est instancié sur la classe à exporter, ici PythonClassSample. On crée un objet obj de type boost :: python :: class_ < PythonClassSample >. Son constructeur prend comme arguments : 1. Le nom de la classe en Python. 2. L’aide qui lui est associée. Ce second paramètre est facultatif mais il est conseillé de le renseigner. Au cas où la classe aurait un constructeur avec des paramètres, il faudrait ajouter le code suivant : boost::python::class_<PythonClassSample> obj ( "ClassSample", "help on PythonClassSample", boost::python::init<int,const char*> ( | // ajout | (boost::python::arg ("a"), | // ajout | boost::python::arg ("s") = "default value for s"), | // ajout | "help on PythonClassSample constructor" ) ) ; | // ajout boost::python::class_<PythonClassSample, boost::python::bases<ClassBase> > obj ( "ClassSample", "help on PythonClassSample") ; | Le template class_ peut recevoir un autre argument qui est boost :: python :: bases < ClassBase > avec ClassBase désignant la classe mère de la classe déclarée. Cette dernière doit avoir été déclarée au préalable avec cette même syntaxe boost :: python :: class_. boost::python::class_<PythonClassSample, boost::python::bases<ClassBase> > obj ( // Remarque 6.5 : template C++ Lorsqu’un template est un argument d’un autre template, il faut insérer un espace entre deux symboles < ou > consécutifs pour éviter la confusion avec les opérateurs << ou >> qui existent en C++. 6.5.2.4 Ajout d’une méthode La déclaration d’une classe passe par la déclaration d’une instance du template class_. Dans les exemples utilisés, cette instance porte le nom obj. C’est cette instance qui va permettre de déclarer une méthode exactement de la même manière qu’une fonction : ( "Random", &PythonClassSample::Random, (boost::python::arg ("pos")), "help on the method") ; | Cette même méthode def permet également de surcharge le constructeur au cas où la classe pourrait être initialisée de différentes manières. Cela donne : boost::python::class_<MyVector> obj ("Vector", "help on vector", boost::python::init<int> ( (PY_ARG ("n")), "crée un vecteur de dimension n")) ; // ajout d’un constructeur sans paramètre (boost::python::init<MyVector> ()) ; | La classe Python Vector peut être créée avec un ou aucun paramètre : v = PythonSample.Vector () v2 = PythonSample.Vector (10) 6.5.2.5 Ajout d’un attribut La déclaration d’un attribut s’effectue avec la méthode def_readwrite : Le premier paramètre sera son nom sous Python, le second le paramètre lui-même, le troisième l’aide associée. La méthode def_readwrite permet d’exporter un attribut de sorte qu’il soit lisible et modifiable. 6.5.3 Exemple concret : ajout d’une fonction L’objectif est d’ajouter une fonction au module. La première étape consiste à décompresser le fichier téléchargé , à compiler l’ensemble des fichiers sous Microsoft Visual C++ en version release et debug. L’exécution du programme PythonSample/test/test.py doit s’effectuer sans problème. Ajouter une fonction revient ensuite à suivre les étapes suivantes : 1. Déclarer la fonction dans le fichier libSample/mafonction.h qu’on crée. 2. Implémenter la fonction dans le fichier libSample/mafonction.cpp qu’on crée également. La partie purement C++ est terminée , il faut maintenant définir la transition C++?Python grâce aux étapes suivantes : 1. On déclare une fonction dans un fichier PythonSample/mafonction.h qu’on crée. Elle peut par exemple porter le même nom que la précédente mais préfixé par python_. 2. On peut passer à l’implémentation de la fonction Python dans un fichier PythonSample/mafonction.cpp qu’on crée aussi. Le code est sensiblement le même pour chaque fonction ajoutée : il faut convertir les paramètres depuis des structures Python en type C++, appeler la fonction C++ puis effectuer la conversion inverse. On pourra s’aider des fonctions contenues dans le fichier PythonSample/python/conversion.h. 3. Il suffit de déclarer la fonction au sein de la macro BOOST_PYTHON_MODULE dans le fichier PythonSample/python/definition.cpp. Il ne reste plus qu’à tester la fonction nouvellement incluse depuis le programme PythonSample/test/test.py. Le contenu de chaque fichier modifié lors de ces étapes est présenté par la figure 6.4 page 168. Le langage Python gère lui-même la destruction des objets au travers d’un garbage collector. Cela signifie qu’un objet créé en Python provenant d’un module défini dans module en C++ sera lui aussi détruit par Python. A partir du moment où le destructeur d’un objet libère les pointeurs qu’il a alloué, la mémoire ne sera pas corrompue. Ce schéma ne permet pas de prévoir quand l’objet sera détruit. Pour parer aux problèmes éventuels qui pourraient survenir notamment lorsqu’un objet contient des pointeurs sur des données détenues par un autre objet, il est possible de spécifier à Boost Python qu’un résultat est une référence : son identificateur Python peut être détruit mais pas l’objet qu’il désigne. C’est l’objet du template boost :: python :: return_internal_reference <> (). Il est utilisé pour spécifier qu’une fonction retourne un résultat qui ne doit pas être détruit. Par exemple, on utilise cette fonctionnalité pour définir un opérateur d’affectation comme l’opérateur +=. La fonction suivante a la même signature C++ que l’opérateur +=. class PythonClassSample { PythonClassSample & __iadd__ (const PythonClassSample &a) ; } ; | Son résultat est une référence sur un objet existant. Si aucune mention spécifique n’est précisée lors de l’export de la méthode (avec def), lors d’un appel à l’opérateur +=, Python va considérer que le résultat est un objet différent alors qu’il s’agit de deux identificateurs Python faisant référence au même objet C++. C’est pour cela qu’on ajoute l’argument suivant à la méthode def : x.def ("__iadd__", &PythonClassSample::__iadd__, boost::python::return_internal_reference<>(), "addition") ; 6.5.5 Utilisation, installation Le module terminé se compose de trois fichiers : 1. la DLL compilée avec le code C++ du module, 2. un fichier portant le même nom que la DLL mais d’extension .py : PythonSample.py, 3. un fichier __init__.py. Voici ce fichier pour l’exemple présenté au paragraphe 6.5.1. La fonction importante de ce fichier est load_dynamic qui charge la DLL en mémoire au cas où elle n’aurait jamais été chargée. import sys if "PythonSample" not in sys.modules : PythonSample = imp.load_dynamic (’PythonSample’, ) sys.modules ["PythonSample"] = PythonSample | 6.5.6 Détails sur l’exemple du paragraphe 6.5.1 Certains fichiers exposés ici ont été simplifiés par rapport à l’exemple téléchargeable. Celui-ci a été configuré de manière à produire deux DLL, une autre pour la version release PythonSample.dll et une pour la version debug PythonSampled.dll. Ceci permet d’utiliser l’une ou l’autre des versions dans le même programme. La version debug facilite la correction des erreurs qui pourraient survenir lors de l’utilisation du module sous Python(voir le paragraphe 6.5.7). Il suffit alors d’écrire : import PythonSampled as PythonSample # au lieu de import PythonSample De cette façon, passer d’une DLL à l’autre est simple et permet de débugger, tâche qu’introduit le paragraphe suivant. Si le programme Python s’étale sur plusieurs fichiers, il est plus pratique d’ajouter au début de chacun fichier qui utilise le module C++ : import sys if "PythonSampled" in sys.modules : PythonSample = sys.modules ["PythonSampled"] else : import PythonSample 6.5.7 Debuggage Une manière de débugger consiste à momentanément arrêter l’exécution du programme Python (à l’aide de la fonction raw_input par exemple), le temps d’attacher le débuggeur de Microsoft Visual C++ au processus dont le nom est ou . Il ne reste plus qu’à placer des pointeurs d’arrêt puis à continuer l’exécution du programme Python jusqu’à ce que ces pointeurs d’arrêt soient atteints. Une seconde méthode consiste à volontairement insérer dans le code C++ du module l’instruction __debugbreak(). Lorsque le module est exécuté via un programme Python et qu’il arrive sur cette ligne, il vous demande l’autorisation de continuer l’exécution via un débuggeur. Il faudra penser à enlever la ligne une fois le module corrigé. Il reste toutefois une dernière façon plus classique de débugger qui est l’ajout dans le code d’instructions écrivant des messages dans un fichier texte. On écrit des logs ou des traces. De cette façon, lorsque le programme provoque une erreur, on connaît la séquence de messages générés par le programme. S’ils sont suffisamment explicites, cela permet de corriger l’erreur. Sinon, l’ajout de messages supplémentaires puis une nouvelle exécution du programme permettront d’être plus précis quant à la cause de l’erreur. Outre ces trois méthodes de débuggage, il est possible qu’un module doivent retourner un code d’erreur ou générer une exception qui soit attrapée par l’interpréteur Python puis afficher. C’est l’objet des lignes qui suivent : if (<condition>) throw std::runtime_error ("message d’erreur") ; Toute exception dérivant de runtime_error sera interceptée par l’interpréteur Python. La librairie Boost Python génère également des exceptions notamment lorsque les paramètres envoyés à une fonction du module ne correspondent pas avec sa si- gnature. 6.5.8 Pour aller plus loin La librairie Boost Python permet de faire beaucoup plus de choses que ce qui est présenté dans ces paragraphes. Ceux-ci contiennent néanmoins l’essentiel pour exporter vers Python les objets les plus simples. Il est également possible de : – surcharger une méthode C++ dans un programme Python, – exécuter un programme écrit en Python depuis un programme C++. Il faut parfois une certaine dose de volonté et de ténacité pour parvenir à maîtriser un outil tel que Boost Python, surtout lorsqu’une erreur se déclare dans le code de librairie après une modification en dehors de cette même librairie. calendar | Gérer les calendriers, les dates (voir chapitre 7). | cgi | Utilisé dans les scripts CGI (programmation Internet) | cmath | Fonctions mathématiques complexes. | codecs | Jeux de caractères (voir paragraphe 7.8) | copy | Copies d’instances de classes. | csv | Gestion des fichiers au format CSV (utilisés par Microsoft Excel). | datetime | Calculs sur les dates et heures (voir chapitre 7). | gc | Gestion du garbage collector. | getopt | Lire les options des paramètres passés en arguments d’un programme Python. | glob | Chercher des fichiers (voir chapitre 7). | hashlib | Fonctions de cryptage. | htmllib | Lire le format HTML. | math | Fonctions mathématiques standard telles que cos, sin, exp, log | os | Fonctions systèmes dont certaines fonctions permettant de gérer les fichiers (voir chapitre 7). | os.path | Manipulations de noms de fichiers (voir chapitre 7). | pickle | Sérialisation d’objets, la sérialisation consiste à convertir des données structurées de façon complexe en une structure linéaire facilement enregistrable dans un fichier (voir chapitre 7). | profile | Etudier le temps passé dans les fonctions d’un programme. | random | re | Expressions régulières (voir paragraphe 7.6). | shutil | Copie de fichiers (voir chapitre 7). | sqlite3 | Accès aux fonctionnalités du gestionnaire de base de données SQLite3. | string | Manipulations des chaînes de caractères. | sys | Fonctions systèmes, fonctions liées au langage Python (voir chapitre 7). | threading | Utilisation de threads (voir chapitre 9). | time | Accès à l’heure, l’heure système, l’heure d’une fichier. | Tkinter | Interface graphique (voir chapitre 8). | unittest | Tests unitaires (ou comment améliorer la fiabilité d’un programme). | urllib | Pour lire le contenu de page HTML sans utiliser un navigateur. | urllib2 | Module plus complet que urllib. | xml.dom | Lecture du format XML. | xml.sax | Lecture du format XML. | zipfile | Lecture de fichiers ZIP (voir chapitre 7). | Table 6.1 : Liste de modules souvent utilisés. La liste exhaustive est disponible à l’adresseLes moteurs de recherche sur Internet retournent des résultats assez pertinents sur des requêtes du type python + le nom du module. Les résultats sont principalement en langue anglaise. Fichier libSample/mafonction.h Fichier libSample/mafonction.cpp #ifndef LIB_MAFONCTION_H #define LIB_MAFONCTION_H #include <vector> void somme_vecteur ( const std::vector<double> &v1, const std::vector<double> &v2, std::vector<double> &res) ; #endif | | #include "mafonction.h" void somme_vecteur ( const std::vector<double> &v1, const std::vector<double> &v2, std::vector<double> &res) { if ( () != ()) throw std::runtime_error ( "dimensions différentes") ; res.resize ( ()) ; std::vector<double>:: const_iterator it1,it2 ; *it3 = *it1 + *it2 ; } | | Fichier PythonSample/mafonction.h Fichier PythonSample/mafonction.cpp #ifndef PYTHON_MAFONCTION_H #define PYTHON_MAFONCTION_H #include "python/definition.h" boost::python::list python_somme_vecteur ( boost::python::list v1, boost::python::list v2) ; #endif | | #include "python/definition.h" #include "mafonction.h" #include "../libsample/mafonction.h" #include "python/conversion.h" #include "" boost::python::list python_somme_vecteur ( boost::python::list v1, boost::python::list v2) { std::vector<double> cv1,cv2,cres ; PythonConvert (v1, cv1) ; PythonConvert (v2, cv2) ; somme_vecteur (cv1, cv2, cres) ; boost::python::list res ; PythonConvert (cres, res) ; return res ; } | | Fichier PythonSample/python/main.cpp Fichier PythonSample/test/test.py // #include "../mafonction.h" BOOST_PYTHON_MODULE(PythonSample) { // def ("somme_vecteur", &python_somme_vecteur, (boost::python::arg ("v1"), boost::python::arg ("v2")), "retourne la somme de deux vecteurs"); // } ; | | import PythonSample as PS v1 = [1.0, 2.0] v2 = [6.5, 7.8] v3 = PS.somme_vecteur (v1, v2) | | Figure 6.4 : Six fichiers modifiés pour ajouter une fonction dans un module Python écrit en C++ à partir de l’exemple accessible depuis l’adresse
Chapitre 7 Fichiers, expressions régulières, dates Ce chapitre abordera pour commencer les formats texte, binaire et compressé (zip) directement manipulable depuis Python. Les manipulations de fichiers suivront pour terminer sur les expressions régulières qui sont très utilisées pour effectuer des recherches textuelles. A l’issue de ce chapitre, on peut envisager la recherche à l’intérieur de tous les documents textes présents sur l’ordinateur, de dates particulières, de tous les numéros de téléphones commençant par 06 En utilisant des modules tels que reportlab ou encore win32com , il serait possible d’étendre cette fonctionnalité aux fichiers de type pdf et aux fichiers Microsoft Word, Excel. 7.1 Format texte Les fichiers texte sont les plus simples : ce sont des suites de caractères. Le format HTML et XML font partie de cette catégorie. Ils servent autant à conserver des informations qu’à en échanger comme par exemple transmettre une matrice à Microsoft Excel. Ce format, même s’il est simple, implique une certaine organisation dans la façon de conserver les données afin de pouvoir les récupérer. Le cas le plus fréquent est l’enregistrement d’une matrice : on choisira d’écrire les nombres les uns à la suite des autres en choisissant un séparateur de colonnes et un séparateur de lignes. Ce point sera abordé à la fin de cette section. 7.1.1 Ecriture La première étape est l’écriture. Les informations sont toujours écrites sous forme de chaînes de caractères et toujours ajoutées à la fin du fichier qui s’allonge jusqu’à ce que toutes les informations y soient écrites. L’écriture s’effectue toujours selon le même schéma. 1. création ou ouverture du fichier, 2. écriture, 3. fermeture. f = open ("nom-fichier", "w") # ouverture f.write ( s ) # écriture de la chaîne de caractères s f.write ( s2 ) # écriture de la chaîne de caractères s2 f.close () # fermeture Les étapes d’ouverture et de fermeture sont toujours présentes en ce qui concerne les fichiers. Il s’agit d’indiquer au système d’exploitation que le programme souhaite accéder à un fichier et interdire à tout autre programme l’accès à ce fichier. Un autre programme qui souhaiterait créer un fichier du même nom ne le pourrait pas tant que l’étape de fermeture n’est pas exécutée. En revanche, il pourrait tout à fait le lire car la lecture ne perturbe pas l’écriture. Remarque 7.1 : fichiers ouverts et non fermés Lorsque que le programme se termine, même s’il reste des fichiers "ouverts" pour lesquels la méthode close n’a pas été appelée, ils seront automatiquement fermés. Certains caractères sont fort utiles lors de l’écriture de fichiers texte afin d’organiser les données. Le symbole ; est très utilisé comme séparateur de colonnes pour une matrice, on utilise également le passage à la ligne ou la tabulation. Comme ce ne sont pas des caractères "visibles", ils ont des codes : \n passage à la ligne \t insertion d’une tabulation, indique un passage à la colonne suivante dans le logiciel Microsoft Excel Il existe peu de manières différentes de conserver une matrice dans un fichier, le programme ressemble dans presque tous les cas à celui qui suit : mat = # matrice de type liste de listes f = open ("", "w") for i in range (0,len (mat)) : | # la fonction join est aussi | for j in range (0, len (mat [i])) : | # fréquemment utilisée | f.write ( str (mat [i][j]) + "\t") | # pour assembler les chaînes | f.write ("\n") | f.close () | # nombre d’appels à f.write | Remarque 7.2 : fonction open La fonction open accepte deux paramètres, le premier est le nom du fichier, le second définit le mode d’ouverture : ”w” pour écrire (write), "a" pour écrire et ajouter (append), "r" pour lire (read). Ceci signifie que la fonction open sert à ouvrir un fichier quelque soit l’utilisation qu’on en fait. Remarque 7.3 : autre syntaxe avec print Il existe une autre syntaxe qui permet de remplacer la méthode write. Elle utilise la même syntaxe que celle de l’instruction print et fonctionne de la même manière à ceci près qu’elle ajoute >> f, entre le mot-clé print et le texte à écrire dans le fichier f. Néanmoins, ce n’est pas cette syntaxe qui sera mise en avant dans les versions futures de Python. mat = # matrice de type liste de listes f = open ("", "w") for i in range (0,len (mat)) : for j in range (0, len (mat [i])) : print >> f, str (mat [i][j]), "\t", # ligne changée print >> f # ligne changée f.close () | Remarque 7.4 : écriture différée A la première écriture dans un fichier (premier appel à la fonction write), la taille du fichier créée est souvent nulle. L’écriture dans un fichier n’est pas immédiate, le langage Python attend d’avoir reçu beaucoup d’informations avant de les écrire physiquement sur le disque dur. Les informations sont placées dans un tampon ou buffer. Lorsque le tampon est plein, il est écrit sur disque dur. Pour éviter ce délai, il faut soit fermer puis réouvrir le fichier soit appeler la méthode flush qui ne prend aucun paramètre. Ce mécanisme vise à réduire le nombre d’accès au disque dur, il n’est pas beaucoup plus long d’y écrire un caractère plutôt que 1000 en une fois. 7.1.2 Ecriture en mode "ajout" f = open ("nom-fichier", "a") | # ouverture en mode ajout, mode "a" | Pour comprendre la différence entre ces deux modes d’ouverture, voici deux programmes. Celui de gauche n’utilise pas le mode ajout tandis que celui de droite l’utilise lors de la seconde ouverture. f = open ("", "w") f.write (" premiere fois ") f.close () f = open ("", "w") f.write (" seconde fois ") f.close () # : seconde fois | | f = open ("", "w") f.write (" premiere fois ") f.close () f = open ("", "a") ### f.write (" seconde fois ") f.close () # : premiere fois seconde fois | Le premier programme crée un fichier ”essai.txt” qui ne contient que les informations écrites lors de la seconde phase d’écriture, soit secondefois. Le second utilise le mode ajout lors de la seconde ouverture. Le fichier ”essai.txt”, même s’il existait avant l’exécution de ce programme, est effacé puis rempli avec l’information premierefois. Lors de la seconde ouverture, en mode ajout, une seconde chaîne de caractères est ajoutée. le fichier ”essai.txt”, après l’exécution du programme contient donc le message : premierefoissecondefois. Remarque 7.5 : fichier de traces Un des moyens pour comprendre ou suivre l’évolution d’un programme est d’écrire des informations dans un fichier ouvert en mode ajout qui est ouvert et fermé sans cesse. Ce sont des fichiers de traces ou de log. Ils sont souvent utilisés pour vérifier des calculs complexes. Ils permettent par exemple de comparer deux versions différentes d’un programme pour trouver à quel endroit ils commencent à diverger. 7.1.3 Lecture La lecture d’un fichier permet de retrouver les informations stockées grâce à une étape préalable d’écriture. Elle se déroule selon le même principe, à savoir : 1. ouverture du fichier en mode lecture, 2. lecture, 3. fermeture. f = open ("", "r") # ouverture du fichier en mode lecture for ligne in f : # pour toutes les lignes du fichier print ligne # on affiche la ligne (*) f.close () # on ferme le fichier Pour des fichiers qui ne sont pas trop gros (< 100000 lignes), il est possible d’utiliser la méthode readlines qui récupère toutes les lignes d’un fichier texte en une seule fois. Le programme suivant donne le même résultat que le précédent. f = open ("", "r") # ouverture du fichier en mode lecture l = f.readlines () # lecture de toutes les lignes, placées dans une liste f.close () # fermeture du fichier for s in l : print s # on affiche les lignes à l’écran (*) Remarque 7.6 : code de fin de ligne Lorsque le programme précédent lit une ligne dans un fichier, le résultat lu inclut le ou les caractères (\n \r) qui marquent la fin d’une ligne. C’est pour cela que la lecture est parfois suivie d’une étape de nettoyage. f = open ("", "r") # ouverture du fichier en mode lecture l = f.readlines () # lecture de toutes les lignes, placées dans une liste f.close () # fermeture du fichier # contiendra la liste des lignes nettoyées l_net = [ s.strip ("\n\r") for s in l ] décryptent. Un fichier texte est le moyen le plus simple d’échanger des matrices avec un tableur et il n’est pas besoin de modules dans ce cas. Lorsqu’on enregistre une feuille de calcul sous format texte, le fichier obtenu est organisé en colonnes : sur une même ligne, les informations sont disposées en colonne délimitées par un séparateur qui est souvent une tabulation (\t) ou un point virgule comme dans l’exemple suivant : nom ; prénom ; livre Hugo ; Victor ; Les misérables Kessel ; Joseph ; Le lion Woolf ; Virginia ; Mrs Dalloway Calvino ; Italo ; Le baron perché Pour lire ce fichier, il est nécessaire de scinder chaque ligne en une liste de chaînes de caractères, on utilise pour cela la méthode split des chaînes de caractères. mat = [] # création d’une liste vide, f = open ("", "r") # ouverture du fichier en mode lecture for li in f : # pour toutes les lignes du fichier s = li.strip ("\n\r") # on enlève les caractères de fin de ligne l = s.split (";") # on découpe en colonnes mat.append (l) # on ajoute la ligne à la matrice f.close () # fermeture du fichier | Ce format de fichier texte est appelé CSV , il peut être relu depuis un programme Python comme le montre l’exemple précédent ou être chargé depuis Microsoft Excel en précisant que le format du fichier est le format CSV. Pour les valeurs numériques, il ne faut pas oublier de convertir en caractères lors de l’écriture et de convertir en nombres lors de la lecture. Remarque 7.7 : nombres français et anglais 7.2 Fichiers zip Les fichiers zip sont très répandus de nos jours et constituent un standard de compression facile d’accès quelque soit l’ordinateur et son système d’exploitation. Le langage Python propose quelques fonctions pour compresser et décompresser ces fichiers par l’intermédiaire du module zipfile. Le format de compression zip est un des plus répandus bien qu’il ne soit pas le plus performant . Ce format n’est pas seulement utilisé pour compresser mais aussi comme un moyen de regrouper plusieurs fichiers en un seul. 7.2.1 Lecture L’exemple suivant permet par exemple d’obtenir la liste des fichiers inclus dans un fichier zip : import zipfile file = zipfile.ZipFile ("", "r") for info in file.infolist () : print info.filename, info.date_time, info.file_size file.close () | Les fichiers compressés ne sont pas forcément des fichiers textes mais de tout format. Le programme suivant extrait un fichier parmi ceux qui ont été compressés puis affiche son contenu (on suppose que le fichier lu est au format texte donc lisible). import zipfile file = zipfile.ZipFile ("", "r") data = ("") file.close () print data On retrouve dans ce cas les étapes d’ouverture et de fermeture même si la première est implicitement inclus dans le constructeur de la classe ZipFile. 7.2.2 Ecriture Pour créer un fichier zip, le procédé ressemble à la création de n’importe quel fichier. La seule différence provient du fait qu’il est possible de stocker le fichier à compresser sous un autre nom à l’intérieur du fichier zip, ce qui explique les deux premiers arguments de la méthode write. Le troisième paramètre indique si le fichier doit être compressé (zipfile.ZIP_DEFLATED) ou non (zipfile.ZIP_STORED). import zipfile file = zipfile.ZipFile ("", "w") file.write ("", "", zipfile.ZIP_DEFLATED) file.close () Internet. 7.3 Manipulation de fichiers Il arrive fréquemment de copier, recopier, déplacer, effacer des fichiers. Lorsqu’il s’agit de quelques fichiers, le faire manuellement ne pose pas de problème. Lorsqu’il s’agit de traiter plusieurs centaines de fichiers, il est préférable d’écrire un programme qui s’occupe de le faire automatiquement. Cela peut être la création automatique d’un fichier zip incluant tous les fichiers modifiés durant la journée ou la réorganisation de fichiers musicaux au format mp3 à l’aide de modules complémentaires tel que mutagen ou pymedia . Remarque 7.8 : majuscules et minuscules Pour ceux qui ne sont pas familiers des systèmes d’exploitation, il faut noter que Windows ne fait pas de différences entre les majuscules et les minuscules à l’intérieur d’un nom de fichier. Les systèmes Linux et Mac OS X font cette différence . 7.3.1 Gestion des noms de chemins Le module os.path propose plusieurs fonctions très utiles qui permettent entre autres de tester l’existence d’un fichier, d’un répertoire, de récupérer diverses informations comme sa date de création, sa taille Cette liste présentée par la table 7.1 est loin d’être exhaustive mais elle donne une idée de ce qu’il est possible de faire. 7.3.2 Copie, suppression Le module shutil permet de copier des fichiers, le module os permet de les supprimer ou de les renommer, il permet également de créer ou de supprimer des répertoires. Ces opérations sont regroupées dans la table 7.2. 7.3.3 Liste de fichiers La fonction listdir du module os permet de retourner les listes des éléments inclus dans un répertoire (fichiers et sous-répertoires). Toutefois, le module glob propose abspath(path) | Retourne le chemin absolu d’un fichier ou d’un répertoire. | commonprefix(list) | dirname(path) | Retourne le nom du répertoire. | exists(path) | Dit si un chemin est valide ou non. | getatime(path) getmtime(path) getctime(path) | Retourne diverses dates concernant un chemin, date du dernier accès (getatime), date de la dernière modification (getmtime), date de création (getctime). | getsize(file) | Retourne la taille d’un fichier. | isabs(path) | Retourne True si le chemin est un chemin absolu. | isfile(path) | Retourne True si le chemin fait référence à un fichier. | isdir(path) | Retourne True si le chemin fait référence à un répertoire. | join(p1,p2, ) | Construit un nom de chemin étant donné une liste de répertoires. | split(path) | Découpe un chemin, isole le nom du fichier ou le dernier répertoire des autres répertoires. | splitext(path) | Découpe un chemin en nom + extension. | Table 7.1 : Liste non exhaustive des fonctionnalités offertes par le module os.path. La pageréférence toutes les fonctions de ce module. copy(f1,f2) | (module shutil) Copie le fichier f1 vers f2 | chdir(p) | (module os) Change le répertoire courant, cette fonction peut être importante lorsqu’on utilise la fonction system du module os pour lancer une instruction en ligne de commande ou lorsqu’on écrit un fichier sans préciser le nom du répertoire, le fichier sera écrit dans ce répertoire courant qui est par défaut le répertoire où est situé le programme Python. C’est à partir du répertoire courant que sont définis les chemins relatifs. | getcwd() | (module os) Retourne le répertoire courant, voir la fonction chdir. | mkdir(p) | (module os) Crée le répertoire p. | makedirs(p) | (module os) Crée le répertoire p et tous les répertoires des niveaux supérieurs s’ils n’existent pas. Dans le cas du répertoire d : /base/repfinal, crée d’abord d : /base s’il n’existe pas, puis d : /base/repfinal. | remove(f) | rename(f1,f2) | (module os) Renomme un fichier | rmdir(p) | (module os) Supprime un répertoire | Table 7.2 : Liste non exhaustive des fonctionnalités offertes par les modules shutil et os. Les pageetréférencent toutes les fonctions de ces modules. une fonction plus intéressante qui permet de retourner la liste des éléments d’un répertoire en appliquant un filtre. Le programme suivant permet par exemple de retourner la liste des fichiers et des répertoires inclus dans un répertoire. # coding: latin-1 import glob import def liste_fichier_repertoire (folder, filter) : # résultats file,fold = [], [] # recherche des fichiers obéissant au filtre res = (folder + "\\" + filter) # on inclut les sous-répertoires qui n’auraient pas été # sélectionnés par le filtre rep = (folder + "\\*") for r in rep : if r not in res and .isdir (r) : res.append (r) # on ajoute fichiers et répertoires aux résultats for r in res : path = r if .isfile (path) : # un fichier, rien à faire à part l’ajouter file.append (path) else : # sous-répertoire : on appelle à nouveau la fonction # pour retourner la liste des fichiers inclus fold.append (path) fi,fo = liste_fichier_repertoire (path, filter) file.extend (fi) # on étend la liste des fichiers fold.extend (fo) # on étend la liste des répertoires # fin return file,fold folder = r"D:\Dupre\_data\informatique" filter = "*.tex" file,fold = liste_fichier_repertoire (folder, filter) for f in file : print "fichier ", f for f in fold : print "répertoire ", f # coding: latin-1 import os def liste_fichier_repertoire (folder) : file, rep = [], [] for r, d, f in (folder) : for a in d : rep.append (r + "/" + a) for a in f : file.append (r + "/" + a) return file, rep folder = r"D:\Dupre\_data\informatique" file,fold = liste_fichier_repertoire (folder) for f in file : print "fichier ", f for f in fold : print "répertoire ", f | 7.4 Format binaire Ecrire et lire des informations au travers d’un fichier texte revient à convertir les informations quel que soit leur type dans un format lisible pour tout utilisateur. Un entier est écrit sous forme de caractères décimaux alors que sa représentation en mémoire est binaire. Cette conversion dans un sens puis dans l’autre est parfois jugée coûteuse en temps de traitement et souvent plus gourmande en terme de taille de fichiers même si elle permet de relire les informations écrites grâce à n’importe quel éditeur de texte. Il est parfois plus judicieux pour une grande masse d’informations d’utiliser directement le format binaire, c’est-à-dire celui dans lequel elles sont stockées en mémoire. Les informations apparaissent dans leur forme la plus simple pour l’ordinateur : une suite d’octets (bytes en anglais). Deux étapes vont intervenir que ce soit pour l’écriture : 1. On récupère les informations dans une suite d’octets (fonction pack du module struct). 2. On les écrit dans un fichier (méthode write affiliée aux fichiers). Ou la lecture : 1. On lit une suite d’octets depuis un fichier (méthode read affiliée aux fichiers). 2. On transforme cette suite d’octets pour retrouver l’information qu’elle formait initialement (fonction unpack du module struct). L’écriture et la lecture d’un fichier binaire soulèvent les mêmes problèmes que pour un fichier texte : il faut organiser les données avant de les enregistrer pour savoir comment les retrouver. Les types immuables (réel, entier, caractère) sont assez simples à gérer dans ce format. Pour les objets complexes, Python propose une solution grâce au module pickle. 7.4.1 Ecriture dans un fichier binaire L’écriture d’un fichier binaire commence par l’ouverture du fichier en mode écriture par l’instruction file = open ("<nom_fichier>", "wb"). C’est le code ”wb” qui est important (w pour write, b pour binary), il spécifie le mode d’ouverture ”w” et le format ”b”. La fermeture est la même que pour un fichier texte. Le module struct et la fonction pack permet de convertir les informations sous forme de chaîne de caractères avant de les enregistrer au format binaire. La fonction pack construit une chaîne de caractères égale au contenu de la mémoire. Son affichage avec la fonction print produit quelque chose d’illisible le plus souvent. Le tableau suivant montre les principaux formats de conversion . code | type correspondant | c | caractère | B | caractère non signé (octet) | i | entier (4 octets) | I | entier non signé (4 octets) | d | double (8 octets) | L’utilisation de ces codes est illustrée au paragraphe 7.4.3. 7.4.2 Lecture d’un fichier binaire Le code associé à l’ouverture d’un fichier binaire en mode lecture est ”rb”, cela donne : file = open ("<nom_fichier>", "rb"). La lecture utilise la fonction unpack pour effectuer la conversion inverse, celle d’une chaîne de caractères en entiers, réels, Le paragraphe suivant illustre la lecture et l’écriture au format binaire. 7.4.3 Exemple complet # coding: latin-1 import struct # on enregistre un entier, un réel et 4 caractères i = 10 x = 3.1415692 s = "ABCD" # écriture file = open ("", "wb") | file.write ( ("i" , i) ) file.write ( ("d" , x) ) file.write ( ("cccc" , *s) ) file.close () # lecture file = open ("", "rb") i = struct.unpack ("i", (4)) x = struct.unpack ("d", (8)) s = struct.unpack ("cccc", (4)) file.close () # affichage pour vérifier que les données ont été bien lues print i # affiche (10,) print x # affiche (3.1415692000000002,) print s # affiche (’A’, ’B’, ’C’, ’D’) | Les résultats de la méthode unpack apparaissent dans un tuple mais les données sont correctement récupérées. Ce programme fait aussi apparaître une des particularité du format binaire. On suppose ici que la chaîne de caractères est toujours de longueur 4. En fait, pour stocker une information de dimension variable, il faut d’abord enregistrer cette dimension puis s’en servir lors de la relecture pour connaître le nombre d’octets à lire. On modifie le programme précédent pour sauvegarder une chaîne de caractères de longueur variable. # coding: latin-1 import struct # on enregistre un entier, un réel et n caractères i = 10 x = 3.1415692 s = "ABCDEDF" # écriture file = open ("", "wb") file.write ( ("i" , i) ) file.write ( ("d" , x) ) file.write ( ("i" , len(s)) ) # on sauve la dimension de s file.write ( ("c" * len(s) , *s) ) file.close () # lecture file = open ("", "rb") i = struct.unpack ("i", (4)) x = struct.unpack ("d", (8)) l = struct.unpack ("i", (4)) # on récupère la dimension de s l = l [0] # l est un tuple, on s’intéresse à son unique élément s = struct.unpack ("c" * l, (l)) file.close () | Cette méthode utilisée pour les chaînes de caractères est applicable aux listes et aux dictionnaires de longueur variable : il faut d’abord stocker leur dimension. Il faut retenir également que la taille d’un réel est de huit octets, celle d’un entier de quatre octets et celle d’un caractère d’un octet . Cette taille doit être passée en argument à la méthode read. 7.4.4 Objets plus complexes Il existe un moyen de sauvegarder dans un fichier des objets plus complexes à l’aide du module pickle. Celui-ci permet de stocker dans un fichier le contenu d’un dictionnaire à partir du moment où celui-ci contient des objets standard du langage Python. Le principe pour l’écriture est le suivant : import pickle dico = {’a’: [1, 2.0, 3, "e"], ’b’: (’string’, 2), ’c’: None} lis = [1, 2, 3] f = open (’’, ’wb’) (dico, f) (lis, f) f.close() | La lecture est aussi simple : f = open (’’, ’rb’) dico = (f) lis = (f) f.close() Un des avantages du module pickle est de pouvoir gérer les références circulaires : il est capable d’enregistrer et de relire une liste qui se contient elle-même, ce peut être également une liste qui en contient une autre qui contient la première Le module pickle peut aussi gérer les classes définies par un programmeur à condition qu’elles puissent convertir leur contenu en un dictionnaire dans un sens et dans l’autre, ce qui correspond à la plupart des cas. import pickle import copy class Test : def __init__ (self) : self.chaine = "a" self.entier = 5 self.tuple = { "h":1, 5:"j" } t = Test () f = open(’’, ’wb’) # lecture (t, f) | f.close() f = open(’’, ’rb’) # écriture t = (f) f.close() import pickle import copy class Test : def __init__ (self) : self.x = 5 self.y = 3 self.calcule_norme () # attribut calculé def calcule_norme (self) : self.n = (self.x ** 2 + self.y ** 2) ** 0.5 def __getstate__ (self) : """conversion de Test en un dictionnaire""" d = (self.__dict__) del d ["n"] # attribut calculé, on le sauve pas return d def __setstate__ (self,dic) : """conversion d’un dictionnaire dic en Test""" self.__dict__.update (dic) self.calcule_norme () # attribut calculé t = Test () f = open(’’, ’wb’) # lecture (t, f) f.close() f = open(’’, ’rb’) # écriture t = (f) f.close() | 7.5 Paramètres en ligne de commande Certains développements nécessitent de pouvoir exécuter un programme Python en ligne de commande sans intervention extérieure. Ce peut être un programme de synchronisation de deux répertoires ou la récupération du contenu de pages HTML, des tâches souvent exécutées la nuit à une heure où personne n’est présent pour le faire. Prenons par exemple le cas d’un programme très simple de synchronisation qui recopie tous les fichiers d’un répertoire vers un autre. La fonctionne copie_repertoire effectue cette tâche. # coding: latin-1 import glob import shutil def copie_repertoire (rep1, rep2) : """copie tous les fichiers d’un répertoire rep1 vers un autre rep2""" li = (rep1 + "/*.*") # récupère dans une liste fichiers et # répertoires qui respectent le filtre for l in li : to = l.replace (rep1, rep2) # nom du fichier copié # (on remplace rep1 par rep2) (l, to) copie_repertoire ("c:/original", "c:/backup") c:\python26\ c:/original c:/backup Figure 7.1 : Un programme Python lancé en ligne de commande. Pour lancer un programme Python en ligne de commande, il faut écrire dans une fenêtre de commande ou un programme d’extension bat les instructions suivantes séparées par des espaces : interpréteur_python programme_python argument1 argument2 Il faut maintenant récupérer ces paramètres au sein du programme Python grâce au module sys qui contient une variable argv contenant la liste des paramètres de la ligne de commande. S’il y a deux paramètres, cette liste argv contiendra trois éléments : 1. nom du programme 2. paramètre 1 3. paramètre 2 Le programme suivante utilise ce mécanisme avec le programme synchro contenant la fonction copie_repertoire : # coding: latin-1 import glob import shutil def copie_repertoire (rep1, rep2) : """copie tous les fichiers d’un répertoire rep1 vers un autre rep2""" li = (rep1 + "/*.*") for l in li : to = l.replace (rep1, rep2) # nom du fichier copié # (on remplace rep1 par rep2) (l, to) | import sys # [0] --> nom du programme (ici, ) rep1 = [1] # récupération du premier paramètre rep2 = [2] # récupération du second paramètre copie_repertoire (rep1, rep2) | Remarque 7.9 : paramètres contenant des espaces Lorsque les paramètres écrits sur la ligne de commande contiennent eux-mêmes des espaces, il faut les entourer avec des guillemets. c:\python26\ c:\batch\ "c:\Program Files\source" "c:\Program Files\backup" | Pour des besoins plus complexes, le module getopt propose des fonctions plus élaborées pour traiter les paramètres de la ligne de commande. Il est également possible de lancer un programme en ligne de commande depuis Python grâce à la fonction system du module os comme ceci : import os os.system ("c:\python26\ c:\batch\ " \ "\"c:\Program Files\source\" \"c:\Program Files\backup\"") mat = ["Victor Hugo 6".split (), "Marcel Proust 3".split () ] f = open ("", "w") f.write ("<body><html>\n") f.write ("<table border=\"1\">\n") for m in mat : f.write ("<tr>") for c in m : f.write ("<td>" + c + "</td>") f.write ("</tr>\n") f.write ("</table>") f.close () import os os.system ("\"C:\\Program Files\\Mozilla Firefox\\\" ") os.system ("\"C:\\Program Files\\Internet Explorer\\\"" \ " d:\\temp\\") | 7.6 Expressions régulières 7.6.1 Premiers pas Chercher un mot dans un texte est une tâche facile, c’est l’objectif de la méthode find attachée aux chaînes de caractères, elle suffit encore lorsqu’on cherche un mot au pluriel ou au singulier mais il faut l’appeler au moins deux fois pour chercher ces deux formes. Pour des expressions plus compliquées, il est conseillé d’utiliser les expressions régulières. C’est une fonctionnalité qu’on retrouve dans beaucoup de langages. C’est une forme de grammaire qui permet de rechercher des expressions. Lorsqu’on remplit un formulaire, on voit souvent le format "MM/JJ/AAAA" qui précise sous quelle forme on s’attend à ce qu’une date soit écrite. Les expressions régulières permettent de définir également ce format et de chercher dans un texte toutes les chaînes de caractères qui sont conformes à ce format. La liste qui suit contient des dates de naissance. On cherche à obtenir toutes les dates de cet exemple sachant que les jours ou les mois contiennent un ou deux chiffres, les années deux ou quatre. s = """date 0 : 14/9/2000 date 1 : 20/04/1971 date 2 : 14/09/1913 date 3 : 2/3/1978 date 4 : 1/7/1986 date 5 : 7/3/47 date 6 : 15/10/1914 date 7 : 08/03/1941 date 8 : 8/1/1980 date 9 : 30/6/1976""" [0-3]?[0-9]/[0-1]?[0-9]/([0-2][0-9])?[0-9][0-9] Le module re gère les expressions régulières, celui-ci traite différemment les parties de l’expression régulière qui sont entre parenthèses de celles qui ne le sont pas : c’est un moyen de dire au module re que nous nous intéressons à telle partie de l’expression qui est signalée entre parenthèses. Comme la partie qui nous intéresse - une date - concerne l’intégralité de l’expression régulière, il faut insérer celle-ci entre parenthèses. La première étape consiste à construire l’expression régulière, la seconde à rechercher toutes les fois qu’un morceau de la chaîne s définie plus haut correspond à l’expression régulière. import re # première étape : construction expression = re.compile ("([0-3]?[0-9]/[0-1]?[0-9]/([0-2][0-9])?[0-9][0-9])") # seconde étape : recherche res = expression.findall (s) print res | Le résultat de ce programme est le suivant : [(’14/9/2000’, ’20’), (’20/04/1971’, ’19’), (’14/09/1913’, ’19’), (’2/3/1978’, ’19’), (’1/7/1986’, ’19’), (’7/3/47’, ’’), (’15/10/1914’, ’19’), (’08/03/1941’, ’19’), (’8/1/1980’, ’19’), (’30/6/1976’, ’19’)] C’est une liste de couples dont chaque élément correspond aux parties comprises entre parenthèses qu’on appelle des groupes. Lorsque les expressions régulières sont utilisées, on doit d’abord se demander comment définir ce qu’on cherche puis quelles fonctions utiliser pour obtenir les résultats de cette recherche. Les deux paragraphes qui suivent y répondent. 7.6.2 Syntaxe La syntaxe des expressions régulières est décrite sur le site officiel de Python . Comme toute grammaire, celle des expressions régulières est susceptible d’évoluer au fur et à mesure des versions du langage Python. Lors d’une recherche, on s’intéresse aux caractères et souvent aux classes de caractères : on cherche un chiffre, une lettre, un caractère dans un ensemble précis ou un caractère qui n’appartient pas à un ensemble précis. Certains ensembles sont prédéfinis, d’autres doivent être définis à l’aide de crochets. Pour définir un ensemble de caractères, il faut écrire cet ensemble entre crochets : [0123456789] désigne un chiffre. Comme c’est une séquence de caractères consécutifs, on peut résumer cette écriture en [0-9]. Pour inclure les symboles ?, +, il suffit d’écrire : [-0-9+]. Il faut penser à mettre le symbole ? au début pour éviter qu’il ne désigne une séquence. Le caractère ^ inséré au début du groupe signifie que le caractère cherché ne doit pas être un de ceux qui suivent. Le tableau suivant décrit les ensembles prédéfinis et leur équivalent en terme d’ensemble de caractères : . | désigne tout caractère non spécial quel qu’il soit | \d | désigne tout chiffre, est équivalent à [0-9] | \D | désigne tout caractère différent d’un chiffre, est équivalent à [ˆ0-9] | \s | désigne tout espace ou caractère approché, est équivalent à [ \t\n\r\f\v] 16 | \S | désigne tout caractère différent d’un espace, est équivalent à [^\t\n\r\f\v] | \w | désigne tout lettre ou chiffre, est équivalent à [a-zA-Z0-9_] | \W | désigne tout caractère différent d’une lettre ou d’un chiffre, est équivalent à [â-zA-Z0-9_] | ^ | désigne le début d’un mot sauf s’il est placé entre crochets | $ | désigne la fin d’un mot sauf s’il est placé entre crochets | A l’instar des chaînes de caractères, comme le caractère \ est un caractère spécial, il faut le doubler : [\\]. Avec ce système, le mot "taxinomie" qui accepte deux orthographes s’écrira : tax[io]nomie. Le caractère \ est déjà un caractère spécial pour les chaînes de caractères en Python, il faut donc le quadrupler pour l’insérer dans un expression régulière. L’expression suivante filtre toutes les images dont l’extension est png et qui sont enregistrées dans un répertoire image. s = r"D:\Dupre\_data\informatique\support\vba\" print re.compile ( "[\\\\/]image[\\\\/].*[.]png").search(s) # résultat positif print re.compile (r"[\\/]image[\\/].*[.]png").search(s) # même résultat 7.6.2.2 Les multiplicateurs Les multiplicateurs permettent de définir des expressions régulières comme : un mot entre six et huit lettres qu’on écrira [\w]{6,8}. Le tableau suivant donne la liste des multiplicateurs principaux : ? | présence de l’ensemble de caractères qui précède entre 0 fois et l’infini | + | présence de l’ensemble de caractères qui précède entre 1 fois et l’infini | ? | présence de l’ensemble de caractères qui précède entre 0 et 1 fois | {m,n} | présence de l’ensemble de caractères qui précède entre m et n fois, si m = n, cette expression peut être résumée par {n}. | (?!( )) | absence du groupe désigné par les points de suspensions (voir paragraphe 7.6.4). | L’algorithme des expressions régulières essaye toujours de faire correspondre le plus grand morceau à l’expression régulière. Par exemple, dans la chaîne <h1>mot</h1>, <.*> correspond avec <h1>, </h1> ou encore <h1>mot</h1>. Par conséquent, l’expression régulière correspond à trois morceaux. Par défaut, il prendra le plus grand. Pour choisir les plus petits, il faudra écrire les multiplicateurs comme ceci : ??, +?, ??. 7.6.2.3 Groupes Lorsqu’un multiplicateur s’applique sur plus d’un caractère, il faut définir un groupe à l’aide de parenthèses. Par exemple, le mot yoyo s’écrira : (yo){2}. Les parenthèses jouent un rôle similaire à celui qu’elles jouent dans une expression numérique. Tout ce qui est compris entre deux parenthèses est considéré comme un groupe. 7.6.2.4 Assembler les caractères On peut assembler les groupes de caractères les uns à la suite des autres. Dans ce cas, il suffit de les juxtaposer comme pour trouver les mots commençant par s : s[a ? z]?. On peut aussi chercher une chaîne ou une autre grâce au symbole |. Chercher dans un texte l’expression Xavier Dupont ou M. Dupont s’écrira : (Xavier)|(M[.])Dupont. 7.6.3 Fonctions La fonction compile du module re permet de construire un objet "expression régulière". A partir de cet objet, on peut vérifier la correspondance entre une expression régulière et une chaîne de caractères (méthode match). On peut chercher une expression régulière (méthode search). On peut aussi remplacer une expression régulière par une chaîne de caractères (méthode sub). La table 7.3 récapitule ces méthodes. match(s[,pos[,end]]) | Vérifie la correspondance entre l’expression régulière et la chaîne s. Il est possible de n’effectuer cette vérification qu’entre les caractères dont les positions sont pos et end. La fonction retourne None s’il n’y a pas de correspondance et sinon un objet de type Match. | search(s[,pos[,end]]) | Fonction semblable à match, au lieu de vérifier la correspondance entre toute la chaîne et l’expression régulière, cherche la première chaîne de caractères extraite correspondant à l’expression régulière. | split(s [,maxsplit = 0]) | findall(s[,pos[,end]]) | Identique à split mais ne retourne pas les morceaux entre les chaînes extraites qui vérifient l’expression régulière. | sub(repl,s [,count = 0]) | Remplace dans la chaîne repl les éléments \1, \2, par les parties de s qui valident l’expression régulière. | flags | Mémorise les options de construction de l’expression régulière. C’est un attribut. | pattern | Chaîne de caractères associée à l’expression régulière. C’est un attribut. | Table 7.3 : Liste non exhaustive des méthodes et attributs qui s’appliquent à un objet de type "expression régulière" retourné par la fonction compile du module re. La pagecontient la documentation associée au module re. Les méthodes search et match retournent toutes des objets Match dont les méthodes sont présentées par la table 7.4. Appliquées à l’exemple décrit page 185 concernant les dates, cela donne : expression = re.compile ("([0-3]?[0-9]/[0-1]?[0-9]/([0-2][0-9])?[0-9][0-9])[^\d]") print expression.search (s).group(1,2) # affiche (’14/9/2000’, ’20’) c = expression.search (s).span(1) | # affiche (9, 18) | print s [c[0]:c[1]] | # affiche 14/9/2000 | group([g1, ]) | Retourne la chaîne de caractères validée par les groupes g1 | groups([default]) | Retourne la liste des chaînes de caractères qui ont été validées par chacun des groupes. | span([gr]) | Retourne les positions dans la chaîne originale des chaînes extraites validées le groupe gr. | | | | Table 7.4 : Liste non exhaustive des méthodes qui s’appliquent à un objet de type Match qui est le résultat des méthodes search et match. Les groupes sont des sous-parties de l’expression régulière, chacune d’entre elles incluses entre parenthèses. Le nème correspond au groupe qui suit la nème parenthèse ouvrante. Le premier groupe a pour indice 1. La pagecontient la documentation associée au module re. L’exemple suivant présente trois cas d’utilisation des expressions régulières. On s’intéresse aux titres de chansons MP3 stockées dans un répertoire. Le module mutagen permet de récupérer certaines informations concernant un fichier MP3 dont le titre, l’auteur et la durée. Le premier problème consiste à retrouver les chansons sans titre ou dont le titre contient seulement son numéro : track03, track - 03, audiotrack 03, track 03, piste 03, piste - 03, audiopiste 03, Ce titre indésirable doit valider l’expression régulière suivante : ˆ(((audio)?track( )?( - )?[0-9]1,2)|(piste [0-9]1,2))$. Le second problème consiste à retrouver toutes les chansons dont le titre contient le mot heart mais ni heartland ni heartache. Ce titre doit valider l’expression régulière : ((heart)(?!((ache)|(land)))). Le troisième problème consiste à compter le nombre de mots d’un titre. Les mots sont séparés par l’ensemble de caractères [?,;!0.?& :]. On utilise la méthode split pour découper en mots. Le résultat est illustré par le programme suivant. # coding: latin-1 import mutagen.mp3, mutagen.easyid3, os, re def infoMP3 (file, tags) : """retourne des informations sur un fichier MP3 sous forme de dictionnaire (durée, titre, artiste, )""" a = mutagen.mp3.MP3(file) b = mutagen.easyid3.EasyID3(file) info = { "minutes".length/60, "nom":file } for k in tags : try : info [k] = str (b [k][0]) except : continue return info def all_files (repertoire, tags, ext = re.compile (".mp3$")) : | """retourne les informations pour chaque fichier d’un répertoire""" all = [] for r, d, f in (repertoire) : for a in f : if not ext.search (a) : continue t = infoMP3 (r + "/" + a, tags) if len (t) > 0 : all.append (t) return all def heart_notitle_mots (all, avoid,sep,heart) : """retourne trois résultats - les chansons dont le titre valide l’expression régulière heart if "title" not in a : notitle.append (a) continue ti = a ["title"].lower () if avoid.match (ti) : notitle.append (a) continue if heart.search (ti) : liheart.append (a) nbsong += 1 nbmot += len ([ m for m in sep.split (ti) if len (m) > 0 ]) return liheart, notitle, float (nbmot)/nbsong tags = "title album artist genre tracknumber".split () all = all_files (r"D:\musique", tags) avoid = re.compile ("^(((audio)?track( )?( - )?[0-9]{1,2})|(piste [0-9]{1,2}))$") sep = re.compile ("[- ,;!’.?&:]") heart = re.compile ("((heart)(?!((ache)|(land))))") liheart, notitle, moymot = heart_notitle_mots (all, avoid, sep, heart) print "nombre de mots moyen par titre ", moymot print "somme des durée contenant heart ", sum ( [ s ["minutes"] for s in liheart] ) print "chanson sans titre ", len (notitle) print "liste des titres " for s in liheart : print " ",s ["title"] | Remarque 7.11 : Nommer des groupes Une expression régulière ne sert pas seulement de filtre, elle permet également d’extraire le texte qui correspond à chaque groupe, à chaque expression entre parenthèses. L’exemple suivant montre comment récupérer le jour, le mois, l’année à l’intérieur d’une date. import re date = "05/22/2010" exp = "([0-9]{1,2})/([0-9]{1,2})/(((19)|(20))[0-9]{2})" com = re.compile (exp) print com.search (date).groups () # (’05’, ’22’, ’2010’, ’20’, None, ’20’) Il n’est pas toujours évident de connaître le numéro du groupe qui contient l’information à extraire. C’est pourquoi il paraît plus simple de les nommer afin de les récupérer sous la forme d’un dictionnaire et non plus sous forme de tableau. La syntaxe (?P<nom_du_groupe>expression) permet de nommer un groupe. Elle est appliquée à l’exemple précédent. exp = "(?P<jj>[0-9]{1,2})/(?P<mm>[0-9]{1,2})/(?P<aa>((19)|(20))[0-9]{2})" com = re.compile (exp) 7.7 Dates Le module datetime fournit une classe datetime qui permet de faire des opérations et des comparaisons sur les dates et les heures. L’exemple suivant calcule l’âge d’une personne née le 11 août 1975. import datetime naissance = datetime.datetime (1975,11,8,10,0,0) jour = () # obtient l’heure et la date actuelle print jour # affiche 2010-05-22 11:24:36.312000 age = jour - naissance # calcule une différence print age # affiche 12614 days, 1:25:10.712000 L’objet datetime autorise les soustractions et les comparaisons entre deux dates. Une soustraction retourne un objet de type timedelta qui correspond à une durée. qu’on peut multiplier par un réel ou ajouter à un objet de même type ou à un objet de type datetime. L’utilisation de ce type d’objet évite de se pencher sur tous les problèmes de conversion. Le module calendar est assez pratique pour construire des calendriers. Le programme ci-dessous affiche une liste de t-uples incluant le jour et le jour de la semaine du mois d’août 1975. Dans cette liste, on y trouve le t-uple (11,0) qui signifie que le 11 août 1975 était un lundi. Cela permet de récupérer le jour de la semaine d’une date de naissance. import calendar c = calendar.Calendar () for d in c.itermonthdays2 (1975,8) : print d 7.8 Problème de jeux de caractères 1. Le jeu de caractères utilisé par l’éditeur de texte pour afficher le programme. 2. Le jeu de caractères du programme, par défaut ascii mais il peut être changé en insérant une première ligne de commentaire (voir paragraphe 1.5.2, page 22). Les chaînes de caractères du programme sont codées avec ce jeu de caractères. Ce jeu devrait être identique à celui utilisé par l’éditeur de texte afin d’éviter les erreurs. 3. Le jeu de caractères de la sortie, utilisé pour chaque instruction print, il est désigné par le code cp1252 sur un système Windows. 4. Le jeu de caractères dans lequel les chaînes de caractères sont manipulées. Un jeu standard qui permet de représenter toutes les langues est le jeu de caractères utf ? 8. Il peut être différent pour chaque variable. 5. Le jeu de caractères d’un fichier texte. Il peut être différent pour chaque fichier. Le langage Python offre deux classes pour représenter les chaînes de caractères. La classe str qui est adaptée aux langues latines, le jeu de caractères n’est pas précisé. La classe unicode représente un caractère sur un à quatre octets avec un jeu de caractères désigné par l’appellation unicode. Il est impératif de savoir quel jeu est utilisé à quel endroit et il faut faire des conversions de jeux de caractères pour passer l’information d’un endroit à un autre. Il existe deux méthodes pour cela présentées par la table 7.5. # coding: latin-1 st = "eé" su = u"eé" # raccourci pour su = unicode ("eé", "latin-1") # ou encore su = unicode ("eé".decode ("latin-1")) print type (st) # affiche <type ’str’> print type (su) # affiche <type ’unicode’> print len (st), ";", st # affiche 2 ; eé | Lorsqu’on manipule plusieurs jeux de caractères, il est préférable de conserver un unique jeu de référence pour le programme par l’intermédiaire de la classe unicode. Il "suffit" de gérer les conversions depuis ces chaînes de caractères vers les entrées sorties du programme comme les fichiers texte. Par défaut, ceux-ci sont écrits avec le jeu de caractères du système d’exploitation. Dans ce cas, la fonction open suffit. En revanche, si le jeu de caractères est différent, il convient de le préciser lors de l’ouverture du fichier. On utilise la fonction open du module codecs qui prend comme paramètre supplémentaire le jeu de caractères du fichier. Toutes les chaînes de caractères seront lues et converties au format unicode. import codecs f = ("", "r", "cp1252") # jeu Windows s = "".join (f.readlines ()) f.close () print type (s) # affiche <type ’unicode’> print s.encode ("cp1252") # pour l’afficher, # il faut convertir l’unicode en "cp1252" | encode( [enc[,err]]) | Cette fonction permet de passer d’un jeu de caractères, celui de la variable, au jeu de caractères précisé par enc à moins que ce ne soit le jeu de caractères par défaut. Le paramètre err permet de préciser comment gérer les erreurs, doit-on interrompre le programme (valeur 0strict0) ou les ignorer (valeur 0ignore0). La documentation Python recense toutes les valeurs possibles pour ces deux paramètres aux adresses #id3 et # . Cette fonction retourne un résultat de type str. | decode( [enc[,err]]) | Cette fonction est la fonction inverse de la fonction encode. Avec les mêmes paramètres, elle effectue la transformation inverse. Le programme suivant permet d’obtenir le jeu de caractères par défaut et celui du système d’exploitation. import sys import locale # retourne le jeu de caractères par défaut print sys.getdefaultencoding () # affiche ascii # retourne le jeu de caractères du système d’exploitation print locale.getdefaultlocale() # affiche (’fr_FR’, ’cp1252’) | Les problèmes de jeux de caractères peuvent devenir vite compliqués lorsqu’on manipule des informations provenant de plusieurs langues différentes. Il est rare d’avoir à s’en soucier tant que le programme gère les langues anglaise et française. Dans le cas contraire, il est préférable d’utiliser le type unicode pour toutes les chaînes de caractères. Il est conseillé de n’utiliser qu’un seul jeu de caractères pour enregistrer les informations dans des fichiers et le jeu de caractères utf ? 8 est le plus indiqué.
Chapitre 8 Interface graphique Les interfaces graphiques servent à rendre les programmes plus conviviaux. Elles sont pratiques à utiliser mais elles demandent un peu de temps pour les concevoir. Un programme sans interface exécute des instructions les unes à la suite des autres, le programme a un début - un point d’entrée - et une fin. Avec une interface, le programme fonctionne de manière différente. Il n’exécute plus successivement les instructions mais attend un événement - pression d’une touche du clavier, clic de souris - pour exécuter une fonction. C’est comme si le programme avait une multitude de points d’entrée. Les interfaces graphiques évoluent sans doute plus vite que les autres modules, des composantes de plus en plus complexes apparaissent régulièrement. Un module comme wxPython change de version plusieurs fois par an. Il est possible de trouver sur Internet des liens qui donnent des exemples de programme. Une excellente source de documentation sont les forums de discussion qui sont un lieu où des programmeurs échangent questions et réponses. Un message d’erreur entré sur un moteur de recherche Internet permet souvent de tomber sur des échanges de ce type, sur des problèmes résolus par d’autres. 8.1 Introduction Un programme muni d’une interface graphique fonctionne différemment d’un programme classique. Un programme classique est une succession presque linéaire d’instructions. Il y a un début ou point d’entrée du programme et aucun événement extérieur ne vient troubler son déroulement. Avec une interface graphique, le point d’entrée du programme est masqué : il est pris en compte automatiquement. Du point de vue du programmeur, le programme a plusieurs points d’entrée : une simple fenêtre avec deux boutons (voir figure 8.1) propose deux façons de commencer et il faut prévoir une action associée à chaque bouton. La conception d’une interface graphique se déroule généralement selon deux étapes. La première consiste à dessiner l’interface, c’est-à-dire choisir une position pour les objets de la fenêtre (boutons, zone de saisie, liste déroulante, ). La seconde étape définit le fonctionnement de la fenêtre, c’est-à-dire associer à chaque objet des fonctions qui seront exécutées si un tel événement se réalise (pression d’un bouton, pression d’une touche, ). graphiques. Figure 8.1 : Une fenêtre contenant deux boutons : ce sont deux points d’entrée du programme. 8.2 Les objets Les interfaces graphiques sont composées d’objets ou widgets . Ce paragraphe décrit les principales méthodes qui permettent de modifier le contenu et l’apparence des objets. Il faut se reporter à la documentation du langage pour avoir la liste de toutes les options disponibles. Les exemples de codes des paragraphes qui suivent permettent de disposer les objets dans une fenêtre qui ne s’affichera pas sans les quelques lignes de code supplémentaires présentées au paragraphe 8.4.1 et l’utilisation d’une méthode pack (paragraphe 8.3). L’exemple suivant crée un objet : zone_texte = Tkinter.Label (text = "zone de texte") Et pour l’afficher, il faut l’enrober : import Tkinter # import de Tkinter root = () # création de la fenêtre principale # obj = Tkinter.Label (text = "zone de texte") # () # on ajoute l’objet à la fenêtre principale root.mainloop () # on affiche enfin la fenêtre principal et on attend # les événements (souris, clic, clavier) | 8.2.1 Zone de texte Une zone de texte sert à insérer dans une fenêtre graphique une légende indiquant ce qu’il faut insérer dans une zone de saisie voisine comme le montre la figure 8.2. Une zone de texte correspond à la classe Label du module Tkinter. Pour créer une zone de texte, il suffit d’écrire la ligne suivante : zone_texte = Tkinter.Label (text = "zone de texte") Il est possible que le texte de cette zone de texte doive changer après quelques temps. Dans ce cas, il faut appeler la méthode config comme suit : zone_texte = Tkinter.Label (text = "premier texte") # # pour changer de texte zone_texte.config (text = "second texte") La figure 8.2 montre deux zones de texte. La seconde est grisée par rapport à la première. Pour obtenir cet état, il suffit d’utiliser l’instruction suivante : zone_texte.config (state = Tkinter.DISABLED) Et pour revenir à un état normal : zone_texte.config (state = Tkinter.NORMAL) Ces deux dernières options sont communes à tous les objets d’une interface graphique. Cette option sera rappelée au paragraphe 8.2.11. 8.2.2 Bouton Un bouton a pour but de faire le lien entre une fonction et un clic de souris. Un bouton correspond à la classe Button du module Tkinter. Pour créer un bouton, il suffit d’écrire la ligne suivante : bouton = Tkinter.Button (text = "zone de texte") Il est possible que le texte de ce bouton doive changer après quelques temps. Dans ce cas, il faut appeler la méthode config comme suit : bouton = Tkinter.Button (text = "premier texte") # # pour changer de texte bouton.config (text = "second texte") | Figure 8.3 : Exemple de boutons, non pressé, pressé, grisé. Le bouton grisé ne peut être pressé. La figure 8.3 montre trois boutons. Le troisième est grisé par rapport au premier. Les boutons grisés ne peuvent pas être pressés. Pour obtenir cet état, il suffit d’utiliser l’instruction suivante : bouton.config (state = Tkinter.DISABLED) Et pour revenir à un état normal : bouton.config (state = Tkinter.NORMAL) C’est pour cet objet que cette option est la plus intéressante car elle permet d’interdire la possibilité pour l’utilisateur de presser le bouton tout en le laissant visible. Il est possible également d’associer une image à un bouton. Par exemple, les trois lignes suivantes créent un bouton, charge une image au format gif puis l’associe au bouton b. Lors de l’affichage de la fenêtre, le bouton b ne contient pas de texte mais une image. b = Tkinter Button () im = Tkinter.PhotoImage (file = "") b.config (image = im) 8.2.3 Zone de saisie Une zone de saisie a pour but de recevoir une information entrée par l’utilisateur. Une zone de saisie correspond à la classe Entry du module Tkinter; pour en créer une, il suffit d’écrire la ligne suivante : saisie = Tkinter.Entry () Pour modifier le contenu de la zone de saisie, il faut utiliser la méthode insert qui insère un texte à une position donnée. # le premier paramètre est la position # où insérer le texte (second paramètre) saisie.insert (pos, "contenu") Pour obtenir le contenu de la zone de saisie, il faut utiliser la méthode get : Pour supprimer le contenu de la zone de saisie, il faut utiliser la méthode delete. Cette méthode supprime le texte entre deux positions. # supprime le texte entre les positions pos1, pos2 saisie.delete (pos1, pos2) Par exemple, pour supprimer le contenu d’une zone de saisie, on peut utiliser l’instruction suivante : saisie.delete (0, len ( ())) Figure 8.4 : Exemple de zones de saisie, normale et grisée, la zone grisée ne peut être modifiée. La figure 8.4 montre deux zones de saisie. La seconde est grisée par rapport à la première. Les zones de saisie grisées ne peuvent pas être modifiées. Pour obtenir cet état, il suffit d’utiliser la méthode config comme pour les précédents objets. Cette option sera rappelée au paragraphe 8.2.11. 8.2.4 Zone de saisie à plusieurs lignes Une zone de saisie à plusieurs lignes est identique à la précédente à ceci près qu’elle autorise la saisie d’un texte sur plusieurs lignes. Cette zone correspond à la classe Text du module Tkinter. Pour créer une telle zone, il suffit d’écrire la ligne suivante : saisie = () # le premier paramètre est la position # où insérer le texte (second paramètre) pos = "0.0" saisie.insert (pos, "première ligne\nseconde ligne") Pour obtenir le contenu de la zone de saisie, il faut utiliser la méthode get qui retourne le texte entre deux positions. La position de fin n’est pas connue, on utilise la chaîne de caractères "end" pour désigner la fin de la zone de saisie. # retourne le texte entre deux positions pos1 = "0.0" pos2 = "end" # ou contenu = (pos1, pos2) Pour supprimer le contenu de la zone de saisie, il faut utiliser la méthode delete. Cette méthode supprime le texte entre deux positions. # supprime le texte entre les positions pos1, pos2 saisie.delete (pos1, pos2) Par exemple, pour supprimer le contenu d’une zone de saisie à plusieurs lignes, on peut utiliser l’instruction suivante : saisie.delete ("0.0", "end") # on peut aussi utiliser # saisie.delete ("0.0", ) Pour modifier les dimensions de la zone de saisie à plusieurs lignes, on utilise l’instruction suivante : # modifie les dimensions de la zone # width <--> largeur # height <--> hauteur en lignes saisie.config (width = 10, height = 5) | | | L’image précédente montre une zone de saisie à plusieurs lignes. Pour griser cette zone, il suffit d’utiliser la méthode config rappelée au paragraphe 8.2.11. 8.2.5 Case à cocher Une case à cocher correspond à la classe Checkbutton du module Tkinter. Pour créer une case à cocher, il suffit d’écrire la ligne suivante : # crée un objet entier pour récupérer la valeur de la case à cocher, # 0 pour non cochée, 1 pour cochée v = Tkinter.IntVar () case = Tkinter.Checkbutton (variable = v) v.get () # égal à 1 si la case est cochée, 0 sinon Pour cocher et décocher la case, il faut utiliser les instructions suivantes : case.select () | # pour cocher | case.deselect () | # pour décocher | Il est possible d’associer du texte à l’objet case à cocher : case.config (text = "case à cocher") La figure 8.5 montre trois cases. La troisième est grisée par rapport à la première. Les cases grisées ne peuvent pas être cochées. Pour obtenir cet état, il suffit d’utiliser la méthode config rappelée au paragraphe 8.2.11. Figure 8.5 : Exemples de cases à cocher, non cochée, cochée, grisée. Lorsqu’elle est grisée, son état ne peut être modifié. La dernière image montre une case à cocher associée à un texte. 8.2.6 Case ronde ou bouton radio Une case ronde ou bouton radio correspond à la classe Radiobutton du module Tkinter. Elles fonctionnent de manière semblable à des cases à cocher excepté le fait qu’elles n’apparaissent jamais seules : elles fonctionnent en groupe. Pour créer un groupe de trois cases rondes, il suffit d’écrire la ligne suivante : # crée un objet entier partagé pour récupérer le numéro du bouton radio activé v = Tkinter.IntVar () case1 = Tkinter.Radiobutton (variable = v, value = 10) case2 = Tkinter.Radiobutton (variable = v, value = 20) case3 = Tkinter.Radiobutton (variable = v, value = 30) La variable v est partagée par les trois cases rondes. L’option value du constructeur permet d’associer un bouton radio à une valeur de v. Si v == 10, seul le premier bouton radio sera sélectionné. Si v == 20, seul le second bouton radio le sera. Si deux valeurs sont identiques pour deux boutons radio, ils seront cochés et décochés en même temps. Et pour savoir quel bouton radio est coché ou non, il suffit d’exécuter l’instruction : v.get () # retourne le numéro du bouton radio coché (ici, 10, 20 ou 30) v.set (numero) # numéro du bouton radio à cocher # pour cet exemple, 10, 20 ou 30 | Il est possible d’associer du texte à un bouton radio (voir figure 8.6) : case1.config (text = "premier bouton") case2.config (text = "second bouton") case3.config (text = "troisième bouton") Figure 8.6 : Exemples de cases rondes ou boutons radio, non cochée, cochée, grisée. Lorsqu’elle est grisée, son état ne peut être modifiée. La seconde image présente un groupe de bouton radio. Un seul peut être sélectionné à la fois à moins que deux boutons ne soient associés à la même valeur. Dans ce cas, ils agiront de pair. La figure 8.6 montre trois cases. La troisième est grisée par rapport à la première. Si un des boutons radio est grisé parmi les trois, le choix du bouton à cocher s’effectue parmi les deux restants ou aucun si le bouton radio grisé est coché. 8.2.7 Liste Un objet liste contient une liste d’intitulés qu’il est possible de sélectionner. Une liste correspond à la classe ListBox du module Tkinter. Pour la créer, il suffit d’écrire la ligne suivante : Pour modifier les dimensions de la zone de saisie à plusieurs lignes, on utilise l’instruction suivante : # modifie les dimensions de la liste # width <--> largeur # height <--> hauteur en lignes li.config (width = 10, height = 5) On peut insérer un élément dans la liste avec la méthode insert : pos = 0 # un entier, "end" ou pour insérer ce mot à la fin li.insert (pos, "première ligne") On peut supprimer des intitulés de cette liste avec la méthode delete. pos1 = 0 # un entier pos2 = None # un entier, "end" ou pour supprimer tous les éléments # de pos1 jusqu’au dernier li.delete (pos1, pos2 = None) pos1 = 0 li.select_set (pos1, pos2 = None) # sélectionne tous les éléments entre les indices pos1 et # pos2 inclus ou seulement celui d’indice pos1 si pos2 == None | Réciproquement, il est possible d’enlever un intitulé de la sélection à l’aide de la méthode select_clear. pos1 = 0 li.select_clear (pos1, pos2 = None) # retire la sélection de tous les éléments entre les indices # pos1 et pos2 inclus ou seulement celui d’indice pos1 si pos2 == None La méthode curselection permet d’obtenir la liste des indices des éléments sélectionnés. La méthode get permet récupérer un élément de la liste tandis que la méthode size retourne le nombre d’éléments : for i in range (0, ()) : print (i) | Figure 8.7 : Exemple de liste. La seconde liste est grisée et ne peut être modifiée. La figure 8.7 montre deux listes. La seconde est grisée par rapport à la première. Elle ne peut être modifiée. Pour obtenir cet état, il faut appeler la méthode config rappelée au paragraphe 8.2.11. Remarque 8.1 : liste et barre de défilement Il est possible d’adjoindre une barre de défilement verticale. Il faut pour cela inclure l’objet dans une sous-fenêtre Frame qui est définie au paragraphe 8.3.2 comme dans l’exemple suivant : frame = Tkinter.Frame (parent) scrollbar = Tkinter.Scrollbar (frame) li = Tkinter.Listbox (frame, width = 88, height = 6, \ yscrollcommand = ) scrollbar.config (command = li.yview) (side = ) (side = Tkinter.RIGHT, fill = Tkinter.Y) | Il suffit de transposer cet exemple pour ajouter une barre de défilement horizontale. Toutefois, il est préférable d’utiliser un objet prédéfini présent dans le module Tix qui est une extension du module Tkinter. Elle est présentée au paragraphe 8.2.8. Remarque 8.2 : plusieurs Listbox li = Tkinter.Listbox (frame, width = 88, height = 6, exportselection=0) Il existe des méthodes plus avancées qui permettent de modifier l’aspect graphique d’un élément comme la méthode itemconfigure. Son utilisation est peu fréquente à moins de vouloir réaliser une belle interface graphique. Le paragraphe 8.6.2 montre l’utilisation qu’on peut en faire. 8.2.8 Liste avec barre de défilement, Combobox Il n’existe pas dans le module Tkinter d’objets List avec une barre de défilement incluse mais il existe un autre module interne Tix qui étend la liste des objets proposés par Tkinter. Ce module propose notamment une liste avec une barre de défilement intégrée : # coding: latin-1 import Tix as Tk root = () o = Tk.ScrolledListBox (root) for k in range (0,100) : o.listbox.insert (, "ligne " + str (k)) o.pack () def print_file () : # voir chapitre sur les événements print o.listbox.selection_get () # idem b = Tk.Button (root, text = "print") b.config (command = print_file) # idem b.pack () root.mainloop () # idem | Le module Tix quoique assez riche puisqu’il propose des fenêtres permettant de sélectionner un fichier ou de remplir un tableau est mal documenté sur Python. Il existe une documentation officielle et des exemples 6. La plupart du temps, on trouve un exemple approché. Dans l’exemple précédent comme dans le suivant avec une "ComboBox", la pression du bouton print écrit la zone sélectionnée. La figure 8.8 illustre graphiquement ce qu’est une Combobox. # coding: latin-1 import Tix as Tk root = () o = Tk.ComboBox (root, label = "label") o.insert (, "ligne 1") o.insert (, "ligne 2") o.insert (, "ligne 3") o.insert (, "ligne 4") o.pack () b = Tk.Button (root, text = "print") b.config (command = print_file) # idem b.pack () root.mainloop () # idem | L’avantage du module Tix est d’être installé automatiquement avec le langage Python. Il reste malgré tout très peu documenté et arriver à ses fins peut nécessiter quelques heures de recherche. 8.2.9 Canevas Pour dessiner, il faut utiliser un objet canevas, correspondant à la classe Canvas du module Tkinter. Pour la créer, il suffit d’écrire la ligne suivante : Pour modifier les dimensions de la zone de saisie à plusieurs lignes, on utilise l’instruction suivante : # modifie les dimensions du canevas # width <--> largeur en pixels # height <--> hauteur en pixels ca.config (width = 10, height = 5) Cet objet permet de dessiner des lignes, des courbes, d’écrire du texte grâce aux méthodes create_line, create_rectangle, create_text. La figure 8.9 illustre ce que donnent les quelques lignes qui suivent. # dessine deux lignes du point 10,10 au point 40,100 et au point 200,60 # de couleur bleue, d’épaisseur 2 ca.create_line (10,10,40,100, 200,60, fill = "blue", width = 2) # dessine une courbe du point 10,10 au point 200,60 # de couleur rouge, d’épaisseur 2, c’est une courbe de Bézier # pour laquelle le point 40,100 sert d’assise ca.create_line (10,10, 40,100, 200,60, smooth=1, fill = "red", width = 2) # dessine un rectangle plein de couleur jaune, de bord noir et d’épaisseur 2 ca.create_rectangle (300,100,60,120, fill = "gray", width = 2) # écrit du texte de couleur noire au point 80,80 et avec la police arial ca.create_text (80,80, text = "écrire", fill = "black", font = "arial") 8.2.10 Menus Les menus apparaissent en haut des fenêtres. La plupart des applications arborent un menu commençant par Fichier Edition Affichage Le paragraphe 8.4.5 page 214 les décrit en détail. 8.2.11 Méthodes communes Nous avons vu que tous les objets présentés dans ce paragraphe possèdent une méthode config qui permet de définir l’état du widget (grisé ou normal) . widget.config (state = Tkinter.DISABLED) # grisé widget.config (state = Tkinter.NORMAL) # aspect normal Elle permet également de modifier le texte d’un objet, sa position, De nombreuses options sont communes à tous les objets et certaines sont spécifiques. L’aide associée à cette méthode ne fournit aucun renseignement. En fait, le constructeur et cette méthode ont les mêmes paramètres optionnels. Il est équivalent de préciser ces options lors de l’appel au constructeur : l = Tkinter.Label (text = "légende") Ou de les modifier à l’aide de la méthode config : l = Tkinter.Label () l.config (text = "légende") L’aide associée à la méthode config est la suivante : Help on method configure in module Tkinter: configure(self, cnf=None, **kw) unbound Tkinter.Label method Configure resources of a widget. The values for resources are specified as keyword arguments. To get an overview about the allowed keyword arguments call the method keys. | Tandis que l’aide associée au constructeur de la classe Label donne plus d’informations : __init__(self, master=None, cnf={}, **kw) unbound Tkinter.Label method Construct a label widget with the parent MASTER. STANDARD OPTIONS activebackground, activeforeground, anchor, background, bitmap, borderwidth, cursor, disabledforeground, font, foreground, highlightbackground, highlightcolor, highlightthickness, image, justify, padx, pady, relief, takefocus, text, textvariable, underline, wraplength WIDGET-SPECIFIC OPTIONS height, state, width 8.3 Disposition des objets dans une fenêtre 8.3.1 Emplacements Chacun des objets (ou widgets) présentés au paragraphe précédent possède trois méthodes qui permettent de déterminer sa position dans une fenêtre : pack, grid, place. Les deux premières permettent de disposer les objets sans se soucier ni de leur dimension ni de leur position. La fenêtre gère cela automatiquement. La dernière place les objets dans une fenêtre à l’aide de coordonnées sans utiliser l’aide d’aucune grille. Dans une fenêtre, tous les objets doivent être placés avec la même méthode. Dans le cas contraire, les résultats risquent ne pas être ceux attendus. 8.3.1.1 Méthode pack Cette méthode empile les objets les uns à la suite des autres. Par défaut, elle les empile les uns en dessous des autres. Par exemple, l’exemple suivant produit l’empilement des objets de la figure 8.10. l = Tkinter.Label (text = "première ligne") l.pack () s = Tkinter.Entry () s.pack () e = Tkinter.Label (text = "seconde ligne") e.pack () Figure 8.10 : Les objets sont empilés à l’aide de la méthode pack les uns en dessous des autres pour la première image, les uns à droite des autres pour la seconde image. On peut aussi les empiler les uns à droite des autres grâce à l’option side. l = Tkinter.Label (text = "première ligne") l.pack (side = Tkinter.RIGHT) s = Tkinter.Entry () s.pack (side = Tkinter.RIGHT) e = Tkinter.Label (text = "seconde ligne") e.pack (side = Tkinter.RIGHT) La méthode pack possède trois options : 1. side : à choisir entre Tkinter.TOP (valeur par défaut), Tkinter.LEFT, Tkinter.BOTTOM, Tkinter.RIGHT. 2. expand : égale à True ou False (valeur par défaut), si cette option est vraie, l’objet occupe tout l’espace. Il n’est pas toujours évident d’obtenir du premier coup le positionnement des objets souhaités au départ et il faut tâtonner pour y arriver. Lorsque un objet n’est plus nécessaire, il est possible de le faire disparaître en appelant la méthode pack_forget. Le rappel de la méthode pack le fera réapparaître mais rarement au même endroit. s.pack_forget () # disparition s.pack () # insertion à un autre endroit 8.3.1.2 Méthode grid La méthode grid suppose que la fenêtre qui les contient est organisée selon une grille dont chaque case peut recevoir un objet. L’exemple suivant place trois objets dans les cases de coordonnées (0,0), (1,0) et (0,1). Le résultat apparaît dans la figure 8.11. l = Tkinter.Label (text = "première ligne") l.grid (column = 0, row = 0) s = Tkinter.Entry () s.grid (column = 0, row = 1) e = Tkinter.Label (text = "seconde ligne") e.grid (column = 1, row = 0) Figure 8.11 : Les objets sont placés dans une grille à l’aide de la méthode grid. Une fois que chaque objet a reçu une position, à l’affichage, il ne sera pas tenu compte des lignes et colonnes vides. La méthode grid possède plusieurs options, en voici cinq : 1. column : colonne dans laquelle sera placée l’objet. 2. columnspan : nombre de colonnes que doit occuper l’objet. 3. row : ligne dans laquelle sera placée l’objet. 4. rowspan : nombre de lignes que doit occuper l’objet. Enfin, comme pour la méthode pack, il existe une méthode grid_forget qui permet de faire disparaître les objets. s.grid_forget () # disparition 8.3.1.3 Méthode place La méthode place est sans doute la plus simple à comprendre puisqu’elle permet de placer chaque objet à une position définie par des coordonnées. Elle peut être utilisée en parallèle avec les méthodes pack et grid. l = Tkinter.Label (text = "première ligne") l.place (x=10,y=50) La méthode place_forget permet de faire disparaître un objet placer avec cette méthode. L’inconvénient de cette méthode survient lorsqu’on cherche à modifier l’emplacement d’un objet : il faut en général revoir les positions de tous les autres éléments de la fenêtre. On procède souvent par tâtonnement pour construire une fenêtre et disposer les objets. Ce travail est beaucoup plus long avec la méthode place. 8.3.2 Sous-fenêtre Les trois méthodes précédentes ne permettent pas toujours de placer les éléments comme on le désire. On souhaite parfois regrouper les objets dans des boîtes et placer celles-ci les unes par rapport aux autres. La figure 8.12 montre deux objets regroupés dans un rectangle avec à sa gauche une zone de texte. Les boîtes sont des instances de la classe Frame. Ce sont des objets comme les autres excepté le fait qu’une boîte contient d’autres objets y compris de type Frame. Pour créer une boîte, il suffit d’écrire la ligne suivante : Ensuite, il faut pouvoir affecter un objet à cette boîte f. Pour cela, il suffit que f soit le premier paramètre du constructeur de l’objet créé : l = Tkinter.Label (f, text = "première ligne") L’exemple qui suit correspond au code qui permet d’afficher la fenêtre de la figure 8.12 : f = Tkinter.Frame () l = Tkinter.Label (f, text = "première ligne") s.pack () # positionne s à l’intérieur de f f.pack (side = ) # positionne f à l’intérieur # de la fenêtre principale e = Tkinter.Label (text = "seconde ligne") e.pack_forget () e.pack (side = Tkinter.RIGHT) # positionne e à l’intérieur # de la fenêtre principale | L’utilisation de ces blocs Frame est pratique lorsque le même ensemble de contrôles apparaît dans plusieurs fenêtres différentes ou au sein de la même fenêtre. Cette possibilité est envisagée au paragraphe 8.6.3. Figure 8.12 : Les deux premiers objets - une zone de texte au-dessus d’une zone de saisie - sont regroupés dans une boîte - rectangle rouge, invisible à l’écran. A droite et centrée, une dernière zone de texte. Cet alignement est plus simple à réaliser en regroupant les deux premiers objets dans un rectangle (objet Frame). 8.4 Evénements 8.4.1 Fenêtre principale Tous les exemples des paragraphes précédents décrivent les différents objets disponibles et comment les disposer dans une fenêtre. Pour afficher cette fenêtre, il suffit d’ajouter au programme les deux lignes suivantes : root = () # ici, on trouve le code qui définit les objets # et leur positionnement root.mainloop () Remarque 8.3 : instruction root = Tkinter.Tk() Cette première instruction doit se trouver au début du programme. Si elle intervient alors qu’une méthode de positionnement (pack, grid, ou place) a déjà été appelée, le programme affichera deux fenêtres dont une vide. 8.4.2 Focus Une fenêtre peut contenir plusieurs zones de saisie, toutes capables d’intercepter la pression d’une touche du clavier et d’ajouter la lettre correspondante à la zone de saisie. Or la seule qui ajoute effectivement une lettre à son contenu est celle qui a le focus. La pression de la touche tabulation fait passer le focus d’un objet à l’autre. La figure 8.13 montre un bouton qui a le focus. Lorsqu’on désire qu’un objet en particulier ait le focus, il suffit d’appeler la méthode focus_set. e = Tkinter.Entry () e.pack () e.focus_set () Figure 8.13 : Ce bouton est entouré d’un cercle noir en pointillé, il a le focus. 8.4.3 Lancer une fonction lorsqu’un bouton est pressé La plupart de temps, le seul événement qu’on souhaite attraper est la pression d’un bouton. Le code suivant permet de créer un bouton dont l’identificateur est b. Il a pour intitulé fonctionchange_legende. On définit ensuite une fonction change_legende qui change la légende de ce bouton. L’avant-dernière ligne permet d’associer au bouton b la fonction change_legende qui est alors appelée lorsque le bouton est pressé. La dernière ligne affiche la fenêtre principale et lance l’application. # coding: latin-1 # la première ligne autorise les accents import Tkinter root = () b = Tkinter.Button (text = "fonction change_legende") b.pack () def change_legende () : global b b.config (text = "nouvelle légende") b.config (command = change_legende) root.mainloop () | Lorsque le bouton b est pressé, on vérifie qu’il change bien de légende (voir figure 8.14). L’exemple précédent associe une fonction au bouton. Lorsque l’interface devient conséquente, la lisibilité du programme en est réduite car le nombre de fonctions associées à des boutons augmentent rapidement. Pour éviter cela, il est possible d’associer au bouton une méthode de classe comme le suggère l’exemple du paragraphe 8.6.3. Figure 8.14 : La première fenêtre est celle qui apparaît lorsque le programme de la page 210 est lancé. Comme le bouton change de légende la première fois qu’il est pressé, l’apparence de la fenêtre change aussi, ce que montre la seconde image. 8.4.4 Associer n’importe quel événement à un objet Le paragraphe précédent s’est intéressé à l’association entre une fonction et la pression d’un bouton mais il est possible de faire en sorte que le programme exécute une fonction au moindre déplacement de la souris, à la pression d’une touche. Il est possible d’associer une fonction au moindre événement susceptible d’être intercepté par l’interface graphique. On peut regrouper les événements en deux classes. La première classe regroupe les événements provenant du clavier ou de la souris. Ce sont des événements en quelque sorte bruts. La seconde classe regroupe des événements produit par des objets tels qu’un bouton. En effet, lorsque celui-ci détecte le clic du bouton droit de la souris, il construit un événement ”pressiondubouton” et c’est celui-ci qui va déclencher l’exécution d’une fonction. Il n’est pas souvent nécessaire de revenir aux événements bruts car les objets proposent d’eux-mêmes de pouvoir attacher des fonctions à des événements liés à leur apparence. char | Lorsqu’une touche a été pressée, cet attribut contient son code, il ne tient pas compte des touches dites muettes comme les touches shift, ctrl, alt. Il tient pas compte non plus des touches return ou suppr. | keysym | Lorsqu’une touche a été pressée, cet attribut contient son code, quelque soit la touche, muette ou non. | num | Contient un identificateur de l’objet ayant reçu l’événement. | x,y | Coordonnées relatives de la souris par rapport au coin supérieur gauche de l’objet ayant reçu l’événement. | x_root,y_root | Coordonnées absolues de la souris par rapport au coin supérieur gauche de l’écran. | widget | Identifiant permettant d’accéder à l’objet ayant reçu l’événement. | Table 8.1 : Attributs principaux de la classe Event, ils décrivent les événements liés au clavier et à la souris. La liste complète est accessible en utilisant l’instruction help(Tkinter.Event). La méthode bind permet d’exécuter une fonction lorsqu’un certain événement donné est intercepté par un objet donné. La fonction exécutée accepte un seul paramètre de type Event qui est l’événement qui l’a déclenchée. Cette méthode a pour syntaxe : syntaxe 8.5 : méthode bind w.bind (ev, fonction) w est l’identificateur de l’objet devant intercepter l’événement désigné par la chaîne de caractères ev (voir table 8.2). fonction est la fonction qui est appelée lorsque l’événement survient. Cette fonction ne prend qu’un paramètre de type Event. <Key> | Intercepter la pression de n’importe quelle touche du clavier. | <Button-i> | Intercepter la pression d’un bouton de la souris. i doit être remplacé par 1,2,3. | <ButtonRelease-i> | Intercepter le relâchement d’un bouton de la souris. i doit être remplacé par 1,2,3. | <Double-Button-i> | <Motion> | Intercepter le mouvement de la souris, dès que le curseur bouge, la fonction liée à l’événement est appelée. | <Enter> | Intercepter un événement correspondant au fait que le curseur de la souris entre la zone graphique de l’objet. | <Leave> | Intercepter un événement correspondant au fait que le curseur de la souris sorte la zone graphique de l’objet. | Table 8.2 : Chaînes de caractères correspondant aux principaux types d’événements qu’il est possible d’intercepter avec le module Tkinter. La liste complète est accessible en utilisant l’instruction help(Tkinter.Label.bind). L’exemple suivant utilise la méthode bind pour que le seul bouton de la fenêtre intercepte toute pression d’une touche, tout mouvement et toute pression du premier bouton de la souris lorsque le curseur est au dessus de la zone graphique du bouton. La fenêtre crée par ce programme ainsi que l’affichage qui en résulte apparaissent dans la figure 8.15. import Tkinter root = () b = Tkinter.Button (text = "appuyer sur une touche") b.pack () def affiche_touche_pressee (evt) : print "--------------------------- touche pressee" print " = ", print "evt.keysym = ", evt.keysym print " = ", print "evt.x,evt.y = ", evt.x, ",", evt.y print "evt.x_root,evt.y_root = ", evt.x_root, ",", evt.y_root print "evt.widget = ", evt.widget | b.bind ("<Key>", affiche_touche_pressee) b.bind ("<Button-1>", affiche_touche_pressee) b.bind ("<Motion>", affiche_touche_pressee) b.focus_set () root.mainloop () | | | = ?? evt.keysym = ?? = 1 evt.x,evt.y = 105 , 13 evt.x_root,evt.y_root = 292 , 239 evt.widget = .9261224 | | = evt.keysym = Return = ?? evt.x,evt.y = 105 , 13 evt.x_root,evt.y_root = 292 , 239 evt.widget = .9261224 Remarque 8.6 : focus L’avant dernière ligne du programme fait intervenir la méthode focus_set. Elle stipule que le bouton doit recevoir le focus. C’est-à-dire que cet objet est celui qui peut intercepter les événements liés au clavier. Sans cette instruction, cet objet n’y a pas accès, ces événements sont dirigés vers la fenêtre principale qui ne s’en soucie pas. Remarque 8.7 : mauvais événement Les messages d’erreur liés aux événements ne sont pas forcément très explicites. Ainsi l’instruction suivante adresse un événement inexistant. b.bind ("<button-1>", affiche_touche_pressee) Lors de l’exécution, le programme déclenche la succession d’exceptions suivantes qui signifie que l’événement <button-1> n’existe pas. Traceback (most recent call last): File "", line 17, in ? b.bind ("<button-1>", affiche_touche_pressee) File "c:\python26\lib\lib-tk\", line 933, in bind return self._bind((’bind’, self._w), sequence, func, add) File "c:\python26\lib\lib-tk\", line 888, in _bind (what + (sequence, cmd)) _tkinter.TclError: bad event type or keysym "button" | Remarque 8.8 : associer un événement à tous les objets Il arrive parfois qu’un événement ne doive pas être associé à un seul objet mais à tous ceux que la fenêtre contient. C’est l’objectif de la méthode bind_all. Sa syntaxe est exactement la même que la méthode bind. b.bind_all ("<Button-1>", affiche_touche_pressee) On utilise peu cette fonction, on préfère construire des objets propres à un programme comme suggéré au paragraphe 8.6.2. Remarque 8.9 : désactiver un événement w.unbind (ev) w.unbind_all (ev) w est l’identificateur de l’objet interceptant l’événement désigné par la chaîne de caractères ev (voir table 8.2). Après l’appel à la méthode unbind, l’événement n’est plus intercepté par l’objet w. Après l’appel à la méthode unbind_all, l’événement n’est plus intercepté par aucun objet. Remarque 8.11 : événement spécifique Il est possible de définir des événements propres aux programmes. Ceux-ci ne sont générés par aucun périphérique mais explicitement par le programme lui-même. Ce mécanisme est presque toujours couplé à l’utilisation de threads. Le paragraphe 9.3 (page 231) illustre ce principe à l’aide d’un exemple à base de thread. Le paragraphe 8.6.5 page 224 propose un exemple plus simple. 8.4.5 Menu Les menus fonctionnent de la même manière que les boutons. Chaque intitulé du menu est relié à une fonction qui sera exécutée à la condition que l’utilisateur sélectionne cet intitulé. L’objet Menu ne désigne pas le menu dans son ensemble mais seulement un niveau. Par exemple, le menu présenté par la figure 8.17 est en fait un assemblage de trois menus auquel on pourrait ajouter d’autres sous-menus. Figure 8.16 : La représentation d’un menu tient plus d’un graphe que d’une liste. Chaque intitulé du menu peut être connecté à une fonction ou être le point d’entrée d’un nouveau sous-menu. Pour créer un menu ou un sous-menu, il suffit de créer un objet de type Menu : Ce menu peut être le menu principal d’une fenêtre auquel cas, il suffit de préciser à la fenêtre en question que son menu est le suivant : root.config (menu = m) mainmenu = () msousmenu = () mainmenu.add_cascade (label = "sous-menu 1", menu = msousmenu) En revanche, si on souhaite affecter une fonction à un menu, on utilisera la méthode add_command : def fonction1 () : . m = () mainmenu.add_command (label = "fonction 1", command = fonction1) | L’exemple suivant regroupe les fonctionnalités présentées ci-dessus. import Tkinter root = () e = (width = 50, height = 10) e.pack () m = () sm1 = () sm2 = () m.add_cascade (label = "sous-menu 1", menu = sm1) m.add_cascade (label = "sous-menu 2", menu = sm2) nb = 0 def affiche () : print "fonction affiche" def calcul () : print "fonction calcul ", 3 * 4 def ajoute_bouton () : global nb nb += 1 b = Tkinter.Button (text = "bouton " + str (nb)) b.pack () sm1.add_command (label = "affiche", command = affiche) sm1.add_command (label = "calcul", command = calcul) sm2.add_command (label = "ajoute_bouton", command = ajoute_bouton) sm2.add_command (label = "fin", command = root.destroy) root.config (menu = m, width = 200) root.title ("essai de menu") #help () root.mainloop () | Chaque intitulé d’un menu est ajouté en fin de liste, il est possible d’en supprimer certains à partir de leur position avec la méthode delete : m = () m.add_command ( ) m.delete (1, 2) # supprime le second intitulé # supprime les intitulés compris entre 1 et 2 exclu | 8.4.6 Fonctions prédéfinies Il est possible de détruire la fenêtre principale, ce qui mettra fin au programme si celui-ci ne prévoit rien après la fonction mainloop. La destruction de la fenêtre s’effectue par la méthode destroy. Le programme suivant crée une fenêtre avec un seul bouton qui, s’il est pressé, mettra fin à l’application. import Tkinter root = () Tkinter.Button (text = "fin", command = root.destroy).pack () root.mainloop () destroy() | Détruit la fenêtre. | deiconify() | La fenêtre reprend une taille normale. | geometry(s) | Modifie la taille de la fenêtre. s est une chaîne de caractères de type ”wxhxy”. w et h sont la largeur et la hauteur. x et y sont la position du coin supérieur haut à l’écran. | iconify() | La fenêtre se réduit à un icône. | resizable(w,h) | Spécifie si la fenêtre peut changer de taille. w et h sont des booléens. | title(s) | Change le titre de la fenêtre, s est une chaîne de caractères. | withdraw() | Fait disparaître la fenêtre. La fonction inverse est deiconify. | Table 8.3 : Liste non exhaustives des méthodes de la classe Toplevel qui le type de la fenêtre principale. 8.5 D’autres fenêtres 8.5.1 Créer une seconde boîte de dialogues Lorsqu’un programme doit utiliser plusieurs fenêtres et non pas une seule, l’emploi de l’objet Toplevel est inévitable. L’instruction root = Tkinter.Tk() crée la fenêtre principale, l’instruction win = Tkinter.Toplevel() crée une seconde fenêtre qui fonctionne exactement comme la fenêtre principale puisqu’elle dispose elle aussi d’une boucle de messages via la méthode mainloop. import Tkinter win = Tkinter.Toplevel () win.mainloop () # zone_texte appartient à la fenêtre principale zone_texte = Tkinter.Label (text = "premier texte") Pour l’inclure à une fenêtre Toplevel, cette syntaxe devient : # zone_texte appartient à la fenêtre top top = Tkinter.Toplevel () zone_texte = Tkinter.Label (top, text = "premier texte") Lors de la définition de chaque objet ou widget, si le premier paramètre est de type Toplevel, alors ce paramètre sera affecté à la fenêtre passée en premier argument et non à la fenêtre principale. Ce principe est le même que celui de la sous-fenêtre Frame (voir paragraphe 8.3.2, page 208). La seule différence provient du fait que l’objet Toplevel est une fenêtre autonome qui peut attendre un message grâce à la méthode mainloop, ce n’est pas le cas de l’objet Frame. Toutefois, il est possible d’afficher plusieurs fenêtre Toplevel simultanément. Le programme suivant en est un exemple : # coding: latin-1 import Tkinter class nouvelle_fenetre : resultat = [] def top (self) : sec = Tkinter.Toplevel () Tkinter.Label (sec, text="entrer quelque chose").pack () saisie = Tkinter.Entry (sec) | () Tkinter.Button (sec, text = "valider", command = ).pack () sec.mainloop () nouvelle_fenetre.resultat.append ( () ) sec.destroy () root = () #fenetre principale a = Tkinter.Button (text = "fenêtre Toplevel", command = nouvelle_fenetre ().top) a.pack() root.mainloop() for a in nouvelle_fenetre.resultat : print "contenu ", a | 8.5.2 Fenêtres standard import Tix as Tk root = () def command_print () : print ("value") box = Tk.FileSelectBox (root) box.config (directory="c:\\") () Tk.Button (root, text = "print", command = command_print).pack () root.mainloop () | Figure 8.18 : Exemple d’une fenêtre graphique. Celle-ci permet de sélectionner un fichier. C’est une fenêtre prédéfinie du module Tix qui est une extension de Tkinter. 8.6 Constructions classiques L’objectif des paragraphes qui suivent est d’introduire quelques schémas de construction d’interfaces qui reviennent fréquemment. La première règle de programmation qu’il est préférable de suivre est d’isoler la partie interface du reste du programme. La gestion événementielle a pour défaut parfois de disséminer un traitement, un calcul à plusieurs endroits de l’interface. C’est le cas par exemple de longs calculs dont on souhaite connaître l’avancée. Le calcul est lancé par la pression d’un bouton puis son déroulement est "espionné" par un événement régulier comme un compte à rebours. Le principal problème des interfaces survient lors du traitement d’un événement : pendant ce temps, l’interface n’est pas réactive et ne réagit plus aux autres événements jusqu’à ce que le traitement de l’événement en cours soit terminé. Pour contourner ce problème, il est possible soit de découper un calcul en petites fonctions chacune très rapide, cela suppose que ce calcul sera mené à bien par une succession d’événements. Il est également possible de lancer un thread, principe décrit au paragraphe 9.3 (page 231). 8.6.1 Compte à rebours Il est possible de demander à un objet d’appeler une fonction après un certains laps de temps exprimé un millisecondes. Le programme suivant crée un objet de type Label. Il contient une fonction qui change son contenu et lui affecte un compte à rebours qui impose à l’objet de rappeler cette fonction 1000 millisecondes plus tard. Le résultat est un programme qui crée la fenêtre 8.19 et change son contenu toutes les secondes. import Tkinter root = () l = Tkinter.Label (text = "0 secondes") l.pack () sec = 0 id = None def change_legende() : global l global sec global id sec += 1 l.config (text = "%d secondes" % sec) id = l.after (1000, change_legende) l.after (1000, change_legende) root.mainloop () | La méthode after retourne un entier permettant d’identifier le compte à rebours qu’il est possible d’interrompre en utilisant la méthode after_cancel. Dans l’exemple précédent, il faudrait utiliser l’instruction suivante : 8.6.2 Contrôles personnalisés : utilisation des classes On peut personnifier un contrôle. Par exemple, on peut mettre en évidence l’intitulé d’une liste sous le curseur de la souris. Le moyen le plus simple est de créer une nouvelle classe qui se substituera au classique Listbox. Il suffit que cette nouvelle classe hérite de Listbox en prenant soin de lui donner un constructeur reprenant les mêmes paramètres que celui de la classe Listbox. De cette façon, il suffit de remplacer Listbox par MaListbox pour changer l’apparence d’une liste. # coding: latin-1 import Tkinter class MaListbox (Tkinter.Listbox) : def __init__ (self, master = None, cnf={}, **kw) : Tkinter.Listbox.__init__ (self, master, cnf, **kw) ("<Motion>", self.mouvement) = None # mémoire l’ancienne position du curseur def mouvement (self, ev) : if != None : self.itemconfigure(, bg=’’) self.itemconfigure (pos, bg=’gray’) = pos root = () b = MaListbox () b.insert ("end", "ligne 1") b.insert ("end", "ligne 2") b.insert ("end", "ligne 3") b.pack () b.focus_set () root.mainloop () | Dans ce cas précis, on fait en sorte que le contrôle intercepte le mouvement du curseur. Lorsque celui-ci bouge, la méthode mouvement est appelée comme le constructeur de MaListbox l’a spécifié. La méthode nearest permet de définir l’intitulé le plus proche du curseur. La méthode itemconfigure permet de changer le fond de cet intitulé en gris après avoir modifié le fond de l’intitulé précédent pour qu’il retrouve sa couleur d’avant. Le résultat est illustré la figure 8.20. 8.6.3 Fenêtres personnalisées : utilisation des classes Cet exemple prolonge l’idée du paragraphe précédent. Lorsque l’interface devient complexe, il peut être utile de créer ses propres fenêtres. Jusqu’à présent, seules des fonctions ont été attachées à événement comme la pression d’un bouton mais il est possible d’attacher la méthode d’une classe ce que développe l’exemple qui suit. # coding: latin-1 import Tkinter as Tk class MaFenetre : def __init__ (self, win) : = win self.creation () def creation (self) : b1 = Tk.Button (, text="bouton 1", command=self.commande_bouton1) b2 = Tk.Button (, text="bouton 2", command=self.commande_bouton2) b3 = Tk.Button (, text="disparition", command=self.disparition) (row=0, column=0) (row=0, column=1) (row=0, column=2) = Tk.Label (, text = "-") def commande_bouton1 (self) : # on déplace l’objet lab de type Label .configure (text = "bouton 1 appuyé") (row = 1, column = 0) def commande_bouton2 (self) : # on déplace l’objet lab de type Label .configure (text = "bouton 2 appuyé") (row = 1, column = 1) def disparition (self) : if __name__ == "__main__" : root = () f = MaFenetre (root) root.mainloop () | Ce programme crée trois boutons et attache à chacun d’entre eux une méthode de la classe MaFenetre. Le constructeur de la classe prend comme unique paramètre un pointeur sur un objet qui peut être la fenêtre principale, un objet de type Frame ou Toplevel. Cette construction permet de considérer cet ensemble de trois boutons comme un objet à part entière; de ce fait il peut être inséré plusieurs fois comme le montre l’exemple suivant illustré par la figure 8.21. root = () f = Tk.Frame () f.pack () MaFenetre (f) | # première instance | g = Tk.Frame () g.pack () MaFenetre (g) root.mainloop () | # seconde instance | Figure 8.21 : Aspect de la classe MaFenetre définie au paragraphe 8.6.3. La fenêtre est en fait composée de deux instances de MaFenetre. 8.6.4 Séquence d’événements Il est facile de faire réagir le programme en fonction d’un événement, il suffit d’attacher cet événement à une méthode ou une fonction. En revanche, faire réagir le programme en fonction d’une séquence d’événements est plus complexe. En effet, le premier d’événement de la séquence active une fonction, il n’est pas possible d’attendre le second événement dans cette même fonction, ce dernier ne sera observable que si on sort de cette première fonction pour revenir à la fonction mainloop, la seule capable de saisir le prochain événement. La figure 8.22 précise la gestion des messages. Tkinter se charge de la réception des messages puis de l’appel au traitement correspondant indiqué par la méthode ou la fonction attachée à l’événement. Le programmeur peut définir les traitements associés à chaque événement. Ces deux parties sont scindées et à moins de reprogrammer sa boucle de message, il n’est pas évident de consulter les événements intervenus depuis le début du traitement de l’un d’eux. Les classes offrent un moyen simple de gérer les séquences d’événements au sein d’une fenêtre. Celle-ci fera l’objet d’une classe qui mémorise les séquences d’événements. Tous les événements feront appel à des méthodes différentes, chacune d’elles ajoutera l’événement à une liste. Après cette ajout, une autre méthode sera appelée pour rechercher une séquence d’événements particulière. Le résultat est également illustré par la figure 8.23. # coding: latin-1 import Tkinter as Tk class MaFenetreSeq : def __init__ (self, win) : = win self.creation () self.sequence = [] def creation (self) : b1 = Tk.Button (, text="bouton 1", command=self.commande_bouton1) b2 = Tk.Button (, text="bouton 2", command=self.commande_bouton2) b3 = Tk.Button (, text="remise à zéro", ) (row=0, column=0) (row=0, column=1) (row=0, column=2) = Tk.Label (, text = "-") def commande_bouton1 (self) : # ajoute 1 à la liste self.sequence self.sequence.append (1) self.controle () def commande_bouton2 (self) : # ajoute 2 à la liste self.sequence self.sequence.append (2) self.controle () def zero (self) : # on vide la liste self.sequence self.sequence = [] .grid_forget () def controle (self) : # on compare la liste sequence à [1,2,1] et [2,2,1,1] # dans ce cas, on fait apparaître l’objet lab l = len (self.sequence) if l >= 3 and self.sequence [l-3:] == [1,2,1] : .configure (text = "séquence 1 2 1") (row = 1, column = 0) elif l >= 4 and self.sequence [l-4:] == [2,2,1,1] : .configure (text = "séquence 2 2 1 1") (row = 1, column = 1) if __name__ == "__main__" : root = () f = MaFenetreSeq (root) root.mainloop () | 8.6.5 Communiquer un résultat par message Le programme suivant utilise ce concept. La pression d’un bouton appelle une fonction event_generate qui génère un message personnalisé <<perso>> avec comme paramètre rooty = ?5. A son tour, celui-ci est attrapé et dirigé vers la fonction perso qui affiche l’attribut y_root de la classe Event qui a reçu la valeur ?5 lors de l’appel de la fonction event_generate. Ce procédé ne permet toutefois que de renvoyer que quelques résultats entiers. # coding: latin-1 import Tkinter def affiche_touche_pressee () : root.event_generate ("<<perso>>", rooty = -5) def perso (evt) : print "perso", evt.y_root root = () b = Tkinter.Button (text = "clic", \ command = affiche_touche_pressee) b.pack () ("<<perso>>", perso) # on intercepte un événement personnalisé root.mainloop () | Ce principe est plus utilisé lorsque l’interface graphique est couplée avec les threads, l’ensemble est présenté au paragraphe 9.3.
Chapitre 9 Threads Jusqu’aux années 2003-2004, l’évolution des microprocesseurs était une course vers une augmentation de la puissance, autant en terme de nombre de transistors qu’en fréquence de fonctionnement. Arrivant aux limites de la technologie actuelle, cette évolution s’est tournée maintenant vers la construction de processeurs multicœurs, c’est-à-dire des machines capables d’exécuter des programmes simultanément, de maintenir plusieurs fils d’exécution en parallèle. Le second usage est couplé aux interfaces graphiques. Lorsque l’utilisateur entame un processus long après avoir cliqué sur un bouton, l’interface graphique ne réagit plus jusqu’à ce que ce processus s’achève. Afin d’éviter cet inconvénient, l’interface graphique va commencer un thread qui va exécuter ce processus. L’interface graphique n’a plus qu’à attendre la fin du thread, et pendant tout ce temps, elle sera également capable de traiter tout autre événement provenant de l’utilisateur. Le dernier usage concerne la communication entre ordinateurs ou plus généralement la communication Internet. C’est une communication asynchrone : l’ordinateur effectue des tâches en même temps qu’il écoute un port par lequel d’autres ordinateurs communiquent avec lui. Plus précisément, le programme suit deux fils d’exécution : le fil principal et un thread qui ne fait qu’attendre et traiter les messages qu’il reçoit via un port. La synchronisation est un point commun à ces trois usages. Ce terme désigne la dépendance entre les threads. Lors d’un calcul distribué, le résultat final dépend des résultats retournés par chaque thread, il faut donc attendre que les deux fils d’exécution aient produit le résultat attendu : il faut que les deux fils d’exécution se synchronisent. Ce document ne s’étendra pas longuement sur les threads bien qu’ils soient amenés à devenir un élément incontournable de tout programme désirant tirer parti des derniers processeurs. 9.1 Premier thread 1. surcharger la classe threading.Thread, 2. surcharger le constructeur sans oublier d’appeler le constructeur threading.Thread.__init__, 3. surcharger la méthode run, c’est le code que devra exécuter le thread, 4. créer une instance de la nouvelle classe et appeler la méthode start pour lancer le thread secondaire qui formera le second fil d’exécution. Le programme principal est appelé le thread principal. Voici ce que cela donne dans un exemple : # coding: cp1252 import threading, time class MonThread (threading.Thread) : def __init__ (self, jusqua) : # jusqua = donnée supplémentaire threading.Thread.__init__(self)# ne pas oublier cette ligne # (appel au constructeur de la classe mère) self.jusqua = jusqua # donnée supplémentaire ajoutée à la classe def run (self) : for i in range (0, self.jusqua) : print "thread ", i time.sleep (0.1) # attend 100 millisecondes sans rien faire # facilite la lecture de l’affichage m = MonThread (1000) # crée le thread m.start () # démarre le thread, # l’instruction est exécutée en quelques millisecondes # quelque soit la durée du thread for i in range (0,1000) : print "programme ", i time.sleep (0.1) # attend 100 millisecondes sans rien faire # facilite la lecture de l’affichage | Le programme affiche des lignes qui proviennent du thread principal et du thread secondaire dont les affichages diffèrent. Le précédent programme a été adapté pour lancer deux threads secondaires en plus du thread principal. Les lignes modifiées par rapport au programme précédent sont commentées. # coding: cp1252 import threading, time class MonThread (threading.Thread) : def __init__ (self, jusqua, s) : threading.Thread.__init__ (self) self.jusqua = jusqua self.s = s def run (self) : for i in range (0, self.jusqua) : print "thread ", self.s, " : ", i time.sleep (0.1) m = MonThread (1000, "A") m.start () m2 = MonThread (1000, "B") # crée un second thread m2.start () # démarre le thread, for i in range (0,1000) : print "programme ", i time.sleep (0.1) | thread A : 0 programme 0 thread B : 0 thread A : 1 thread B : 1 programme 1 thread B : 2 thread A : 2 | Remarque 9.1 : utilisation le la fonction sleep Tous les exemples présentés dans ce chapitre font souvent intervenir l’instruction time.sleep( ). A moins que ce ne soit explicitement précisé, elle sert la plupart du temps à ralentir l’exécution du programme cité en exemple afin que celle-ci soit humainement observable ou pour exagérer un défaut de synchronisation. Cette fonction est d’ordinaire beaucoup moins fréquente. 9.2 Synchronisation 9.2.1 Attente La première situation dans laquelle on a besoin de synchroniser deux threads est l’attente d’un thread secondaire par le thread principal. Et pour ce faire, on a besoin de l’accès par les deux fils d’exécution à une même variable qui indiquera l’état du thread. Dans le programme suivant, on ajoute l’attribut etat à la classe MonThread qui va indiquer l’état du thread : – True pour en marche – False pour à l’arrêt # coding: latin-1 import threading, time class MonThread (threading.Thread) : def __init__ (self, jusqua) : threading.Thread.__init__ (self) self.jusqua = jusqua = False # l’état du thread est soit False (à l’arrêt) # soit True (en marche) def run (self) : = True # on passe en mode marche for i in range (0, self.jusqua) : print "thread itération ", i time.sleep (0.1) = False # on revient en mode arrêt m = MonThread (10) # crée un thread m.start () # démarre le thread, print "début" while m.etat == False : # on attend que le thread démarre time.sleep (0.1) # voir remarque ci-dessous while m.etat == True : # on attend que le thread s’arrête # il faut introduire l’instruction time.sleep pour temporiser, il n’est pas # nécessaire de vérifier sans cesse que le thread est toujours en marche # il suffit de le vérifier tous les 100 millisecondes # dans le cas contraire, la machine passe son temps à vérifier au lieu # de se consacrer à l’exécution du thread time.sleep (0.1) print "fin" | Ce mécanisme d’attente peut également être codé en utilisation les objets Condition et Event du module threading. Ces deux objets permettent d’éviter l’utilisation de la méthode sleep. # coding: latin-1 import threading, time class MonThread (threading.Thread) : def __init__ (self, jusqua, event) : # event = objet Event threading.Thread.__init__ (self) # = donnée supplémentaire self.jusqua = jusqua def run (self) : for i in range (0, self.jusqua) : print "thread itération ", i time.sleep (0.1) () # on indique qu’on a fini : # on active l’object self.event print "début" event = threading.Event () # on crée un objet de type Event event.clear () # on désactive l’ojet Event m = MonThread (10, event) # crée un thread m.start () # démarre le thread, () # on attend jusqu’à ce que l’objet soit activé # (0.1) : n’attend qu’un print "fin" # seulement 1 dizième de seconde | La méthode wait de l’objet Event attend que l’objet soit activé. Elle peut attendre indéfiniment ou attendre pendant une durée donnée seulement. Pour afficher la durée d’attente, on pourrait utiliser une boucle comme la suivante : m.start () while not event.isSet (): print "j’attends" (0.1) print "fin" | La méthode isSet permet de savoir si l’événement est bloquant ou non. Le programme affiche ”j0attends” puis attend le thread un dixième de secondes. Au delà de cette durée, il vérifie l’état de l’événement puis recommence si le thread n’est pas fini. Ces objets de synchronisation sont plus efficaces que le mécanisme décrit dans le premier programme car il fait appel aux fonctions du système d’exploitation. 9.2.2 Partage d’informations Dans cet exemple, l’information partagée est la chaîne de caractères message, le verrou sert à protéger la fonction ajoute contre des ajouts simultanés. Si les deux threads veulent modifier message en même temps, un thread va entrer dans la fonction ajoute alors que l’autre n’en est pas encore sorti. Les résultats seraient imprévisibles car cette fonction modifie la variable qu’ils utilisent. On aboutit à l’exemple suivant : # coding: latin-1 import threading, time message = "" verrou = () def ajoute (c) : global message # message et verrou sont des variables gloables global verrou # pour ne pas qu’elle disparaisse dès la fin de la fonction verrou.acquire () # on protège ce qui suit (*) s = message + c # instructions jamais exécutée simultanément par 2 threads time.sleep (0.001) # time.sleep : pour exagérer le défaut de synchronisation message = s # si verrou n’est pas utilisé verrou.release () # on quitte la section protégée (*) class MonThread (threading.Thread) : def __init__ (self, jusqua, event, s) : threading.Thread.__init__ (self) self.jusqua = jusqua self.s = s self.event = event def run (self) : for i in range (0, self.jusqua) : ajoute (self.s) time.sleep (0.003) () print "début" # synchronisation attente e1 = threading.Event () e2 = threading.Event () e1.clear () e2.clear () m1 = MonThread (10, e1, "1") # crée un thread m1.start () # démarre le thread, m2 = MonThread (10, e2, "2") # crée un second thread () () print "longueur ", len(message) # affiche 20 print "message = ", message # affiche quelque chose comme 12212112211212121221 | Les trois instructions protégées pourraient être résumées en une seule : message += c; le résultat resterait inchangé. En revanche, en commentant les instructions verrou.acquire() et verrou.release() de ce programme , la longueur du résultat final message est variable alors qu’elle devrait être de 20 puisque les deux threads appellent chacun 10 fois dans la fonction ajoute. Le tableau suivant montre l’évolution des variables message, c, s durant deux premiers appels qui s’entremêlent. Le résultat devrait être ”12” pour message mais un caractère a été perdu. Il faut retenir que si la variable message est globale, les deux autres c, s sont locales et donc différentes pour les deux threads. ordre | thread 1 | thread 2 | message | c | s | 1 | s = message + c | | ”” | ”1” | ”1” | 2 | | s = message + c | ”” | ”2” | ”2” | 3 | time.sleep(0.001) | | ”” | ”1” | ”1” | 4 | | time.sleep(0.001) | ”” | ”2” | ”2” | 5 | message = s | | ”1” | ”1” | ”1” | 6 | | message = s | ”2” | ”2” | ”2” | Le verrou empêche d’exécuter une même portion de code en même temps, un code qui modifie des données partagées. C’est pourquoi le verrou est souvent déclaré au même endroit que les données qu’il protège. Le verrou de type Lock n’autorise qu’un seul thread à la fois à l’intérieur de la portion de code protégée ce qui aboutit au schéma suivant : ordre | thread 1 | thread 2 | message | c | s | 1 | s = message + c | | ”” | ”1” | ”1” | 2 | time.sleep(0.001) | | ”” | ”1” | ”1” | 3 | message = s | | ”1” | ”1” | ”1” | 4 | | s = message + c | ”1” | ”12” | 5 | | time.sleep(0.001) | ”1” | ”2” | ”12” | 6 | | message = s | ”12” | ”2” | ”12” | Remarque 9.2 : réduction des accès à quelques threads Le verrou de type Semaphore autorise un nombre maximal de thread à parcourir le même code. Ce procédé est parfois utile si le code en question permet d’imprimer un document. Cela permet de limiter sans interdire les accès simultanés aux ressources de l’ordinateur. Remarque 9.3 : blocage d’un programme Ce mécanisme de verrou peut aboutir à des blocages avec deux threads et deux portions de code protégées. Chaque thread est "coincé" dans une section attendant que l’autre libère la sienne. Dans ce cas de figure, il est conseillé d’utiliser le même verrou pour protéger les deux sections. Ainsi, chaque thread ne pourra pas entrer dans l’une ou l’autre des portions de code protégées tant que l’une d’entre elles est visitée par l’autre thread. 9.3 Interface graphique Un programme bâti autour d’une interface graphique inclut nécessairement une boucle de message. Celle-ci attend les messages en provenance de l’interface. Lorsqu’un de ceux-ci lui commande de lancer un traitement long, l’interface graphique n’est plus en mesure de réagir aux événements qui lui viennent pendant ce temps. Afin de remédier cela, il suffit d’insérer le traitement dans un thread. A la fin de ce dernier, un événement sera envoyé à l’interface afin de lui signifier la fin du traitement. Le paragraphe 8.4.4 a montré comment associer un événement particulier à une fenêtre. La différence ici est que l’événement accroché à la fenêtre n’est pas prédéfini par le module Tkinter mais par le programme lui-même - dans cet exemple # coding: latin-1 import threading, time, random, copy # définition du thread class MonThread (threading.Thread) : def __init__ (self, win, res) : threading.Thread.__init__ (self) = win # on mémorise une référence sur la fenêtre = res def run (self) : for i in range (0, 10) : print "thread ", i time.sleep (0.1) # afin que le thread retourne un résultat # désigne thread_resultat qui reçoit un nombre de plus h = random.randint (0,100) .append (h) # on lance un événement <<thread_fini>> à la fenêtre principale # pour lui dire que le thread est fini, l’événement est ensuite # géré par la boucle principale de messages # on peut transmettre également le résultat lors de l’envoi du message # en utilisant un attribut de la classe Event pour son propre compte .event_generate ("<<thread_fini>>", x = h) thread_resultat = [] def lance_thread () : global thread_resultat # fonction appelée lors de la pression du bouton # on change la légnde de la zone de texte text .config (text = "thread démarré") text2.config (text = "thread démarré") # on désactive le bouton pour éviter de lancer deux threads en même temps bouton.config (state = TK.DISABLED) # on lance le thread m = MonThread (root, thread_resultat) m.start () def thread_fini_fonction (e) : global thread_resultat # fonction appelée lorsque le thread est fini print "la fenêtre sait que le thread est fini" # on change la légende de la zone de texte text .config (text = "thread fini + résultat " + str (thread_resultat)) text2.config (text = "thread fini + résultat (e.x) " + str (e.x)) # on réactive le bouton de façon à pouvoir lancer un autre thread bouton.config (state = TK.NORMAL) | import Tkinter as TK # on crée la fenêtre root = () # on associe une fonction à un événement <<thread_fini>> propre au programme ("<<thread_fini>>", thread_fini_fonction) # on active la boucle principale de message root.mainloop () | La figure 9.1 contient la fenêtre affichée par le programme lorsqu’elle attend la pression du bouton qui lance le thread et lorsqu’elle attend la fin de l’exécution de ce thread. Remarque 9.4 : méthode event_generate Le programme précédent utilise une astuce pour retourner un résultat autrement qu’un utilisant un paramètre global. On peut adjoindre lors de l’appel à la méthode event_generate quelques informations supplémentaires attachées à l’événement en utilisant les attributs prédéfinis de la classe Event. Dans cet exemple, on utilise l’attribut x pour retourner le dernier entier tiré aléatoirement. Figure 9.1 : La première image est la fenêtre après trois exécutions du thread. La liste thread_resultat contient trois nombres. Une fois l’unique bouton pressé, la fenêtre change d’aspect pour devenir comme la seconde image. Cette transition est assurée par la fonction lance_thread reliée au bouton. La transition inverse est assurée par la fonction thread_fini_fonction qui est reliée à l’événement que génère le thread lorsqu’il a terminé. 9.4 Files de messages En ce qui concerne le troisième usage, c’est un cas où tout au long du programme, il y a constamment au moins deux threads actifs : un thread principal et un thread qui attend. Dans ce cas, l’échange et la synchronisation d’informations est inévitable et il est souvent fastidieux de concevoir la synchronisation. C’est pourquoi on la conçoit presque toujours sous forme de messages que les threads s’envoient. Il existe un objet Queue du module Queue qui facilite cet aspect. C’est une liste qui possède son propre verrou de sorte que n’importe quel thread peut ajouter ou retirer des éléments de cette liste. Elle est utilisée principalement via quatre méthodes. Deux méthodes get sont utilisées au sein du thread qui possède la pile. Deux méthodes put sont appelées par des threads étrangers. get([timeout = ]) | Retourne un élément de la liste ou attend qu’il y en ait un, le supprime si elle en trouve un. Si timeout est renseigné, la fonction attend au plus timeout secondes, sinon, elle déclenche l’exception Queue.Empty. | get_nowait() | Retourne un élément de la liste s’il y a en un, dans ce cas, cet élément est supprimé. Dans le cas contraire, la méthode déclenche l’exception Queue.Empty. | put(e[,timeout = ]) | Ajoute l’élément e à la liste ou attend qu’une place se libère si la liste est pleine. Si timeout est renseigné, la fonction attend au plus timeout secondes, sinon, elle déclenche l’exception Queue.Full. | put_nowait(e) | Ajoute l’élément e à la liste ou déclenche l’exception Queue.Full si la liste est pleine. | qsize() | Retourne la taille de la pile. # coding: latin-1 import threading, time, Queue, random class Joueur (threading.Thread) : # initialisation def __init__ (self, nom, e, nb = 1000, temps = 0.1) : threading.Thread.__init__(self) = nb self.queue = Queue.Queue () = nom self.event = e self.temps = temps # temps de réflexion def Joueur (self, autre_joueur) : self.autre = autre_joueur | # méthodes : l’adversaire m’envoie un message def Joue (self, nombre) : self.queue.put_nowait ( ("essai", nombre) ) def Dessus (self, nombre) : self.queue.put_nowait ( ("dessus", nombre) ) def Dessous (self, nombre) : self.queue.put_nowait ( ("dessous", nombre) ) def Gagne (self, nombre) : while not self.queue.empty () : try () except : pass ( ("gagne", nombre) ) # je joue def run (self) : x = random.randint (0,) print , " : je joue (", x, ")" i = 0 a = 0 b = while True : time.sleep (self.temps) try : m,n = self.queue.get_nowait () # désynchronisé #m,n = (timeout = 0.5)# l’un après l’autre except Queue.Empty : m,n = None,None # traitement du message --> réponse à l’adversaire if m == "essai" : if n == x : self.autre.Gagne (n) print , " : j’ai perdu après ", i, " essais" break elif n < x : self.autre.Dessus (n) else : self.autre.Dessous (n) elif m == "dessus" : a = max (a, n+1) continue # assure l’équité en mode l’un après l’autre elif m == "dessous" : b = min (b, n-1) continue # assure l’équité en mode l’un après l’autre elif m == "gagne" : print , " : j’ai gagné en ", i, " essais, solution ", n break # on fait une tentative if a == b : n = a else : n = random.randint (a,b) (n) i += 1 print , " : je tente ", n, " écart ", b-a, \ " à traiter ", self.queue.qsize () # fini print , " : j’arrête" ()
e2.clear () # création des joueurs A = Joueur ("A", e1, 1000, temps = 0.1) B = Joueur ("B", e2, 1000, temps = 0.3) # chaque joueur sait qui est l’autre A.Joueur (B) B.Joueur (A) # le jeu commence A.start () B.start () # on attend la fin de la partie () () | Si la méthode get est choisie, les joueurs doivent attendre une tentative de l’adversaire avant de proposer la leur. Dans l’autre cas, la méthode get_nowait permet de ne pas attendre sa réponse et d’envoyer plusieurs propositions à l’adversaire qui ne répondra pas plus vite pour autant. Dans cette configuration, le joueur A est trois fois plus réactif ce qui explique les résultats qui suivent. A : je joue ( 8 ) B : je joue ( 569 ) A : je tente 42 écart 1000 à traiter 0 A : je tente 791 écart 1000 à traiter 0 A : je tente 528 écart 62 à traiter 0 B : je tente 20 écart 43 à traiter 57 A : je tente 508 écart 62 à traiter 0 A : je tente 548 écart 62 à traiter 0 B : je tente 8 écart 43 à traiter 59 A : j’ai perdu après 67 essais A : j’arrête B : j’ai gagné en 23 essais, solution 8B : j’arrête Les affichages se chevauchent parfois, il faudrait pour éviter cela synchroniser l’affichage à l’aide d’un verrou. Deuxième partie ENONCÉS PRATIQUES, EXERCICES Cette partie regroupe quelques énoncés que les élèves de l’ENSAE ont étudié ou à partir desquels ils ont été évalués entre les années 2005 et 2008. On retient souvent que le boulot d’un informaticien n’est pas compliqué, qu’il est plutôt plutôt ennuyeux et redondant. On oublie parfois que sans l’informatique, certaines tâches seraient tout autant longues et fastidieuses. Cette partie présente certaines tournures d’esprits qui permettent de gagner du temps lors de la conception de programmes et peut-être derendre la programmation moins répétitive, le travail moins fastidieux. Cette partie se termine par trois années d’énoncés d’examens d’informatique. Les exercices proposés illustrent parfois le côté ennuyeux d’un programme informatique : sa mise en œuvre dépasse souvent de loin sa conception. La correction des erreurs ou bugs est la partie la plus longue et la plus rébarbative. Ces exercices ont pour objectif d’accélérer cette étape en proposant des situations qui reviennent fréquemment. C’est aussi pour cela que lors d’entretiens d’embauche, des exercices similaires sont présentés aux candidats pour vérifier leur expérience en programmation. Chapitre 10 Exercices pratiques pour s’entraîner 10.1 Montant numérique, montant littéral L’objectif de cet exercice est d’écrire un programme qui convertit en entier un nombre écrit sous forme littérale. On suppose que tous les nombres sont écrits en minuscules et correctement orthographiés. Cet exercice tourne autour des manipulations de chaînes de caractères et principalement les deux fonctions suivantes : Fonctions utiles : | | replace(s,old,new) | A l’intérieur de la chaîne s, remplace toutes les occurrences de la chaîne old par la chaîne new. Pour l’utiliser, il faut ajouter au début du programme la ligne importstring puis écrire l = string . replace (”dix ? neuf”, ” ? ”, ” ”), par exemple. | split(s[,sep]) | Retourne une liste contenant tous les mots de la chaîne de caractères s. Si le paramètre facultatif sep est renseigné, c’est cette chaîne qui fait office de séparateur entre mot et non les espaces. Pour l’utiliser, il faut ajouter au début du programme la ligne importstring puis écrire l = string . split (”vingtetun”, ” ”), par exemple. | L’appel à la fonction replace peut se faire en la considérant comme une méthode de la classe str ou une fonction du module string. import string s = "dix-neuf" | 1)Ecrire une fonction qui prend un nombre littéral et retourne une liste contenant tous ses mots. 2)Ecrire une fonction qui reçoit une chaîne de caractères correspondant à un nombre compris entre 0 et 16 inclus ou à un nombre parmi 20, 30, 40, 50, 60. La fonction doit retourner un entier correspondant à sa valeur. 3)A l’aide de la fonction précédente, écrire une fonction qui reçoit une chaîne de caractères contenant un nombre compris entre 0 et 99 inclus et qui retourne sa valeur.
4)Pour vérifier que la fonction précédente marche bien dans tous les cas, écrire une fonction qui écrit un nombre sous sa forme littérale puis vérifier que ces deux fonctions marchent correctement pour les nombres compris entre 0 et 99. Correction A chaque question correspond une fonction utilisant celles décrites dans les questions précédentes. Le programme complet est obtenu en juxtaposant chaque morceau. 1) # coding: latin-1 def lire_separation(s): """divise un nombre littéral en mots""" s = s.replace ("-", " ") # on remplace les tirets par des espaces # pour découper en mots même # les mots composés return s.split () | On pourrait écrire cette fonction en utilisant les expressions régulières en utilisant les expressions régulières (voir split décrite au paragraphe 7.6.3). page 188. import re def lire_separation(s): """divise un nombre littéral en mots avec les expressions régulières""" return re.compile ("[- ]").split (s) | 2)Il n’existe pas qu’une seule manière de rédiger cette fonction. La première, la plus simple, est sans doute la suivante : def valeur_mot (s) : """convertit numériquement les nombres inclus entre 0 et 16 inclus, else : return 0 # ce cas ne doit normalement pas # se produire | La solution suivante utilise un dictionnaire. Cette écriture peut éviter un copiercoller long pour la dernière question. def valeur_mot (s) : dico = {’cinquante’: 50, ’quarante’: 40, ’onze’: 11, ’huit’: 8, ’six’: 6, \ ’quinze’: 15, ’trente’: 30, ’douze’: 12, ’cinq’: 5, ’deux’: 2, \ ’quatorze’: 14, ’neuf’: 9, ’soixante’: 60, ’quatre’: 4, \ ’zéro’: 0, ’treize’: 13, ’trois’: 3, ’seize’: 16, \ ’vingt’: 20, ’un’: 1, ’dix’: 10, ’sept’: 7} if s not in dico : return 0 # cas imprévu, on peut résumer ces deux lignes else : return dico [s] # par return (s, 0) | 3)La réponse à cette question est divisée en deux fonctions. La première traduit une liste de mots en un nombre numérique. Ce nombre est la somme des valeurs retournées par la fonction valeur_mot pour chacun des mots : soixante dix neuf = 60 + 10 + 9. Cette règle s’applique la plupart du temps sauf pour quatre vingt. def lire_dizaine_liste(s): """convertit une liste de chaînes de caractères dont juxtaposition forme un nombre littéral compris entre 0 et 99""" r = 0 # contient le résultat final dizaine = False # a-t-on terminé le traitement des dizaines ? for mot in s: n = lire_unite (mot) if n == 20 : if not dizaine and r > 0 and r != 60 : r *= n # cas 80 dizaine = True else : r += n else : r += n return r | La seconde fonction remplace tous les traits d’union par des espaces puis divise un nombre littéral en liste de mots séparés par des espaces. 4)Tout d’abord, les deux fonctions réciproques de celles présentées ci-dessus. def ecrit_unite (x): """convertit un nombre compris inclus entre 0 et 16 inclus, 20, 30, 40, 50, 60 en une chaîne de caractères""" if x == 0: return "zéro" elif x == 1: return "un" elif x == 2: return "deux" elif x == 3: return "trois" elif x == 4: return "quatre" elif x == 5: return "cinq" elif x == 6: return "six" elif x == 7: return "sept" elif x == 8: return "huit" elif x == 9: return "neuf" elif x == 10: return "dix" elif x == 11: return "onze" | elif x == 12: return "douze" elif x == 13: return "treize" elif x == 14: return "quatorze" elif x == 15: return "quinze" elif x == 16: return "seize" elif x == 20: return "vingt" elif x == 30: return "trente" elif x == 40: return "quarante" elif x == 50: return "cinquante" elif x == 60: return "soixante" elif x == 70: return "soixante-dix" elif x == 80: return "quatre-vingt" elif x == 90: return "quatre-vingt-dix" else : return "zéro" Cette fonction pourrait être simplifiée en utilisant le même dictionnaire que pour la fonction valeur_mot mais inversé : la valeur devient la clé et réciproquement. def mot_valeur (x): """convertit un nombre compris inclus entre 0 et 16 inclus, 20, 30, 40, 50, 60 en une chaîne de caractères""" dico = {’cinquante’: 50, ’quarante’: 40, ’onze’: 11, ’huit’: 8, ’six’: 6, \ ’quinze’: 15, ’trente’: 30, ’douze’: 12, ’cinq’: 5, ’deux’: 2, \ ’quatorze’: 14, ’neuf’: 9, ’soixante’: 60, ’quatre’: 4, \ ’zéro’: 0, ’treize’: 13, ’trois’: 3, ’seize’: 16, \ ’vingt’: 20, ’un’: 1, ’dix’: 10, ’sept’: 7} inv = {} for k,v in dico.iteritems () : inv [v] = k inv [70] = "soixante-dix" inv [80] = "quatre-vingt" inv [90] = "quatre-vingt-dix" return inv [x] def ecrit_dizaine(x): """convertit un nombre entre 0 et 99 sous sa forme littérale""" if x <= 16 : return ecrit_unite(x) s = "" dizaine = x / 10 unite = x % 10 s = mot_valeur (dizaine*10) s += " " s += mot_valeur (unite) return s | Le morceau de code ci-dessous permet de vérifier que le passage d’un nombre sous sa forme littérale et réciproquement fonctionne correctement pour tous les nombres compris entre 0 et 99 inclus. for i in xrange(0,100): s = ecrit_dizaine (i) | j = lire_dizaine (s) if i != j : print "erreur ", i, " != ", j, " : ", s 10.2 Représentation des données, partie de dames Une partie de dames met en jeu quarante pions, vingt noirs, vingt blancs, chacun sur des cases différentes. L’objectif est de savoir si un pion est en mesure d’en prendre un autre. On ne traitera pas le cas des dames. Chaque pion est défini par : – deux coordonnées entières, chacune comprise entre 1 et 10 – une couleur, noir ou blanc 1) On propose deux représentations de l’ensemble de pions : 1. Un tableau de 40 pions indicés de 0 à 39 inclus, chaque pion étant défini par : – deux coordonnées comprises entre 1 et 10, ou (0,0) si le pion n’est plus sur le damier – un entier qui vaut 1 pour blanc, 2 pour noir 2. Un tableau d’entiers à deux dimensions, chaque case contient : – soit 0 s’il n’y a pas de pion – soit 1 si la case contient un pion blanc – soit 2 si la case contient un pion noir Y a-t-il d’autres représentations de ces informations? Si on considère que l’efficacité d’une méthode est reliée à sa vitesse - autrement dit aux coûts des algorithmes qu’elles utilisent -, parmi ces deux représentations, quelle est celle qui semble la plus efficace pour savoir si un pion donné du damier est en mesure d’en prendre un autre? 3)On cherche à écrire l’algorithme qui permet de savoir si un pion donné est un mesure de prendre un pion. Quels sont les paramètres d’entrées et les résultats de cet algorithme? 4)Il ne reste plus qu’à écrire cet algorithme. Correction 1)La seconde représentation sous forme de tableau à deux dimensions est plus pratique pour effectuer les tests de voisinages. Chaque case a quatre voisines aux quatre coins, il est ensuite facile de déterminer si ces quatre voisines sont libres ou si elles contiennent un pion. On sait rapidement le contenu d’une case. Avec la première représentation - le tableau des pions - pour savoir s’il existe un pion dans une case voisine, il faut passer en revue tous les pions pour savoir si l’un d’eux occupe ou non cette case. Avec la seconde représentation - le tableau à deux dimensions - on accède directement à cette information sans avoir à la rechercher. On évite une boucle sur les pions avec la seconde représentation. 2)Pour représenter le tableau en deux dimensions, il existe trois solutions : 1. Une liste de listes, chaque ligne est représentée par une liste. Toutes ces listes sont elles-mêmes assemblées dans une liste globale. 2. Une seule liste, il suffit de numéroter les cases du damier de 0 à 99, en utilisant comme indice pour la case (i,j) : k = 10?i+j. Réciproquement, la case d’indice k aura pour coordonnées (k/10, k%10). 3. Un dictionnaire dont la clé est un couple d’entiers. def pion_prendre(i,j,damier): c = damier [i][j] if c == 0: return False # case vide, impossible de prendre c = 3 - c # couleur de l’adversaire if damier [i-1][j-1] == c : # s’il y a un pion adverse en haut à gauche if damier [i-2][j-2] == 0 : # si la case d’après en diagonale est vide return True # on peut prendre # on répète ce test pour les trois autres cases if damier [i-1][j+1] == c and damier [i-2][j+2] == 0: return True if damier [i+1][j-1] == c and damier [i+2][j-2] == 0: return True if damier [i+1][j+1] == c and damier [i+2][j+2] == 0: return True # si tous les tests ont échoué, on ne peut pas prendre return False | Voici une fonction équivalente lorsque le damier est un dictionnaire dont la clé est un couple d’entiers. def pion_prendre(i,j,damier): c = damier [(i,j)] # ou encore damier [i,j] if c == 0: return False # case vide, impossible de prendre c = 3 - c # couleur de l’adversaire # test pour une prise du pion dans les quatre cases voisines if damier [i-1,j-1] == c and damier [i-2,j-2] == 0: return True if damier [i-1,j+1] == c and damier [i-2,j+2] == 0: return True if damier [i+1,j-1] == c and damier [i+2,j-2] == 0: return True if damier [i+1,j+1] == c and damier [i+2,j+2] == 0: return True # si tous les tests ont échoué, on ne peut pas prendre return False | La même fonction lorsque le damier est représenté par une seule liste. def pion_prendre(i,j,damier): c = damier [10*i+j] if c == 0: return False # case vide, impossible de prendre c = 3 - c # couleur de l’adversaire # test pour une prise du pion dans les quatre cases voisines return False Pour ces trois cas, aucun effet de bord n’a été envisagé. Si la case est trop près d’un des bords, un des indices i, j, i ? 1, j ? 1, i + 1, j + 1, i ? 2, j ? 2, i + 2, j + 2 désignera une case hors du damier. Le code de la fonction pion_prendre devra donc vérifier que chaque case dont elle vérifie le contenu appartient au damier. def pion_prendre(i,j,damier): c = damier [i][j] if c == 0: return False # case vide, impossible de prendre c = 3 - c # couleur de l’adversaire # on répète ce test pour les trois autres cases if i >= 2 and j >= 2 and \ damier [i-1][j-1] == c and damier [i-2][j-2] == 0: return True if i >= 2 and j < len (damier)-2 and \ damier [i-1][j+1] == c and damier [i-2][j+2] == 0: return True if i < len (damier)-2 and j >= 2 and \ damier [i+1][j-1] == c and damier [i+2][j-2] == 0: return True if i < len (damier)-2 and j < len (damier)-2 and \ damier [i+1][j+1] == c and damier [i+2][j+2] == 0: return True return False Avec cette convention, les tests introduits dans le dernier programme ne seront plus nécessaires. Il faudra juste réécrire la seconde ligne de chaque fonction pion_prendre par : if c <= 0 : return False # au lieu de if c == 0 : return False Remarque 10.1 : matrice et dictionnaire Les structures représentées ici sont des tableaux à deux dimensions : ce sont des matrices. Les dictionnaires sont particulièrement indiqués dans le cas où ces matrices sont creuses : elles contiennent beaucoup de valeurs nulles. Pour une liste ou une liste de listes, toutes les cases sont représentées. Dans le cas d’un dictionnaire, il est possible d’adopter comme convention que si un couple de coordonnées n’existe pas en tant que clé, cela veut dire que la case associée est nulle. Il en résulte un gain de place équivalent à la proportion de cases nulles. Remarque 10.2 : dictionnaire et clé de type tuple Le second exemple utilise un dictionnaire avec comme clé un tuple. Dans ce cas, les parenthèses sont superflues. damier [(i-1,j-1)] | # est équivalent à | damier [ i-1,j-1 ] | # cette ligne | 10.3 Reconnaître la langue d’un texte L’objectif est de distinguer un texte anglais d’un texte français sans avoir à le lire. Le premier réflexe consisterait à chercher la présence de mots typiquement anglais ou français. Cette direction est sans doute un bon choix lorsque le texte considéré est une œuvre littéraire. Mais sur Internet, les contenus mélangent fréquemment les deux langues : la présence de tel mot anglais n’est plus aussi discriminante. Il n’est plus aussi évident d’étiqueter un document de langue anglaise lorsque les mots anglais sont présents partout. Pour ces trois raisons, il paraît préférable d’étudier une direction plus simple quitte à revenir aux dictionnaires plus tard. Dans un premier temps, on cherchera donc à distinguer un texte anglais d’un texte français à partir de la fréquence de certaines lettres dont l’usage est différent dans les deux langues comme la lettre w. La première étape consiste à calculer la fréquence des lettres dans différents textes anglais et français qu’on peut se procurer depuis le site Gutenberg par exemple. Il suffit alors de télécharger quelques textes et de les enregistrer dans des fichiers texte. 1)Il faut tout d’abord récupérer dans un programme Python le contenu de ces textes dont on suppose qu’ils ont été téléchargés. On pourra s’inspirer des exemples du chapitre 7. 2)On veut construire une fonction qui compte l’occurrence de chaque lettre dans une chaîne de caractères. Cette fonction prend en entrée une chaîne de caractères et retourne un dictionnaire. Chaque élément contiendra le nombre de fois qu’un caractère apparaît. 3)Afin d’obtenir des probabilités, on divise chaque nombre par le nombre total de lettres (attention aux divisions entières). 4)Ecrire une fonction qui retourne la probabilité de chaque lettre dans un fichier texte. 5)En observant les résultats obtenus sur un texte français et un texte anglais, pensez-vous qu’il soit possible d’écrire une fonction qui détermine automatiquement la langue d’un texte à condition que celle-ci soit l’anglais ou le français. Correction 1)La fonction répondant à la première question revient régulièrement dans beaucoup de programmes. def lit_fichier (nom) : f = open (nom, "r") | # ouverture du fichier | l = f.read () | # on récupère le contenu | f.close () | # on ferme le fichier | return l | # on retourne le contenu import urllib | # import du module urllib | def lit_url (nom) : f = urllib.urlopen (nom) | # on ouvre l’url | res = f.read () | # on lit son contenu | f.close () | # on termine la lecture | return res | # on retourne le résultat | On peut regrouper ces deux fonctions et appeler soit l’une soit l’autre selon que le nom du texte est un fichier texte ou une adresse Internet : def lit (texte) : if texte.startswith ("http") : s = lit_url (texte) # Internet else : s = lit_fichier (texte) # fichier texte return s s = lit ("") | Une autre option consiste à utiliser les exceptions : on essaye d’abord d’ouvrir un fichier avec la fonction open. Si cela ne fonctionne pas, on peut supposer que le nom du fichier fait référence à une adresse Internet. def lit (texte) : try : s = lit_fichier (texte) | # fichier texte | return s except : | # si cela ne marche pas, | s = lit_url (texte) | # on suppose que texte | return s s = lit ("") | # est une adresse Internet | 2)La fonction suivante compte les occurrences de chaque lettre dans la chaîne de caractères texte. Une première solution consiste à appeler 26 fois la méthode count des chaînes de caractères. def compte_lettre_count (texte) : texte = texte.upper () # pour éviter les accents res = { } # résultat, vide pour le moment for c in "ABCDEFGHIJKLMNOPQRSTUVWXYZ" : # pour tout l’alphabet res [c] = texte.count (c) # on compte les occurrences de c return res def compte_lettre (texte) : texte = texte.upper () # pour éviter les accents res = { } # résultat, vide pour le moment for c in texte : # pour tous les caractères du texte if not("A" <= c <= "Z") : continue # si ce n’est pas une lettre, on passe if c not in res : res [c] = 1 # si elle n’est pas là, on lui affecte 1 else : res [c] += 1 # sinon, on augmente son nombre d’apparitions return res | Il n’est pas évident de choisir l’une ou l’autre des méthodes. Calculer le coût algorithmique de chacune des fonctions n’est pas toujours évident car la première utilise la fonction count dont le fonctionnement est inconnu. Il paraît plus facile dans ce cas de comparer la vitesse d’exécution de chaque fonction. Dans l’exemple suivant, la fonction comparaison appelle les deux méthodes sur le même texte. Le module profile va ensuite mesurer le temps passé dans chacune des fonctions. def comparaison () : s = lit_fichier ("") compte_lettre_count (s) # on ne mémorise pas les résultats compte_lettre (s) # car on souhaite mesurer le temps passé import profile # import du module profile ("comparaison()") # mesure du temps passé dans la fonction # comparaison et les fonctions qu’elle appelle ncalls tottime percall cumtime percall filename:lineno(function) 1 0.160 0.160 0.174 0.174 :19(compte_lettre) 1 0.000 0.000 0.017 0.017 :31(compte_lettre_count) 1 0.000 0.000 0.190 0.190 :43(comparaison) | ncalls | Nombre d’appels à la fonction | tottime | Temps passé uniquement dans la fonction, le temps passé dans une fonction appelée par celle-ci n’est pas compté. | percall | Temps moyen passé uniquement dans la fonction. | cumtime | Temps cumulé passé dans la fonction, y compris celui passé dans une fonction appelée. | percall | Temps cumulé moyen. | filename | nom du fichier et nom de la fonction entre parenthèse | La première solution est clairement la plus rapide. Ce résultat s’explique par la rapidité de la méthode count. Mais cela ne permet de pas de conclure dans tous les cas. Il faudrait connaître le coût algorithmique de chaque fonction pour cela. Par exemple, lorsqu’il s’agit de calculer les fréquences de tous les caractères et non plus des seules 26 lettres, on s’aperçoit que la seconde solution n’est plus que 1,5 fois plus lente et non 10 fois. ncalls tottime percall cumtime percall filename:lineno(function) 1 0.120 0.120 0.133 0.133 :19(compte_lettre) 1 0.002 0.002 0.082 0.082 :32(compte_lettre_count) Pour obtenir un temps d’exécution fiable, il faut appeler la fonction un grand nombre de fois. Le tableau suivant montre le temps moyen total passé dans chaque fonction après 100 appels à la fonction comparaison. fonction | 26 lettres | tous les caractères | compte_lettre | 0.087 | 0.061 | compte_lettre_count | 0.017 | 0.073 | La fonction compte_lettre est effectivement plus rapide dans le second cas. Comme on s’intéresse maintenant à tous les caractères, le test ifnot (”A” <= c <= ”Z”) n’est plus nécessaire : la fonction est d’autant plus rapide que cette instruction était exécutée un grand nombre de fois. On remarque également que la fonction compte_lettre ne paraît pas dépendre du nombre de caractères considérés. La fonction compte_lettre est un meilleur choix. def compte_lettre (texte) : # s = sum (res.values ()) for k in res : res [k] = float (res [k]) / s return res | 3)Il suffit de diviser chaque valeur du dictionnaire res par la somme de ses valeurs. On insère donc les trois lignes suivantes juste avant l’instruction return de la fonction précédente. 4)Le tableau suivant montre les probabilités obtenues pour les quatre lettres H,U,W,Y pour lesquelles les résultats montrent des différences significatives. Les deux textes sélectionnés sont Le dernier jour d’un condamné de Victor Hugo et sa traduction anglaise The Man Who Laughs. lettre | Le dernier jour d’un condamné | The Man Who Laughs | H | 1,22 % | 6,90 % | U | 6,79 % | 2,57 % | W | 0,11 % | 2,46 % | Y | 0,42 % | 1,34 % | Ces chiffres ont été obtenus avec le programme suivant qui télécharge ces deux textes directement depuis le site Gutenberg2. Les fréquences de ces quatre lettres montrent des différences significatives, ce que montre de façon plus visuelle la figure 10.1 pour les lettres W et H. # le dernier jour d’un condamné c1 = compte_lettre (lit_url ("")) # the man who laughs c2 = compte_lettre (lit_url ("")) car = () () for k in car : print k, " : ", "% 2.2f" % (c1 [k] * 100), "%", " % 2.2f" % (c2 [k] * 100), "%" | 5)La fréquence de la lettre W ou H devrait suffire à départager un texte français d’un texte anglais. Pour vérifier cette hypothèse, on décrit chaque texte par deux coordonnées correspondant aux fréquences des lettres H et W, ce que calcule la fonction suivante : def langue_lettre (texte) : if "http" in texte : s = lit_url (texte) | # cas URL | else : s = lit_fichier (texte) | # cas fichier | c = compte_lettre (s) | # on compte les lettres | return c ["W"], c ["H"] | # on retourne deux fréquences | La dernière instruction de cette fonction suppose que tous les textes incluent au moins un W et un H. Dans le cas contraire, la fonction produirait une erreur car elle ne pourrait pas trouver la clé ”W” ou ”H” dans le dictionnaire c. Pour remédier à cela, on utilise la méthode get des dictionnaires qui permet de retourner une valeur lorsqu’une clé est introuvable. def langue_lettre (texte) : # lignes inchangées return c.get ("W", 0.0), c.get ("H", 0.0) # on retourne deux fréquences | Après cette correction, la fonction retourne une valeur nulle lorsque les lettres W ou H n’apparaissent pas. La fonction suivante permet de construire deux listes cx et cy qui contiennent les fréquences des lettres W et H pour une liste de textes. def curve (li) : cx,cy = [], [] for l in li : | # pour tous les textes de la liste | x,y = langue_lettre (l) | # coordonnées d’un texte, fréquence W et H | cx.append (x) | # on ajoute x à la liste des abscisses | # on ajoute y à la liste des ordonnées | On utilise cette dernière fonction pour faire apparaître sur un graphique plusieurs textes anglais et français. Chaque texte est représenté par deux coordonnées : la fréquence des lettres W et H. On obtient un nuage de points dans un plan ce que montre le graphe de la figure 10.1. Ce dernier a été tracé à l’aide du module matplotlib . Il s’inspire du logiciel Matlab dans la manière de constuire des graphes . frcx,frcy = curve (fr) # on récupère les coordonnées des textes français encx,ency = curve (en) # on récupère les coordonnées des textes anglais import pylab # import du module matplotlib (frcx, frcy, "rx",ms=10,\ mew=2.5) # on trace la courbe des textes français (encx, ency, "bv") # on trace la courbe des textes anglais pylab.legend (("francais", "anglais"), loc=2) # légende (sans accent) pylab.title ("langue") # titre pylab.xlabel ("frequence de W") # légende de l’axe des abscisses pylab.ylabel ("frequence de H") # légende de l’axe des ordonnées pylab.savefig ("") # enregistrement sous forme d’image () # on fait apparaître le graphique | Figure 10.1 : Les textes anglais et français sont cantonnés chacun dans une zone précise du graphe : il apparaît que les fréquences des lettres H et W permettent toutes deux de distinguer des textes anglais et français. Une fréquence inférieure à 1% pour la lettre W signifie un texte français. La figure 10.1 montre qu’au delà d’une fréquence de 1% pour la lettre W, le texte est anglais. On en déduit la fonction qui permet de déterminer la langue d’un texte. w,h = langue_lettre (texte) return w > 0.01 | La réponse à la dernière question pourrait s’arrêter ici. Toutefois, les paragraphes qui suivent s’intéressent à la fiabilité de la fonction de la fonction est_anglais dans un autre contexte : Internet. Interprétation En effet, ce dernier résultat peut être critiqué : ce seuil s’appuie sur seulement quatre textes. Ces derniers sont longs : la fréquence des lettres en est d’autant plus fiable. Sur un texte d’une centaine de lettres, la fréquence de la lettre W est soit nulle, soit supérieure à 1%. Au delà de ce cas difficile et peu fréquent, on vérifie la fiabilité de la fonction est_anglais sur des articles de journaux français et anglais. Internet va nous aider à construire une base de textes : il est facile des récupérer automatiquement de nombreux articles de journaux sur des sites tels que celui du thématiques sur le graphique. L’utilisation de ce genre de modules est plutôt facile, de nombreux exemples de graphiques sont proposés avec le programme Python qui a permis de les réaliser. Il suffit souvent des recopier et de les adapter. Une autre solution consiste à écrire les courbes dans un fichier texte puis à les récupérer avec un tableur tel que OpenOffice pour les représenter sous forme de graphe (voir l’exemple page 170). journal Le Monde ou le New-York Times. On pourra ensuite calculer la fréquence des lettres W et H pour chacun d’entre eux. Récupération de pages HTML La principale différence provient du fait que les liens ne désignent pas toujours une page nouvelle mais parfois la première page du site ou une autre page déjà vue dont il ne faut pas tenir compte. L’autre différence vient du faite que le nombre de pages sur un site comme Le Monde peut s’avérer très grand : il faut bien souvent se limiter aux premières pages et à celles qui appartiennent à ce site. Figure 10.2 : Un site Internet contient généralement une page qui est le moyen d’accéder à toutes les pages du site. C’est un point d’entrée qui mène directement ou indirectement à l’ensemble des pages du site. Il suffit de suivre les liens pour en établir une liste. Certains liens mènent à des pages nouvelles, d’autres à des pages déjà vue : ce graphe est cyclique. Le format HTML fonctionne par balises : une information est encadrée par deux balises : <balise> texte </balise>. Les hyperliens utilisent une syntaxe à peine plus compliquée puisqu’une balise peut aussi recevoir quelques paramètres : <balise param1="valeur1" param2="valeur2"> texte </balise>. Selon cette syntaxe, un lien vers une autre page est indiqué comme suit : <a href="http:// ."> texte qui apparaît à l’écran </a> Les expressions régulières permettent de retrouver aisément tous les liens insérés dans une page sans avoir à se soucier du format HTML. L’algorithme de recherche consiste à recenser tous les liens de la première page. La syntaxe HTML nous apprend que ces liens sont compris entre guillemets. On considère ensuite chacune des pages recensées et on procède de même. On s’arrête après avoir obtenu le nombre de pages souhaité. import urllib | # pour accéder une adresse internet | import re | # pour traiter les expressions régulières | def list_url (site, root, nbpage = 100) : # expression régulières # tous les liens commençant par root et entre guillemets res = [ ] # résultat pile = [ site ] # page à explorer while len (pile) > 0 and len (res) < nbpage : # on bascule toutes les pages de pile vers res for u in pile : if u not in res : res.append (u) u = () # on s’intéresse à la prochaine page try : f = urllib.urlopen (u) # accès à l’url text = f.read () # on lit son contenu f.close () # fin de l’accès liens = exp.findall (text) # recherche de tous les liens for u in liens : if u in pile or u in res : # on passe au suivant si continue # déjà vu # on enlève les images et autres fichiers indésirables if ".gif" in u or ".png" in u or ".jpg" in u : continue if ".cs" in u or ".css" in u or ".js" in u : continue # on ajoute le liens à la liste des liens à explorer pile.append (u) except IOError, exc: print "problème avec url ", u continue return res | Il ne reste plus qu’à appeler cette fonction pour récupérer la liste des URLs : url = "; # un journal français res = list_url (url, url) for r in res : print r url = "; # un journal américain res = list_url (url, url) for r in res : print r Résultats Figure 10.3 : La frontière entre texte anglais et français paraît plus floue et ne dépend plus de la fréquence d’une seule lettre. L’axe des X représente la fréquence de la lettre W, l’axe des Y celle de la lettre H. Pour aller plus loin Jusqu’à présent, seules les lettres W et H ont été utilisées. Les deux autres lettres intéressantes U et Y ont été laissées de côté. On peut se demander quelle est la meilleure façon de procéder avec quatre lettres. Chaque texte est maintenant représenté par quatre fréquences, soit quatre coordonnées et il n’est possible de n’en représenter que deux. Dans ce nuage de points à quatre dimension, l’analyse en composantes principales (ACP) est une méthode qui permet de calculer un plan de projection, plus exactement le meilleur plan de projection : celui qui minimise les distances de chaque point au plan. On utilise pour cela le module mdp qui implémente cet algorithme. On suppose dans l’exemple suivant que fr4 et en4 sont deux matrices de quatre colonnes représentant les coordonnées de chaque document. La figure 10.4 montre que l’utilisation des quatre lettres H, U, W, Y permet de construire une frontière plus efficiente qu’avec l’utilisation de W, H seulement. import mdp,copy,numpy nbfr,nben = len (fr), len (en) all = numpy.array (fr + en) # construction du nuage de points node = mdp.nodes.PCANode() # ACP node.train (all) # construction de l’ACP y = node (all) # on peut aussi écrire y = (all) # obtention des coordonnées des points dans le plan de projection frcx = [ y [i,0] for i in range (0, nbfr) ] frcy = [ y [i,1] for i in range (0, nbfr) ] | encx = [ y [i,0] for i in range (nbfr, nbfr + nben) ] ency = [ y [i,1] for i in range (nbfr, nbfr + nben) ] # dessin des points dans le plan de projection # c’est le même code que précédemment # L’ACP donne de bons résultats dans la mesure où les deux classes sont visiblement disctinctes et concentrées. Dans un cas comme celui-ci pourtant, il serait plus adéquat d’utiliser une Analyse Discriminante Linéaire ou Fisher Discriminant Analysis (FDA) en anglais. Il s’agit de déterminer la meilleure séparation linéaire entre ces deux classes. Les résultats sont dans ce cas assez proches. # même début que le programme précédent allfr = numpy.array (fr) # points français allen = numpy.array (en) # points anglais node = mdp.nodes.FDANode () node.train (allfr, "fr") node.train (allen, "en") node.stop_training () node.train (allfr, "fr") node.train (allen, "en") y = node (all) | 10.4 Carrés magiques Un carré magique est un carré 3x3 dont chaque case contient un nombre entier et pour lequel les sommes sur chacune des lignes, chacune des colonnes, chacune des diagonales sont égales, soit huit sommes égales. Le plus simple des carrés magiques est celui dont tous les chiffres sont égaux : class CarreMagique : def __init__ (self, nb) : = nb m = [ [9, 3, 3], [ 4,5,6] , [3,8,2] ] | 1)Un carré magique n’est a priori qu’une matrice de trois lignes et trois colonnes. Dans un premier temps, on va donc construire une classe CarreMagique qui contiendra une liste de trois listes, chacune de trois colonnes. Compléter le programme pour créer un carré magique cm. def __str__ (self) : s = "" s += str ( [0][0]) return s | 2)On désire afficher le carré magique. Que donne l’instruction printcm? On ajoute la méthode __str__. Que produit l’instruction printcm maintenant? Que faudrait-il changer pour que le programme affiche le carré en entier? 3)Ecrire une méthode qui calcule la somme des chiffres sur la ligne i. 5)Ecrire une méthode qui calcule la somme des chiffres sur la colonne j. 6)Sans tenir compte des diagonales, écrire une méthode qui détermine si un carré est magique. 7)Il ne reste plus qu’à inclure les diagonales. 8)Ecrire une fonction qui détermine si tous les nombres du carré magique sont différents. 9)Terminer le programme pour déterminer tous les carrés magiques 3x3 dont les nombres sont tous différents. Remarque 10.3 : différence entre print et return A la fin d’un calcul, afin de voir son résultat, on utilise souvent l’instruction print. On peut se demander alors si à la fin de chaque fonction, il ne faudrait pas utiliser l’instruction print. A quoi servirait alors l’instruction return? On suppose qu’un calcul est en fait le résultat de trois calculs à la suite : a = calcul1 (3) b = calcul2 (a) c = calcul3 (b) # c résultat souhaité et affiché Chaque terme calculx cache une fonction or seul le résultat de la dernière nous intéresse et doit être affiché. Pour les deux premières, la seule chose importante est que leur résultat soit transmis à la fonction suivante et ceci ne peut se faire que grâce à l’instruction return. L’instruction print insérée dans le code de la fonction calcul1 ou calcul2 permettra d’afficher le résultat mais ne le transmettra pas et il sera perdu. L’instruction return est donc indispensable, print facultative. En revanche, dans la dernière fonction calcul3, il est possible de se passer de return et de se contenter uniquement d’un print. Cependant, il est conseillé d’utiliser quand même return au cas où le résultat de la fonction calcul3 serait utilisé par une autre fonction. Correction 1)Il suffit de créer instance de type CarreMagique. 2)Le message affiché par l’instruction printcm avant d’ajouter la méthode __str__ correspond au suivant : <__main__.CarreMagique instance at 0x01A254E0> # coding: latin-1 class CarreMagique : def __init__ (self, nb) : """on place la matrice nb (liste de listes) dans la classe accessible par le mot-clé self""" = nb # réponse à la question 2 def __str__ (self) : """méthode appelée lorsque on cherche à afficher un carré magique avec l’instruction print""" s = "" for i in range (0, len ()) : for j in range (0, len ()) : s += str ( [i][j]) + " " s += "\n" # pour passer à la ligne # réponse à la question 4 s += "somme " + str (self.somme_ligne (0)) return s # réponse à la question 3 def somme_ligne (self, i) : s = 0 for j in range (0, len ()) : s += [i][j] return s # réponse à la question 5 def somme_colonne (self, j) : s = 0 for i in range (0, len ()) : s += [i][j] return s # réponse à la question 6 def est_magique (self) : # on stocke toutes les sommes l = [] for i in range (0, len ()) : l.append ( self.somme_ligne (i) ) for j in range (0, len ()) : l.append ( self.somme_colonne (j) ) # réponse à la question 7 l.append ( self.somme_diagonale (0)) | l.append ( self.somme_diagonale (1)) # on trie la liste l.sort () # on compare le plus petit et le plus grand, s’il sont égaux, # le carré est magique return l [0] == l [ len(l)-1 ] # réponse à la question 7 def somme_diagonale (self, d) : """d vaut 0 ou 1, première ou seconde diagonale""" s = 0 if d == 0 : for i in range (0, len ()) : s += [i][i] else : for i in range (0, len ()) : s += [i][len()-i-1] return s # réponse à la question 8 def nombre_differents (self) : """retourne True si tous les nombres sont différents, on place les nombres un par un dans un dictionnaire, dès que l’un d’eux s’y trouve déjà, on sait que deux nombres sont identiques, le résultat est False""" k = { } for i in range (0, len ()) : for j in range (0, len ()) : c = [i][j] if c in k : return False # pas besoin d’aller plus loin # il y a deux nombres identiques else : k [c] = 0 return True print cm # affiche 15 print cm.est_magique () # affiche False print cm.nombre_differents () # affiche True m = [ [9, 9, 9], [9, 9, 9], [9, 9, 9] ] cm = CarreMagique (m) print cm # affiche 15 print cm.est_magique () # affiche True print cm.nombre_differents () # affiche False | La dernière question consiste à trouver tous les carrés magiques dont les nombres sont tous différents. Il faut donc passer en revue tous les carrés 3x3 et ne conserver que ceux pour lesquels les méthodes est_magique et nombre_differents retournent un résultat positif. On suppose que le programme précédent contenant la classe CarreMagique porte le nom de carre_magique.py. Ceci explique la ligne from . from carre_magique import CarreMagique res = [] for a in range (1,10) : for b in range (1,10) : for c in range (1,10) : for d in range (1,10) : for e in range (1,10) : for f in range (1,10) : for g in range (1,10) : for h in range (1,10) : for i in range (1,10) : l = [ [a,b,c], [d,e,f], [g,h,i] ] cm = CarreMagique (l) if cm.nombre_differents () and \ cm.est_magique () : res.append (cm) print len (res) for r in res : print r | L’inconvénient de cette solution est qu’elle est inadaptée pour des carrés d’une autre dimension que 3x3. Il faudrait pouvoir inclure un nombre de boucles variable ce qui est impossible. L’autre solution est de programmer un compteur en reproduisant l’algorithme d’une addition. C’est l’objectif du programme suivant. # coding: latin-1 from carre_magique import CarreMagique dim = 3 nb = dim*dim res = [] # contiendra la liste des carrés magiques # transformation d’une liste en une liste de listes # [1,2,3,4,5,6,7,8,9] --> [[1,2,3],[4,5,6],[7,8,9]] l = [] for i in range (0, dim) : l.append ( ind [i*dim:(i+1)*dim] ) # on vérifie que le carré est magique et # a des nombres tous différents cm = CarreMagique (l) if cm.nombre_differents () and cm.est_magique () : res.append (cm) # on passe au carré suivant : i = nb-1 # dernier indice (9 ici) ind [i] += 1 # addition de 1 au dernier indice while i > 0 and ind [i] > nb : ind [i-1] += 1 # un des indices est supérieur à nb (9 ici) ind [i] = 1 # on le remet à 1 i -= 1 # et on propage l’information à l’indice inférieur # résultat final print len (res) for r in res : print r | Il existe une autre manière de parcourir les carrés magiques en décomposant un entier compris entre 1 et 99 en une succession de neuf entiers. On utilise pour cela la décomposition d’un nombre en base neuf : n ? 1 = P9k=1?k(ik ? 1) où tous les nombres ik sont dans l’ensemble {1, ,9}. # coding: latin-1 from carre_magique import CarreMagique dim = 3 nb = dim*dim M = 9 ** nb # on va tester 9^9 carrés possibles res = [] # contiendra la liste des carrés magiques for n in xrange (0,M) : # on décompose n en liste ind = [] k = n for t in range (0,nb) : dec = k % nb k = k / nb ind.append (dec+1) # transformation d’une liste en une liste de listes # [1,2,3,4,5,6,7,8,9] --> [[1,2,3],[4,5,6],[7,8,9]] l = [] for i in range (0, dim) : l.append ( ind [i*dim:(i+1)*dim] ) # on vérifie que le carré est magique et # a des nombres tous différents cm = CarreMagique (l) if cm.nombre_differents () and cm.est_magique () : res.append (cm) # résultat final print len (res) for r in res : print r Traceback (most recent call last): File "", line 9, in <module> for n in range (0,M) : MemoryError | Ces trois solutions sont très lentes car elles explorent 99 solutions. Comme on ne cherche que les carrés magiques dont les éléments sont tous différents, on pourrait n’explorer que les permutations soit 9! configurations. On crée une fonction permutation qui parcourt toutes les permutations possibles par récurrence (voir figure 10.5). # coding: latin-1 from carre_magique import CarreMagique Figure 10.5 : C’est une illustration de l’idée permettant de parcourir les permutations. On suppose que la partie du côté gauche du trait en pointillé (relié à la variable pos, ici égale à 4) est figée. On l’échange tour à tour avec les chiffre à droite (pos + 1,pos + 2, ). A chaque échange, on appelle à nouveau la fonction permutation récursive en décalant le trait en pointillé d’un cran vers la droite. Après cet appel, on effectue l’échange inverse. def echange (p, i, pos) : # échange de deux éléments d’indice i et pos du tableau p if i != pos : e = p [i] p [i] = p [pos] p [pos] = e def permutation (res, dim, pos = 0, p = None) : # parcours des permutations par récurrence # pour le premier appel à la fonction permutation if p == None : p = range (1, dim*dim+1) if pos < len (p) : # on organise les permutations de l’élément p [pos], p [pos+1], for i in range (pos, len (p)) : echange (p, i, pos) permutation (res, dim, pos+1, p) echange (p, i, pos) else : # pos correspond au dernier élément : il n’y a plus de permutation # possible avec les éléments qui suivent # on teste donc le nouveau carré l = [] for i in range (0, len (p)/dim) : l.append ( p [i*dim:(i+1)*dim] ) cm = CarreMagique (l) if cm.est_magique () : res.append (cm) # permutations res = [] permutation ( res, 3 ) | Le programme indique huit carrés magiques qui sont en fait huit fois le même carré obtenus par rotation ou symétrie. La somme des chiffres est 15. 10.5 Tri rapide ou quicksort Cet énoncé a pour objectif de présenter l’algorithme de tri quicksort qui permet de trier par ordre croissant un ensemble d’éléments (ici des chaînes de caractères) avec un coût moyen en O (nlnn) où n est le nombre d’éléments à classer. Le tri quicksort apparaît rarement sous la forme d’un graphe : il est plus simple à programmer sans les graphes mais il est plus simple à appréhender avec les graphes. Dans cette dernière version, l’algorithme insère un à un les éléments d’une liste à trier dans un graphe comme celui de la figure 10.6 (page 265). Chaque nœud de ce graphe est relié à deux autres nœuds : 1. Un nœud avant ou ” < ” qui permet d’accéder à des éléments classés avant celui de ce nœud. 2. Un nœud apres ou ” > ” qui permet d’accéder à des éléments classés après celui de ce nœud. Les nœuds avant et apres sont appelés les successeurs. Le terme opposé est prédécesseur. Ces deux nœuds ont nécessairement un prédécesseur mais un nœud n’a pas forcément de successeurs. S’il en avait toujours un, l’arbre serait infini. 1)On cherche à construire une classe ayant pour nom NoeudTri et qui contient une chaîne de caractères initialisée lors de la création de la classe : n = NoeudTri(”essai”). 2)On écrit la méthode __str__ de sorte que l’instruction printn affiche la chaîne de caractères que contient n. 3)On cherche maintenant à définir d’autres nœuds, reliés à des attributs avant et apres. On suppose que les nœuds utilisent l’attribut mot, on crée alors une méthode insere(s) qui : – Si s < self.mot, alors on ajoute l’attribut avant = NoeudTri(s). Fonction utile : | | cmp(s1,s2) | Compare deux chaînes de caractères, retourne -1,0,1 selon que s1 est classée avant, est égale ou est classée après s2. | 4)La méthode __str__ n’affiche pour le moment qu’un mot. Il s’agit maintenant de prendre en compte les attributs avant et apres afin que l’instruction printn affiche avant.__str__() et apres.__str__(). Il faudra également faire en sorte que la méthode avant.__str__() ne soit appelée que si l’attribut avant existe. Comme la liste des mots à trier est finie, il faut bien que certains nœuds n’aient pas de successeurs. On pourra s’inspirer du programme page 99 (attribut __dict__). Qu’est-ce qu’affiche le programme suivant? racine = NoeudTri ("un") racine.insere ("unite") racine.insere ("deux") print racine 5)Est-il possible de trier plus de trois mots avec ce programme? Que faut-il modifier dans la méthode insere afin de pouvoir trier un nombre quelconque de mots? 6)Ajouter le code nécessaire afin que la méthode insere génère une exception lorsqu’un mot déjà présent dans l’arbre est à nouveau inséré. 7)On se propose de construire une image représentant l’arbre contenant les mots triés par l’algorithme quicksort. Cette représentation utilise le module pydot qui utilise l’outil Graphviz ainsi que le module pyparsing . Leur installation est assez facile sous Windows puisqu’elle consiste seulement à exécuter un programme d’installation. Il faut ensuite installer manuellement le module pydot . Après avoir décompressé les fichiers de ce module, il faut se placer dans le même répertoire et utiliser la ligne de commande suivante : c:\python26\python install | digraph GA { 2 [label="deux",style=filled,shape=record] 3 [label="abc" ,style=filled,shape=record] 2 -> 3 [label="<"] } | | | | Fonction | utile : | id(obj) | Retourne un identifiant entier unique pour chaque variable obj. | | | | | | Une fois que cette chaîne de caractères a été construite, il suffit de l’écrire dans un fichier puis d’appeler le module pydot pour le convertir en image avec le code suivant : g = open ("", "w") # g.write (graph) # partie écriture dans un fichier g.close () # dot = pydot.graph_from_dot_file ("") # partie graphe dot.write_png ("", prog="dot") # avec pydot L’objectif de cette question est de construire une chaîne de caractères pour l’ensemble du graphe. La correction de cette exercice envisage également de construire une page HTML contenant le graphe et la liste triée ainsi que la création automatique d’un fichier au format PDF par le biais de Latex . Correction 1)La chaîne de caractères que contient NoeudTri s’appelle mot. class NoeudTri (object): def __init__(self,s): = s | 2) class NoeudTri (object): def __init__(self,s): = s def __str__(self) : return + "\n" # \n : passage à la ligne | 3) class NoeudTri (object): def __init__(self,s): = s def __str__(self) : return + "\n" def insere (self,s): c = cmp (s, ) if c == -1 : self.avant = NoeudTri (s) # ajout d’un successeur elif c == 1 : self.apres = NoeudTri (s) # ajout d’un successeur | La méthode insere prévoit de ne rien faire dans le cas où le mot s passé en argument est égal à l’attribut mot : cela revient à ignorer les doublons dans la liste de mots à trier. 4) class NoeudTri (object): def __init__(self,s): = s if "avant" in self.__dict__: s += self.avant.__str__ () s += + "\n" if "apres" in self.__dict__: s += self.apres.__str__() return s def insere (self,s): c = cmp (s, ) if c == -1 : self.avant = NoeudTri (s) elif c == 1 : self.apres = NoeudTri (s) | L’insertion des mots donnés dans l’énoncé produit le code suivant : 5)6)Il reste à compléter la fonction insere afin qu’elle puisse trouver le bon nœud où insérer un nouveau mot. Cette méthode est récursive : si un nœud contient deux attributs avant et apres, cela signifie que le nouveau mot doit être inséré plus bas, dans des nœuds reliés soit à avant soit à apres. La méthode insere choisit donc un des attributs et délègue le problème à la méthode insere de ce nœud. # coding: latin-1 import string class SecondeInserstion (AttributeError): "insertion d’un mot déjà inséré" class NoeudTri : def __init__(self,s): = s # la création d’un nouveau noeud a été placée dans une méthode def nouveau_noeud (self, s) : return self.__class__ (s) #return NoeudTri (s) def __str__(self): s = "" if "avant" in self.__dict__: s += self.avant.__str__ () s += + "\n" if "apres" in self.__dict__: s += self.apres.__str__() return s def insere (self,s): c = cmp (s, ) if c == -1: if "avant" in self.__dict__ : self.avant.insere (s) # délégation else : self.avant = self.nouveau_noeud (s) # création elif c == 1: if "apres" in self.__dict__ : self.apres.insere (s) # délégation else: self.apres = self.nouveau_noeud (s) # création else: raise SecondeInsertion, "mot : " + s l = ["un", "deux", "unite", "dizaine", "exception", "dire", \ "programme", "abc", "xyz", "opera", "quel"] racine = None for mot in l : if racine == None : # premier cas : aucun mot --> on crée le premier noeud racine = NoeudTri (mot) else : # second cas : il y a déjà un mot, on ajoute le mot suivant | Chaque nouveau mot va partir du tronc pour s’accrocher à une feuille de l’arbre pour devenir à son tour une feuille. La méthode nouveau_noeud crée un nouveau nœud dans le graphe. Son utilité est mise en évidence par le prochain programme. 7)La figure 10.6 détient le graphe obtenu par le programme qui suit. Plutôt que de modifier la classe NoeudTri, une seconde est créée qui hérite de la première. On lui adjoint la méthode chaine_graphe qui convertit un graphe en une chaîne de caractères dont le format reprend celui énoncé plus haut. Cette fonction s’occupe de construire récursivement cette chaîne de caractères. Pour identifier chaque nœud, on utilise la fonction id qui retourne un identifiant distinct pour chaque instance de classe. Figure 10.6 : Graphe de tri obtenu lors du tri quicksort. Chaque nœud du graphe inclut un mot. Les symboles "<" et ">" des arcs désignent les membres avant et apres de la classe NoeudTri. Tous les mots attachés à un arc "<" d’un nœud sont classés avant le mot de ce nœud. De même, tous les mots attachés à un arc ">" d’un nœud sont classés après le mot de ce nœud. # coding: latin-1 import string import pydot import quicksort class NoeudTri2 (quicksort.NoeudTri): def chaine_graphe (self): # le principe est le même que pour la méthode __str__ # excepté que le format est différent g = str (id (self)) + ’ [label="’ + \ + ’",style=filled,shape=record,fontsize=60]\n’ if "avant" in self.__dict__: h = self.avant.chaine_graphe () g += h + str (id (self)) + " -> " + str (id (self.avant)) \ + ’ [label="<",fontsize=60]’ + ’\n’ if "apres" in self.__dict__: h = self.apres.chaine_graphe () g += h + str (id (self)) + " -> " + str (id (self.apres)) \ + ’ [label=">",fontsize=60]’ + "\n" return g #def nouveau_noeud (self, s) : return NoeudTri2 (s) | def image (self, file, im) : # puis on l’écrit dans le fichier file g = open (file, "w") g.write (graph) g.close () # enfin, on convertit ce fichier en image dot = pydot.graph_from_dot_file (file) dot.write_png (im, prog="dot") def construit_arbre () : # même code que dans le programme précédent # mais inclus dans une fonction l = ["un", "deux", "unite", "dizaine", "exception", "dire", \ "programme", "abc", "xyz", "opera", "quel"] racine = None for mot in l : if racine == None : racine = NoeudTri2 (mot) else : racine.insere (mot) return racine racine = construit_arbre () print racine racine.image ("", "") | La méthode nouveau_noeud permet de s’assurer que tous les nœuds insérés lors de la création du graphe seront bien du type NoeudTri2 qui inclut la méthode chaine_graphe. Cette méthode serait inutile s’il n’y avait qu’une seule classe NoeudTri contenant toutes les méthodes. Si on la met en commentaire, le message d’erreur suivant apparaît : Traceback (most recent call last): File "", line 53, in <module> racine.image ("", "") File "", line 27, in image graph = self.chaine_graphe () File "", line 14, in chaine_graphe h = self.avant.chaine_graphe () AttributeError: NoeudTri instance has no attribute ’chaine_graphe’ | L’erreur signifie que le programmeur cherche à appeler une méthode qui n’existe pas dans la classe NoeudTri parce que seul le premier nœud de l’arbre est de type NoeudTri2 contrairement aux nœuds insérés par la méthode nouveau_noeud de la classe NoeudTri. En surchargeant cette méthode, on s’assure que tous les nœuds sont du même type NoeudTri2. Il existe néanmoins une façon d’éviter de surcharger cette fonction à chaque fous. Il suffit qu’elle crée automatiquement la bonne classe, que l’objet soit une instance de NoeudTri ou NoeudTri2. C’est ce que fait l’exemple suivant où self.__class__ correspond à la classe de l’objet. Au final, la méthode image construit l’image du graphe. Le fichier graphe.txt doit ressembler à ce qui suit : digraph GA { 18853120 [label="un",style=filled,shape=record] 28505472 [label="deux",style=filled,shape=record] 28505712 [label="abc",style=filled,shape=record] 28505472 -> 28505712 [label="<"] 28505552 [label="dizaine",style=filled,shape=record] 28505592 [label="dire",style=filled,shape=record] 28505552 -> 28505592 [label="<"] 28505632 [label="exception",style=filled,shape=record] 28505672 [label="programme",style=filled,shape=record] 28505792 [label="opera",style=filled,shape=record] 28505672 -> 28505792 [label="<"] 28505832 [label="quel",style=filled,shape=record] 28505672 -> 28505832 [label=">"] 28505632 -> 28505672 [label=">"] 28505552 -> 28505632 [label=">"] 28505472 -> 28505552 [label=">"] 18853120 -> 28505472 [label="<"] 28505512 [label="unite",style=filled,shape=record] 28505752 [label="xyz",style=filled,shape=record] 28505512 -> 28505752 [label=">"] 18853120 -> 28505512 [label=">"] } La numérotation des nœuds importe peu du moment que chaque nœud reçoit un identifiant unique. C’est pour cela que la fonction id est pratique dans ce cas-là. Le programme suivant construit une sortie au format HTML mélangeant image et texte. Il commence par importer le programme quicksort2 qui n’est autre que celui incluant la classe NoeudTri2. Il termine en appelant le navigateur Mozilla Firefox afin d’afficher le résultat automatiquement. # coding: latin-1 import quicksort2 # construction de l’arbre racine = quicksort2.construit_arbre () # construction de l’image du graphe racine.image ("", "") # création d’un fichier HTML f = open ("", "w") f.write ("<body><html>\n") # début s = str (racine) # on récupère la liste triée s = s.replace ("\n", "<BR>\n") # <BR> permet de passer à la ligne f.write (s) f.write ("<H1> graphe </H1>\n") # titre pour l’image f.write (’<img src="/" width=400/>\n’) # image f.write ("<H1> code du graphe </H1>\n") # titre pour le code du graphe s = racine.chaine_graphe () # on récupère le code du graphe f.write ("<pre>\n") # on l’affiche tel quel f.write (s) | f.write ("</pre>\n") f.write ("</html></body>\n") # fin f.close () # on lance le navigateur automatiquement pour afficher la page import os os.system (r’"C:\Program Files\Mozilla Firefox\firefox" ’) Le fichier page.html contient les lignes suivantes excepté les points de suspension qui remplacent la partie tronquée qu’on peut aisément deviner. <body><html> <H1> liste triée </H1> abc<BR> deux<BR> unite<BR> xyz<BR> <H1> graphe </H1> <img src="/" width=400/> <H1> code du graphe </H1> <pre> 13697312 [label="un",style=filled,shape=record,fontsize=60] 13697192 [label="deux",style=filled,shape=record,fontsize=60] 34692472 [label="abc",style=filled,shape=record,fontsize=60] 13697232 -> 34692592 [label=">",fontsize=60] 13697312 -> 13697232 [label=">",fontsize=60] </pre> </html></body> # coding: latin-1 import quicksort2 # construction de l’arbre racine = quicksort2.construit_arbre () # construction de l’image du graphe racine.image ("", "") # construction du début du fichier tex package = """a4 amsmath amssymb subfigure float latexsym amsfonts epic eepic makeidx multido varindex moreverb alltt fancyvrb fancyhdr color eurosym tabularx placeins url shorttoc""".split () header = """\\documentclass[french,11pt]{article}\n\\usepackage[french]{babel} \\usepackage[usenames]{color}\\usepackage{""" + \ "}\n\\usepackage{".join (package) + \ | """}\\usepackage[small,normal]{caption2}\\urlstyle{sf} \\usepackage[pdftex]{graphicx}\usepackage[T1]{fontenc} \DefineVerbatimEnvironment{verbatimx}{Verbatim}{frame=single, framerule=.1pt, framesep=1.5mm, fontsize=\\footnotesize,xleftmargin=0pt} \\begin{document}\n""" # création d’un fichier tex f = open ("", "w") f.write (header) f.write ("\\title{Tri quicksort}\n") # définit le titre f.write ("\\maketitle\n") # écrit le titre f.write ("\\tableofcontents\n") # table des matières f.write ("\\section{liste triée}\n") # titre pour la liste triée s = str (racine) # on récupère la liste triée s = s.replace ("\n", "\\\\ \n") # \\ passe à la ligne f.write ("\\begin{tabular}{|l|}\n") f.write (s) f.write ("\\end{tabular}\n") f.write (’\\includegraphics[height=5cm]{}\n’) # image f.write ("\\section{code du graphe}\n") # titre pour le code du graphe s = racine.chaine_graphe () # on récupère le code du graphe f.write ("\\begin{verbatimx}\n") # on l’affiche tel quel f.write (s) f.write ("\\end{verbatimx}\n") f.write ("\\end{document}\n") # fin f.close () # on compile deux fois le fichier pour que la table des matières # soit bien prise en compte import os os.system (r’"C:\Program Files\MiKTeX 2.7\miktex\bin\pdflatex" ’) os.system (r’"C:\Program Files\MiKTeX 2.7\miktex\bin\pdflatex" ’) # on affiche le résultat avec Adobe Reader os.system (r’"C:\Program Files\Adobe\Reader 9.0\Reader\" ’) | La partie header est longue dans cet exemple, elle inclut des packages Latex qui ne sont pas utilisés mais qui pourraient l’être dans le cas d’un rapport plus long. Il faut bien sûr connaître quelques rudiments de ce langage pour construire le document PDF. L’avantage de ce système est de pouvoir retravailler manuellement le document final. Il est également indiqué lorsque le rapport mélange tableaux de chiffres récupérés depuis différentes sources et graphiques qui peuvent être générés par Python via un module comme matplotlib . Sous Linux, il suffit de modifier les chemins d’accès aux différentes applications. Une dernière remarque, il existe sous Windows un éditeur convenable qui est TeXnicCenter .
Chapitre 11 Exercices pratiques pour s’évaluer 11.1 Recherche dichotomique L’objectif de cet exercice est de programmer une recherche dans une liste triée. 1)Il faut d’abord récupérer un fichier texte disponible sur Intranet . Ce fichier contient un mot par ligne. Il faut lire ce fichier et construire une liste avec tous ces mots. 2)Construire une fonction qui vérifie que la liste chargée à la question précédente est triée. 3)Construire une fonction qui recherche un mot X dans la liste et qui retourne sa position ou -1 si ce mot n’y est pas. Cette fonction prend deux paramètres : la liste et le mot à chercher. Elle retourne un entier. On précise que pour savoir si deux chaînes de caractères sont égales, il faut utiliser l’opérateur ==. 4)Quels sont les positions des mots "UN" et "DEUX"? La réponse doit figurer en commentaire dans le programme. Il faudra écrire aussi le nombre de comparaisons effectuées pour trouver ces deux positions. 5)Lorsqu’une liste est triée, rechercher un élément est beaucoup plus rapide. Si on cherche le mot X dans la liste, il suffit de le comparer au mot du milieu pour savoir si ce mot est situé dans la partie basse (X inférieur au mot du milieu), la partie haute (X supérieur au mot du milieu). S’il est égal, le mot a été trouvé. Si le mot n’a pas été trouvé, on recommence avec la sous-liste inférieure ou supérieure selon les cas jusqu’à ce qu’on ait trouvé le mot ou qu’on soit sûr que le mot cherché n’y est pas. Le résultat de la recherche est la position du mot dans la liste ou -1 si ce mot n’a pas été trouvé. Cette recherche s’appelle une recherche dichotomique. 6)Normalement, les positions des mots "UN" et "DEUX" n’ont pas changé mais il faut de nouveau déterminer le nombre d’itérations effectuées pour trouver ces deux positions avec la recherche dichotomique. 7)Quel est, au pire , le coût d’une recherche non dichotomique? La réponse doit figurer en commentaire dans le programme. 8)Quel est, au pire, le coût d’une recherche dichotomique? La réponse doit figurer en commentaire dans le programme. Correction # coding: latin-1 # question 1 def lit_fichier (file) : f = open (file, "r") mot = [] for l in f : mot.append ( l.replace ("\n", "") ) f.close () return mot mot = lit_fichier ("") print mot # question 2 def est_trie (mot) : for i in range (1, len (mot)) : if mot [i-1] > mot [i] : return False return True tri = est_trie (mot) print "liste triée ", tri # question 3 def cherche (mot, m) : for i in range (0, len (mot)) : if mot [i] == m : return i return -1 print "mot ACHATS ", cherche (mot, "ACHATS") print "mot achats ", cherche (mot, "achats") # question 4 un = cherche (mot, "UN") deux = cherche (mot, "DEUX") print "recherche normale ", un, deux print "nombre d’itérations", un + deux # question 5, 6, nbun et nbdeux contiennent le nombre de comparaisons def cherche_dicho (mot, m) : | a = 0 b = len (mot)-1nb = 0 while a < b : nb += 1 p = (a+b)/2 if mot [p] == m : return p,nb elif mot [p] > m : b = p-1 else : a = p+1 return -1,nb un,nbun = cherche_dicho (mot, "UN") deux,nbdeux = cherche_dicho (mot, "DEUX") print "recherche dichotomique ", un, deux print "nombre d’itérations ", nbun + nbdeux # question 7 """ Lors d’une recherche simple, au pire, l’élément cherche sera en dernière position, ce qui signifie n itérations pour le trouver. Le coût de la recherche simple est en O(n). """ # question 8 """ """ | 11.2 Ajouter un jour férié 11.2.1 Avec des fonctions uniquement Le gouvernement désire ajouter un jour férié mais il voudrait le faire à une date éloignée des jours fériés existant. On suppose également que ce jour ne sera pas inséré entre Noël et le jour de l’an. On va donc calculer le nombre de jours qui sépare deux jours fériés dont voici la liste pour l’année 2007 : Jour de l’an | 1er janvier 2007 | Lundi de Pâques | 9 avril 2007 | Fête du travail | 1er mai 2007 | Victoire de 1945 | 8 mai 2007 | Ascension | 17 mai 2007 | Lundi de Pentecôte | 4 juin 2007 | Fête nationale | 14 juillet 2007 | Assomption | 15 août 2007 | Toussaint | 1er novembre 2007 | Armistice de 1918 | 11 novembre 2007 | Noël | 25 décembre 2007 | On rappelle que l’année 2007 n’est pas une année bissextile et qu’en conséquence, le mois de février ne comporte que 28 jours. 1)Afin de simplifier la tâche, on cherche à attribuer un numéro de jour à chaque jour férié : l’année a 365 jours, pour le numéro du lundi de Pâques, soit 31 (mois de janvier) + 28 (février) + 31 (mars) + 9 = 89. La première question consiste à construire une fonction qui calcule le numéro d’une date étant donné un jour et un mois. Cette fonction prend comme entrée : – un numéro de jour – un numéro de mois – une liste de 12 nombres correspondant au nombre de jours dans chacun des douze mois de l’année 2)Si on définit la liste des jours fériés comme étant une liste de couples (jour, mois) triée par ordre chronologique, il est facile de convertir cette liste en une liste de nombres correspondant à leur numéro dans l’année. La fonction à écrire ici appelle la précédente et prend une liste de couples en entrée et retourne comme résultat une liste d’entiers. 11.2.2 Programme équivalent avec des classes Le programme précédent n’utilise pas de classe. L’objectif de ce second exercice est de le réécrire avec une classe. 1)Une fonction du programme précédent effectue la conversion entre un couple jourmois et un numéro de jour. Les calculs sont faits avec le numéro mais le résultat désiré est une date : les numéros ne sont que des intermédiaires de calculs qui ne devraient pas apparaître aussi explicitement. La première question consiste à créer une classe Date : class Date : def __init__ (self, jour, mois) : | 2)A cette classe, on ajoute une méthode qui retourne la conversion du couple jourmois en un numéro de jour de l’année. 3)On ajoute maintenant une méthode calculant le nombre de jours séparant deux dates (ou objet de type Date et non pas numéros). Cette méthode pourra par exemple s’appeler difference. 4)Il ne reste plus qu’à compléter le programme pour obtenir les mêmes résultats que le programme de l’exercice 1. 5)Avec ce programme, lors du calcul des écarts entre tous les jours fériés consécutifs, combien de fois effectuez-vous la conversion du couple jour-mois en numéro pour le second jour férié de l’année? Est-ce le même nombre que pour le programme précédent (en toute logique, la réponse pour le premier programme est 1)? 6)La réponse à la question précédente vous suggère-t-elle une modification de ce second programme? Correction # coding: latin-1 #################################### # exercice 1 #################################### # question 1 def numero (jour, mois, duree = [31, 28, 31,30,31,30,31,31,30,31,30,31] ) : s = 0 for i in range (0,mois-1) : s += duree [i] s += jour - 1 return s+1 # question 2 def conversion_liste (li) : res = [] for jour,mois in s : res.append ( numero (jour, mois)) # pareil que def ecart (num) : res = [] for i in range (1, len (num)) : d = num [i] - num [i-1] res.append (d) return res s = [ (1,1), (9,4), (1,5), (8,5), (17,5), (4,6), (14,7), \ (15,8), (1,11), (11,11), (25,12) ] r = conversion_liste (s) ec = ecart (r) # question 3 pos = ec.index ( max (ec) ) print "position de l’écart le plus grand ", pos print "jour ", s [pos], " --> ", s [pos+1] #################################### # exercice 2 #################################### # question 4 class Date : def __init__ (self, jour, mois) : = jour = mois self.duree = [31, 28, 31,30,31,30,31,31,30,31,30,31] | # question 5 def numero (self) : s = 0 for i in range (0,-1) : s += self.duree [i] s += - 1 return s+1 # question 6 def difference (self, autre) : return self.numero () - autre.numero () def conversion_date (s) : res = [] for jour,mois in s : res.append ( Date (jour, mois) ) return res def ecart_date (date) : ec = [] for i in range (1, len (date)) : ec.append ( date [i].difference ( date [i-1] ) ) return ec # question 7 s = [ (1,1), (9,4), (1,5), (8,5), (17,5), (4,6), \ (14,7), (15,8), (1,11), (11,11), (25,12) ] r = conversion_date (s) ec = ecart_date (r) pos = ec.index ( max (ec) ) print "position de l’ecart le plus grand ", pos print "jour ", s [pos], " --> ", s [pos+1] # question 8 """ La conversion en Date est faite une fois pour les dates (1,1) et (25,12) et 2 fois pour les autres en effet, la méthode difference effectue la conversion en numéros des dates self et autre la fonction ecart_date calcule date [i].difference ( date [i-1] ) et date [i+1].difference ( date [i] ) --> la date [i] est convertie 2 fois """ # question 9 """ On peut par exemple stocker la conversion en numéro dans le constructeur comme suit : """ class Date : def __init__ (self, jour, mois) : = jour = mois self.duree = [31, 28, 31,30,31,30,31,31,30,31,30,31] = self.numero () # question 5 s += self.duree [i] s += - 1 return s+1 # question 6 def difference (self, autre) : return - r = conversion_date (s) ec = ecart_date (r) pos = ec.index ( max (ec) ) print "position de l’écart le plus grand ", pos print "jour ", s [pos], " --> ", s [pos+1] | On pourrait encore améliorer la dernière classe Date en créant un attribut statique pour l’attribut duree qui est identique pour toutes les instances de la classe Date. 11.3 Fréquentation d’un site Internet Lorsqu’on se connecte à un site internet, celui-ci enregistre votre adresse IP, l’heure et la date de connexion ainsi que la page ou le fichier désiré. Ainsi le fichier recense ces quatre informations séparées par des espaces pour les fichiers d’extension pdf téléchargés sur le site . 1)La première étape consiste à charger les informations depuis le fichier texte dans une matrice de 4 colonnes (liste de listes, liste de t-uples). On utilisera pour cela les fonctions open et les méthodes split, replace associées aux chaînes de caractères. On pourra s’aider des corrections des TD précédents. 2)On souhaite dans un premier temps faire des statistiques sur les dates : on veut compter le nombre de fichiers téléchargés pour chaque date présente dans les données. On pourra pour cela utiliser un dictionnaire dont la clé sera bien choisie. Le résultat devra être inséré dans une fonction prenant comme entrée une matrice (ou liste de listes, liste de t-uples) et retournant un dictionnaire comme résultat. 3)On s’intéresse au programme suivant : l = [ (1, "un"), (3, "deux",), (2, "deux"), (1, "hun"), (-1, "moinsun") ] l.sort (reverse = True) print l Donc le résultat est : [(3, ’deux’), (2, ’deux’), (1, ’un’), (1, ’hun’), (-1, ’moinsun’)] Que s’est-il passé? 5)Effectuez le même travail pour déterminer les dix documents les plus téléchargés. 6)Ecrire une fonction qui retourne l’heure sous forme d’entier à partir d’une date définie par une chaîne de caractères au format ”hh : mm : ss”. Par exemple, pour ”14 : 55 : 34”, la fonction doit retourner 14 sous forme d’entier. L’instruction int(”14”) convertit une chaîne de caractères en un entier. 7)Calculer le nombre de documents téléchargés pour chaque heure de la journée. Le site est-il consulté plutôt le matin ou le soir? Serait-il possible de conclure aussi rapidement pour un site d’audience internationale? Correction # coding: latin-1 # la première ligne autorise les accents dans un programme Python # la langue anglaise est la langue de l’informatique, # les mots-clés de tous les langages # sont écrits dans cette langue. #################################### # exercice 1 #################################### # # question 1 def lit_fichier (file) : f = open (file, "r") li = f.readlines () # découpage sous forme de lignes f.close () res = [] for l in li : s = l.replace ("\n", "") s = s.split (" ") # le séparateur des colonnes est l’espace res.append (s) return res mat = lit_fichier ("") for m in mat [0:5] : # on affiche les 5 premières lignes print m # parce que sinon, c’est trop long # question 2 def compte_date (mat) : d = { } for m in mat : date = m [1] # clé if date in d : d [date] += 1 else : d [date] = 1 return d dico_date = compte_date (mat) print dico_date | hs = s [0:2] | # on extrait la partie correspondant à l’heure | return int (hs) | # on retourne la conversion sous forme d’entiers | # remarque générale : si le fichier contient des lignes # vides à la fin, il se produira une erreur à la ligne 34 (date = m [1]) # car la matrice mat contiendra des lignes avec une seule colonne et non quatre. # question 3 # La méthode sort trie la liste mais comment ? # Il est facile de trier une liste de nombres mais une liste de couples de # nombres ? L’exemple montre que la liste est triée selon le premier élément de # chaque couple. Pour les cas où deux couples ont un premier élément en commun, # les éléments semblent triés selon le second élément. L’exemple suivant le monte : l = [(1, "un"), (3, "deux",), (2, "deux"), (1, "hun"), (1, "un"), (-1, "moinsun")] l.sort (reverse = True) print l # affiche [(3, ’deux’), (2, ’deux’), (1, ’un’), # (1, ’un’), (1, ’hun’), (-1, ’moinsun’)] # question 4 def dix_meilleures (dico) : # dans cette fonction on crée une liste de couples (valeur,clé) ou # la clé représente une date et valeur le nombre de téléchargement # pour cette date li = [] for d in dico : cle = d valeur = dico [cle] li.append ( ( valeur, cle ) ) (reverse = True) return li [0:10] dix = dix_meilleures (dico_date) print dix # la première date est (283, ’26/Sep/2007’) # les quatre premières dates correspondent aux quatre premiers TD en 2007 à l’ENSAE # question 5 # la date est en colonne 1, le document en colonne 3 # on fait un copier-coller de la fonction compte_date en changeant un paramètre def compte_document (mat) : d = { } for m in mat : doc = m [3] # clé, 3 au lieu de 1 à la question 2 if doc in d : d [doc] += 1 else : d [doc] = 1 return d dix = dix_meilleures ( compte_document (mat) ) print dix # le premier document est # (323, ’/mywiki/Enseignements? ’), # question 6 def heure (s) : # question 7 # on recommence avec un copier-coller def compte_heure (mat) : d = { } for m in mat : h = m [2] # clé, 2 au lieu de 1 à la question 2 cle = heure (h) if cle in d : d [cle] += 1 else : d [cle] = 1 return d for i in h : print i, "h ", h [i] # Il y a beaucoup plus de téléchargement entre 20h et 2h du matin # que le matin avant 10h. # Le site est plutôt consulté le soir. # La conclusion ne serait pas aussi évidente avec un site consulté par des gens # du monde entier puisque 6h du matin est une heure de l’après midi au Japon. # Il faudrait croiser l’heure avec la position géographique de la personne # qui consulte le site. |
Chapitre 12 Exercices écrits Chacun des énoncés qui suit est une série d’exercices auxquels une personne expérimentée peut répondre en une heure. C’est souvent le temps imparti à un test informatique lors d’un entretien d’embauche pendant lequel le candidat propose des idées plus qu’il ne rédige des solutions. Chaque énoncé est un panel représentatif des problèmes que la programmation amène inévitablement. La difficulté de chaque exercice est notée du plus facile au plus difficile : *, **, ***. Le sixième énoncé 12 est structuré sous forme de problème. Il cherche à reproduire la démarche suivie lors de la conception d’un algorithme. 12.1 Premier énoncé 12.1.1 Programme à deviner * Que fait le programme suivant? Que contiendra res à la fin du programme? l = [ 0,1,2,3,4,6,5,8,9,10] res = True for i in range (1,len (l)) : if l[i-1] > l[i] : res = False | Correction Le programme vérifie que la liste l est triée, res vaut True si elle est triée, False sinon. Et dans ce cas précis, elle vaut False car la liste n’est pas triée (4,6,5). 12.1.2 Somme des chiffres d’un nombre * Ecrire une fonction qui calcule la somme des chiffres d’un entier positif. Correction Tout d’abord, obtenir la liste des chiffres d’un nombre entier n n’est pas immédiat bien qu’elle puisse s’écrire en une ligne : def somme (n) : return sum ( [ int (c) for c in str (n) ] ) def somme (n) : l = str (n) | # il ne faut pas confondre l=str (n) avec l = "n" | s = 0 for c in l : | # ou for i in range (0, len (c)) : | s += int (c) return s | # ou s += int (c [i]) | Une version numérique maintenant, celle qui utilise l’opération % ou modulo pour obtenir le dernier chiffre : def somme (n) : s = 0 while n > 0 : s += n % 10 n /= 10 # ici, c’est une division entière, si vous n’êtes pas sûr : # n = int (n/10) return n | Enfin, une autre solution utilisant la récurrence et sans oublier la condition d’arrêt : def somme (n) : if n <= 0 : return 0 else : return (n % 10) + somme ( n / 10 ) | Parmi les autres solutions, certaines, exotiques, ont utilisé la fonction log en base 10 ou encore la fonction exp. En voici une : import math def somme (n) : k = int ( (n) / (10) + 1) s = 0 for i in range (1,k+1) : d = 10 ** i # ou encore d = int (exp ( k * log (10) ) ) c = n / d e = n - c * d f = e / (d / 10) s += f return s | L’idée principale permet de construire une fonction retournant le résultat souhaité mais il faut avouer qu’il est plus facile de faire une erreur dans cette dernière fonction que dans les trois précédentes. 12.1.3 Calculer le résultat d’un programme ** Que vaut n à la fin de l’exécution du programme suivant? Expliquez en quelques mots le raisonnement suivi. n = 0 for i in range (0,10) : if (n + i) % 3 == 0 : n += 1 | Correction i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | n avant le test | 0 | 1 | 1 | 2 | 2 | 3 | 3 | 4 | 4 | 5 | i+n | 0 | 2 | 3 | 5 | 6 | 8 | 9 | 11 | 12 | 14 | n après le test | 1 | 1 | 2 | 2 | 3 | 3 | 4 | 4 | 5 | 5 | range(0,10) contient les nombres de 0 à 10 exclu. La réponse cherchée est donc 5. 12.1.4 Suite récurrente (Fibonacci) * Ecrire un programme qui calcule l’élément u13 de la suite définie par : u1 | = | 1 | u2 | = | 2 | ?n ? 2, un | = | un?1 + un?2 | u13 est le nombre de manières possibles pour une grenouille de monter en haut d’une échelle de 13 barreaux si elle peut faire des bonds de un ou deux barreaux seulement. Si cette affirmation, qui n’est pas à démontrer, vous semble douteuse, avant d’y revenir, faites d’abord le reste de l’énoncé. Correction Tout d’abord, la grenouille : elle peut faire des bonds de un ou deux barreaux à chaque fois. Donc, lorsqu’elle est sur le barreau n, elle a pu venir du barreau n ? 1 et faire un saut de 1 barreau ou venir du barreau n ? 2 et faire un bond de 2 barreaux. Par conséquent, le nombre de manières qu’a la grenouille d’atterrir sur le barreau n, c’est la somme des manières possibles de venir jusqu’au barreau n?1 et du barreau n ? 2. Maintenant, comment le programmer? Tout d’abord les solutions récursives, les plus simples à programmer : def grenouille (n) : if n == 2 : return 2 elif n == 1 : return 1 else : return grenouille (n-1) + grenouille (n-2) print grenouille (13) | Cette solution n’est pas très rapide car si grenouille(13) va être appelée une fois, grenouille(12) une fois aussi, mais grenouille(11) va être appelée deux fois Cette solution implique un grand nombre de calculs inutiles. def grenouille (fin, n = 2, u1 = 1, u2 = 2) : if fin == 1 : return u1 elif fin == 2 : return u2 elif n == fin : return u2 u = u1 + u2 return grenouille (fin, n+1, u2, u) print grenouille (13) | La méthode la plus simple est non récursive mais il ne faut pas se tromper dans les indices : def grenouille (n) : if n == 1 : return 1 u1 = 1 u2 = 2 for i in range (3,n+1) : u = u1 + u2 # il est impossible de u1 = u2 # résumer ces trois lignes u2 = u # en deux return u2 print grenouille (13) | Quelques variantes utilisant les listes : def grenouille (n) : if n == 1 : return 1 u = [1,2] for i in range (2,n) : u.append (u [i-1] + u [i-2]) return u [n-1] print grenouille (12) | Ou encore : def grenouille (n) : if n == 1 : return 1 u = range (0, n) # il ne faut pas oublier de créer le tableau # avec autant de cases que nécessaire # ici 13 u [0] = 1 u [1] = 2 for i in range (2,n) : u [i] = u [i-1] + u [i-2] return u [n-1] print grenouille (12) | 12.1.5 Comprendre une erreur d’exécution * On écrit le programme suivant : a = "abcdefghijklmnopqrstuvwxyz" print len (a) d = {} for i in range (0,len (a)) : d [ a [ i ] ] = i print d ["M"] Une erreur est déclenchée lors de l’exécution : Que faudrait-il écrire pour que le programme marche? Qu’affichera-t-il alors? Correction L’erreur signifie que la clé ”M” n’est pas présente dans le dictionnaire d. Elle n’est présente qu’en minuscule. La modification proposée est la suivante : a = "abcdefghijklmnopqrstuvwxyz" print len (a) d = {} for i in range (0,len (a)) : d [ a [ i ] ] = i print d ["m"] ###### ligne modifiée | Mais il était également possible de procéder comme suit : Dans les deux cas, le programme affiche la position de M dans l’alphabet qui est 12 car les indices commencent à 0. 12.1.6 Copie de variables ** def somme (tab) : l = tab[0] for i in range (1, len (tab)) : l += tab [i] return l ens = [[0,1],[2,3]] print somme ( ens ) # affiche [0,1,2,3] print ens # affiche [ [0,1,2,3], [2,3] ] | La fonction somme est censée faire la concaténation de toutes les listes contenues dans ens. Le résultat retourné est effectivement celui désiré mais la fonction modifie également la liste ens, pourquoi? Correction Le problème vient du fait qu’une affectation en Python (seconde ligne de la fonction somme) ne fait pas une copie mais crée un second identificateur pour désigner la même chose. Ici, l et tab[0] désignent la même liste, modifier l’une modifie l’autre. Ceci explique le résultat. Pour corriger, il fallait faire une copie explicite de tab[0] : import copy ###### ligne ajoutée def somme (tab) : l = (tab[0]) ###### ligne modifiée for i in range (1, len (tab)) : l += tab [i] return l ens = [[0,1],[2,3]] print somme ( ens ) # affiche [0,1,2,3] print ens # affiche [ [0,1,2,3], [2,3] ] | Il était possible, dans ce cas, de se passer de copie en écrivant : def somme (tab) : l = [] ###### ligne modifiée for i in range (0, len (tab)) : ###### ligne modifiée l += tab [i] return l ens = [[0,1],[2,3]] print somme ( ens ) # affiche [0,1,2,3] print ens # affiche [ [0,1,2,3], [2,3] ] | 12.1.7 Comprendre une erreur d’exécution ** On écrit le programme suivant : li = range (0,10) sup = [0,9] for i in sup : del li [i] print li Mais il produit une erreur : :44: IndexError: list assignment index out of range Correction L’erreur signifie qu’on cherche à accéder à un élément de la liste mais l’indice est soit négatif, soit trop grand. Pour comprendre pourquoi cette erreur se produit, il faut suivre les modifications de la liste li dans la boucle for. Au départ, li vaut range(0,10), soit : [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9] Lors du premier passage de la boucle, on supprime l’élément d’indice 0, la liste li est donc égale à : [ 1, 2, 3, 4, 5, 6, 7, 8, 9] Et elle ne contient plus que 9 éléments au lieu de 10, donc le dernier élément est celui d’indice 8. Or lors du second passage dans la boucle for, on cherche à supprimer l’élément d’indice 9, ce qui est impossible. L’idée la plus simple pour corriger l’erreur est de supprimer les éléments en commençant par ordre d’indices décroissant : li = range (0,10) sup = [9,0] ####### ligne modifiée for i in sup : del li [i] print li Mais on pouvait tout à fait écrire aussi sup = [0,8] même si ce n’est la réponse que je conseillerais. L’objectif du programme n’était pas de supprimer toute la liste li. Une dernière précision, pour supprimer l’élément d’indice i de la liste li, on peut écrire soit delli[i] soit del li[i :i+1]. 12.1.8 Comprendre une erreur de logique ** Le programme suivant fonctionne mais le résultat n’est pas celui escompté. l = ["un", "deux", "trois", "quatre", "cinq"] for i in range (0,len (l)) : mi = i for j in range (i, len (l)) : if l[mi] < l [j] : mi = j e = l [i] l [mi] = l [i] l [i] = e print l | Le résultat affiché est : [’un’, ’deux’, ’deux’, ’deux’, ’cinq’] Qu’est censé faire ce programme? Quelle est l’erreur? Correction Ce programme est censé effectuer un tri par ordre alphabétique décroissant. Le problème intervient lors de la permutation de l’élément l[i] avec l’élément l[mi]. Il faut donc écrire : mi = i for j in range (i, len (l)) : if l[mi] < l [j] : mi = j e = l [mi] ######## ligne modifiée l [mi] = l [i] l [i] = e print l | 12.1.9 Coût d’un algorithme ** Le coût d’un algorithme ou d’un programme est le nombre d’opérations (additions, multiplications, tests, ) qu’il effectue. Il s’exprime comme un multiple d’une fonction de la dimension des données que le programme manipule. Par exemple : O(n), O(n2), O(nlnn), Quel est le coût de la fonction variance en fonction de la longueur de la liste tab? N’y a-t-il pas moyen de faire plus rapide? def moyenne (tab) : s = 0.0 for x in tab : s += x return s / len (tab) def variance (tab) : s = 0.0 for x in tab : t = x - moyenne (tab) s += t * t return s / len (tab) l = [ 0,1,2, 2,3,1,3,0] print moyenne (l) print variance (l) | Correction Tout d’abord, le coût d’un algorithme est très souvent exprimé comme un multiple de la dimension des données qu’il traite. Ici, la dimension est la taille du tableau tab. Par exemple, si on note n = len(tab), alors le coût de la fonction moyenne s’écrit O(n) car cette fonction fait la somme des n éléments du tableau. La fonction variance contient quant à elle un petit piège. Si elle contient elle aussi une boucle, chacun des n passages dans cette boucle fait appel à la fonction moyenne. Le coût de la fonction variance est donc O(n2). Il est possible d’accélérer le programme car la fonction moyenne retourne le même résultat à chaque passage dans la boucle. Il suffit de mémoriser son résultat dans une variable avant d’entrer dans la boucle comme suit : def variance (tab) : s = 0.0 m = moyenne (tab) for x in tab : t = x - m s += t * t return s / len (tab) | Le coût de la fonction variance est alors O(n). Remarque 12.1 : coût d’un algorithme Python aboutit à un programme lent qui inclut une quantité de tests invisibles pour celui qui programme mais qui détecte les erreurs plus vite et favorise une conception rapide. Il n’est pas adapté au traitement d’information en grand nombre et fait une multitude d’opérations cachées. 12.1.10 Héritage ** On a besoin dans un programme de créer une classe carre et une classe rectangle. Mais on ne sait pas quelle classe doit hériter de l’autre. Dans le premier programme, rectangle hérite de carre. class carre : def __init__ (self, a) : self.a = a def surface (self) : return self.a ** 2 class rectangle (carre) : def __init__ (self, a,b) : carre.__init__(self,a) self.b = b def surface (self) : return self.a * self.b | Dans le second programme, c’est la classe carre qui hérite de la classe rectangle. class rectangle : def __init__ (self, a,b) : self.a = a self.b = b def surface (self) : return self.a * self.b class carre (rectangle) : def __init__ (self, a) : rectangle.__init__ (self, a,a) def surface (self) : return self.a ** 2 | 1)Dans le second programme, est-il nécessaire de redéfinir la méthode surface dans la classe carre? Justifiez. 2)Quel est le sens d’héritage qui vous paraît le plus censé, classrectangle(carre) ou classcarre(rectangle)? Justifiez. 3)On désire ajouter la classe losange. Est-il plus simple que rectangle hérite de la classe carre ou l’inverse pour introduire la classe losange? Quel ou quels attributs supplémentaires faut-il introduire dans la classe losange? Correction 1)Le principe de l’héritage est qu’une classe carre héritant de la classe rectangle hérite de ses attributs et méthodes. L’aire d’un carré est égale à celle d’un rectangle dont les côtés sont égaux, par conséquent, la méthode surface de la classe retourne la même valeur que celle de la classe rectangle. Il n’est donc pas nécessaire de la redéfinir. 3)Un losange est défini par un côté et un angle ou un côté et la longueur d’une de ses diagonales, soit dans les deux cas, deux paramètres. Dans la première question, il paraissait plus logique que la classe la plus spécifique hérite de la classe la plus générale afin de bénéficier de ses méthodes. Pour introduire le losange, il paraît plus logique de partir du plus spécifique pour aller au plus général afin que chaque classe ne contienne que les informations qui lui sont nécessaires. class carre : def __init__ (self, a) : self.a = a def surface (self) : return self.a ** 2 class rectangle (carre) : def __init__ (self, a,b) : carre.__init__(self,a) self.b = b def surface (self) : return self.a * self.b class losange (carre) : def __init__ (self, a,theta) : carre.__init__(self,a) self.theta = theta def surface (self) : return self.a * (self.theta) * self.a * (self.theta) * 2 | Le sens de l’héritage dépend de vos besoins. Si l’héritage porte principalement sur les méthodes, il est préférable de partir du plus général pour aller au plus spécifique. La première classe sert d’interface pour toutes ses filles. Si l’héritage porte principalement sur les attributs, il est préférable de partir du plus spécifique au plus général. Dans le cas général, il n’y a pas d’héritage plus sensé qu’un autre mais pour un problème donné, il y a souvent un héritage plus sensé qu’un autre. 12.1.11 Précision des calculs *** 1)On exécute le programme suivant : x = 1.0 for i in range (0,15) : x = x / 10 print i, "\t", 1.0 - x, "\t", x, "\t", x **(0.5) | Il affiche à l’écran le résultat suivant : 0 | 0.90000000000000002220 | 0.1 | 0.316227766017 | 1 | 0.98999999999999999112 | 0.01 | 0.1 | 2 | 0.99899999999999999911 | 0.001 | 0.0316227766017 | 3 | 0.99990000000000001101 | 0.0001 | 0.01 | 4 | 0.99999000000000004551 | 1e-05 | 5 | 0.99999899999999997124 | 1e-06 | 0.001 | 6 | 0.99999990000000005264 | 1e-07 | 0.000316227766017 | 7 | 0.99999998999999994975 | 1e-08 | 0.0001 | 8 | 0.99999999900000002828 | 1e-09 | 3.16227766017e-05 | 9 | 0.99999999989999999173 | 1e-10 | 1e-05 | 10 | 0.99999999998999999917 | 1e-11 | 3.16227766017e-06 | 11 | 0.99999999999900002212 | 1e-12 | 1e-06 | 12 | 0.99999999999989996891 | 1e-13 | 3.16227766017e-07 | 13 | 0.99999999999999000799 | 1e-14 | 1e-07 | 14 | 0.99999999999999900080 | 1e-15 | 3.16227766017e-08 | 15 | 0.99999999999999988898 | 1e-16 | 1e-08 | 16 | 1.00000000000000000000 | 1e-17 | 3.16227766017e-09 | 17 | 1.00000000000000000000 | 1e-18 | 1e-09 | 18 | 1.00000000000000000000 | 1e-19 | 3.16227766017e-10 | 19 | 1.00000000000000000000 | 1e-20 | 1e-10 | Que peut-on en déduire? 2)On écrit une classe matrice_carree_2 qui représente une matrice carrée de dimension 2. class matrice_carree_2 : def __init__ (self, a,b,c,d) : self.a, self.b, self.c, self.d = a,b,c,d def determinant (self) : return self.a * self.d - self.b * self.c m1 = matrice_carree_2 (1.0,1e-6,1e-6,1.0) m2 = matrice_carree_2 (1.0,1e-9,1e-9,1.0) print m1.determinant () print m2.determinant () | Qu’affichent les deux dernières lignes? 3)On considère la matrice. On pose D = det(M) = 1?10?18 et T = tr(M) = 2. ? est le déterminant de M et T sa trace. On sait que les valeurs propres de M notées ?1,?2 vérifient : D = ?1?2 T = ?1 + ?2 On vérifie que (x ? ?1)(x ? ?2) = x2 ? x(?1 + ?2) + ?1?2. Les valeurs propres de M sont donc solutions de l’équation : x2 ? Tx + D = 0. Le discriminant de ce polynôme est ? = T2?4D. On peut donc exprimer les valeurs propres de la matrice M par : On ajoute donc la méthode suivante à la classe matrice_carree_2 : D’après la précédente question, que retourne cette méthode pour la matrice M ? (à justifier) 4)On décompose la matrice. On peut démontrer que si ? est une valeur propre de M0, alors 1+? est une valeur propre de M. Que donne le calcul des valeurs propres de M0 si on utilise la méthode valeurs_propres pour ces deux matrices? 5)On considère maintenant la matrice. En décomposant la matrice M00 de la même manière qu’à la question 4, quelles sont les valeurs propres retournées par le programme pour la matrice M00 ? Quelles sont ses vraies valeurs propres? Correction L’exercice a pour but de montrer que l’ordinateur ne fait que des calculs approchés et que la précision du résultat dépend de la méthode numérique employée. 1)Le programme montre que l’ordinateur affiche 1 lorsqu’il calcule 1 ? 10?17. Cela signifie que la précision des calculs en Python est au mieux de 10?16. 2)Il s’agit ici de donner le résultat que calcule l’ordinateur et non le résultat théorique qui sera toujours exact. On cherche à calculer ici le déterminant des matrices M1 et M2 définies par : Or, le programme proposé calcule les déterminants comme suit : detM1 = 1 ? 10?12 et detM2 = 1 ? 10?18 D’après les affichages du programme de la question 1, le programme de la question 2 donnera comme réponse : 0.99999999999900002212 1.00000000000000000000 La seconde valeur est donc fausse. 3)Le déterminant est utilisé pour calculer les valeurs propres d’une matrice. Cette question s’intéresse à la répercussion de l’approximation faite pour le déterminant sur les valeurs propres. D’après l’énoncé, les deux valeurs propres sont calculées comme étant : l1 | = 0.5 * (trace - ((trace ** 2 - 4 * det) ** (0.5)) ) | l2 | = 0.5 * (trace + ((trace ** 2 - 4 * det) ** (0.5)) ) l1 = 1,000001 l2 = 0.99999899999999997124 # égale à 1 - 1e-6 Pour la matrice M2, le déterminant vaut 1. En remplaçant trace par 2 et det par 1, on obtient : 4)On change la méthode de calcul pour la matrice M2, on écrit que M2 = I + . Cette fois-ci le déterminant calculé par Python est bien 1e ? 18. La trace de la matrice est nulle, on applique les formules suivantes à la matrice pour trouver : l1 | = 0.5 * (trace - ((trace ** 2 - 4 * det) ** (0.5)) ) = - det ** 0.5 = -1e-9 | l2 | = 0.5 * (trace + ((trace ** 2 - 4 * det) ** (0.5)) ) = det ** 0.5 = 1e-9 | D’après l’énoncé, les valeurs propres de la matrice M2 sont les sommes de celles de la matrice I et de la matrice. Par conséquent, ce second calcul mène au résultat suivant : l1 | = 1-1e-9 = 0.99999999900000002828 | l2 | = 1+ 1e-9 = 1.000000001 | 5)La matrice M00 n’est en fait pas diagonalisable, c’est-à-dire que tr (M00)2 ? 4 ? detM00 = 4 ? 4(1 + 10?18) < 0. Or le calcul proposé par la question 3 aboutit au même résultat faux que pour la matrice M2, les deux valeurs propres trouvées seront égales à 1. Si on applique la décomposition de la question 4 : Le programme calcule sans erreur le discriminant négatif de la matrice N00 qui n’est pas diagonalisable. Il est donc impossible d’obtenir des valeurs propres réelles pour la matrice M00 avec cette seconde méthode. Cette question montre qu’une erreur d’approximation peut rendre une matrice diagonalisable alors qu’elle ne l’est pas. Il est possible d’accroître la précision des calculs mais il faut faire appel à des modules externes . 12.2 Second énoncé 12.2.1 Logarithme en base deux ** Correction L’indication qui préconisait d’utiliser autre chose qu’une boucle for ne voulait pas dire ne pas utiliser de boucle du tout mais une boucle while. La majorité des élèves ont réussi à trouver la fonction suivante : def fonction_log2 (k) : n = 0 while 2**n < k : n += 1 return n | Même s’il est possible d’utiliser malgré tout une boucle for : def fonction_log2 (k) : for i in range (0,1000) : if 2**i >= k : return i | Voici un exemple de fonction récursive : def fonction_log2 (k) : if k <= 1 : return 0 else : return fonction_log2 ((k+1)/2)+1 | 12.2.2 Calculer le résultat d’un programme ** On définit la fonction suivante : def parcours (n) : i = 1 j = 1 while i+j < n : print (i,j) i += 1 j -= 1 if j < 1 : j = i+j i = 1 | Quelles sont les 6 lignes qu’affiche cette fonction si n = 5? Correction La solution est : Et si on continue : (1,1) (1,2) (2,1) (1,3) (2,2) (3,1) | | (1,4) (2,3) (3,2) (4,1) (1,5) (2,4) (3,3) | Il fallait voir deux choses importantes dans l’énoncé de l’exercice, tout d’abord, les deux lignes suivantes : Elles signifient que lorsque i augmente de 1, j diminue de 1. L’autre information est que j ne devient jamais inférieur à 1 : Et lorsque ce cas arrive, i devient égal à 1, j devient égale à i+j, soit i+1 puisque la condition est vérifiée lorsque j == 1. Ce programme propose une façon de parcourir l’ensemble des nombres rationnels de la forme. 12.2.3 Calculer le résultat d’un programme ** Que vaut n à la fin du programme suivant? Il n’est en principe pas nécessaire d’aller jusqu’à i = 10. def suite (n) : n = n ** 2 / 2 + 1 return n n = suite (n) % 17 print n | Correction Il est difficile d’arriver au bout des calculs correctement lors de cet exercice. La première chose à faire est d’identifier la relation entre n et n + 1. On rappelle que le symbole % désigne le reste d’une division entière et que / désigne une division entière car tous les nombres manipulés dans ce programme sont entiers. La fonction suite aboutit à désigne la partie entière de x. Ajouté aux trois dernières lignes, on obtient : %17 Même si i n’est pas utilisé dans la boucle for, celle-ci s’exécute quand même 10 fois. Pour trouver la valeur finale de n, on calcule donc les premiers termes comme suit : i | 0 | 1 | 2 | 3 | 4 | ui = n | 3 | 5 | 13 | 0 | 1 | n2 | 9 | 25 | 169 | 0 | 1 | | 4 | 12 | 85 | 1 | 1 | ui+1 = y%17 | 5 | 13 | 0 | 1 | 1 | A partir de i = 3, la suite reste constante et égale à 1. Il n’est pas nécessaire d’aller plus loin. 12.2.4 Comprendre une erreur d’exécution * Le programme suivant est incorrect. def compte_lettre (s) : nombre = {} for c in s : nombre [c] += 1 return nombre print compte_lettre ( "mysteres" ) | Il retourne l’erreur suivante : 1)Quelle est la cause de l’erreur? 2)Quelle est la correction à apporter pour que cette fonction compte les lettres d’un mot? 3)Qu’afficherait le programme une fois corrigé? 4)La fonction compte_lettre est-elle réservée aux chaînes de caractères? Voyezvous d’autres types de données auxquels elle pourrait s’appliquer? Correction 1)L’erreur provient de la ligne nombre [c] += 1 pour c =0 m0, cette ligne équivaut à nombre[c] = nombre[c] + 1. Cela signifie que nombre[c] doit exister avant l’exécution de la ligne, c’est-à-dire que le dictionnaire nombre doit avoir une valeur associée à la clé c =0 m0, ce qui n’est pas le cas ici. 2)3)La solution compte le nombre d’occurrences de chaque lettre dans le mot s. def compteur (s) : nombre = {} for c in s : nombre [c] = 0 for c in s : nombre [c] += 1 return nombre | # ligne ajoutée | Ou encore : def compteur (s) : nombre = {} for c in s : if c not in nombre : nombre [c] = 0 nombre [c] += 1 return nombre | # ligne ajoutée | Ces deux programmes retournent : {’e’: 2, ’m’: 1, ’s’: 2, ’r’: 1, ’t’: 1, ’y’: 1} La dernière solution pour ceux qui n’aiment pas les dictionnaires : def compteur (s) : alpha = "abcdefghijklmnopqrstuvwxyz" nombre = [ 0 for c in alpha ] for c in s : i = alpha.index (c) nombre [i] += 1 return nombre | Le programme retourne : [0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 2, 1, 0, 0, 0, 0, 1, 0] 4)La fonction compteur accepte d’autres types de données à condition que les deux lignes for c in s : et nombre[c]+=1 aient un sens et elles ne sont valables que si le paramètre s contient des éléments susceptibles de servir de clé pour le dictionnaire s, c’est-à-dire un autre dictionnaire, un tuple, une liste d’éléments de type immuable. Autrement dit, les lignes suivantes sont correctes : print compteur ( "mysteres" ) print compteur ( compteur ("mysteres") ) print compteur ( [0,1,1,4,-1, (6,0), 5.5, "ch"] ) print compteur ( { 1:1, 2:2, 1:[] } ) Mais pas celle-ci : print compteur ( [0, [0,0] ] ) Toutefois, cette dernière ligne est valide si la fonction compteur se contente seulement de compter le nombre d’éléments, c’est-à-dire la première solution citée aux questions 2) et 3). 12.2.5 Comprendre une erreur de logique *** On précise que l’instruction random.randint(0,1) retourne un nombre aléatoire choisi dans l’ensemble {0,1} avec des probabilités équivalentes ( et ). La fonction ligne_nulle doit compter le nombre de lignes nulles def ligne_nulle (mat) : nb = 0 for i in range (0, len (mat)) : lig = 0 for j in range (0, len (mat [i])) : if mat [i][j] > 0 : lig += 1 if lig == 0 : nb += 1 return nb matri = [ [ random.randint (0,1) for i in range (0,4) ] for j in range (0,20) ] print ligne_nulle (matri) | Après avoir exécuté le programme trois fois de suite, les résultats affichés sont successivement 15,19,17. Bien que l’exécution du programme ne provoque aucune erreur, le concepteur de la fonction s’interroge quand même sur ces résultats. 1)Sachant que chaque case de la matrice reçoit aléatoirement 0 ou 1, quelle est la probabilité qu’une ligne soit nulle? Quelle est la probabilité d’avoir 15 lignes nulles dans la matrice sachant que cette matrice a 20 lignes? Ces deux réponses peuvent être littérales. Donnez-vous raison à celui qui a écrit le programme (il pense s’être trompé)? 2)Si vous lui donnez raison, ce qui est fort probable, où serait son erreur? Correction 1)La probabilité d’avoir une ligne nulle est la probabilité d’avoir 4 zéros, c’est donc : Comment calculer la probabilité d’avoir 15 lignes nulles parmi 20? Cela revient à estimer la probabilité de tirer 15 fois sur 20 une boule blanche lors d’un tirage à remise sachant que dans l’urne, il y a 1 boule blanche et 15 boules noires. Le nombre de boules blanches tirées suit une loi binomiale de paramètre. On en déduit que : Cette probabilité est très faible, il est donc presque impossible d’obtenir trois fois de suite un nombre de lignes supérieur à 15. Le programme est sans aucun doute faux. def ligne_nulle (mat) : nb = 0 for i in range (0, len (mat)) : lig = 0 for j in range (0, len (mat [i])) : if mat [i][j] > 0 : lig += 1 if lig == 0 : nb += 1 return nb | # ligne décalée vers la gauche | 12.2.6 Récursivité * Le programme suivant provoque une erreur dont le message paraît sans fin. class erreur : def __init__ (self) : self.e = erreur () e = erreur () | Auriez-vous une explication pour ce qui suit? Traceback (most recent call last): File "", line 4, in ? e = erreur () File "", line 2, in __init__ self.e = erreur () File "", line 2, in __init__ self.e = erreur () File "", line 2, in __init__ self.e = erreur () | Correction L’erreur retournée est une erreur d’exécution et non une erreur de syntaxe. Cela veut dire que le programme de l’exercice est syntaxiquement correct. Il n’est donc pas possible de dire que l’attribut e n’est pas défini et de corriger le programme comme suit pour expliquer l’erreur : class erreur : def __init__ (self,e) : self.e = e | En fait, la classe erreur définit un attribut qui est également de type erreur. Cet attribut va lui aussi définir un attribut de type erreur. Ce schéma va se reproduire à l’infini puisqu’à aucun moment, le code du programme ne prévoit la possibilité de s’arrêter. Le programme crée donc des instances de la classe erreur à l’infini jusqu’à atteindre une certaine limite dépendant du langage Python. C’est à ce moment-là que se produit l’erreur citée dans l’énoncé. 12.2.7 Compléter un programme ** def base3 (n) : s = "" while n > 0 : r = n % 3 # . à compléter # . à compléter return s | Correction Il y a effectivement deux lignes à corriger. r désigne le reste de la division de n par 3. Il faut le convertir en chaîne de caractères et l’ajouter à gauche à la chaîne s. On passe au chiffre suivant en division n par 3 ce qui est rassurant puisque n va tendre vers zéro et le programme s’arrêter nécessairement au bout d’un moment. Pour vérifier que ce programme est correct, il suffit de l’appliquer à un nombre, voire appliquer le même algorithme mais en base 10 pour être vraiment sûr. def base3 (n) : s = "" while n > 0 : r = n % 3 s = str (r) + s n = n / 3 return s | # équivalent à n = (n-r) / 3 # puisque / est une division entière | 12.2.8 Comprendre une erreur de logique *** Un professeur désireux de tester une répartition aléatoire des notes à un examen décide de tirer au hasard les notes de ses élèves selon une loi normale de moyenne 15 et d’écart-type 3. Il arrondit ses notes à l’entier le plus proche en n’omettant pas de vérifier que ses notes sont bien dans l’intervalle [0,20]. On précise que l’instruction float(i) convertit un nombre i en nombre réel, cette conversion est utilisée pour être sûr que le résultat final sera bien réel et non le résultat d’opérations sur des entiers. Cette conversion intervient le plus souvent lors de divisions. import copy import math import random class Eleve : def __init__ (self, note) : = note e = Eleve (0) l = [] for i in range (0,81) : e.note = int (random.gauss (15, 3) + 0.5) # tirage aélatoire et arrondi if e.note >= 20 : e.note = 20 # pas de note au-dessus de 20 if e.note < 0 : e.note = 0 # pas de note négative l.append (e) moy = 0 var = 0 for e in l : moy += e.note moy = float (moy) / len (l) # les notes sont entières, for e in l : var += (e.note - moy) ** 2 var = ( float (var) ) / len (l) print "moyenne ", moy print "écart-type ", var | Il songe à vérifier néanmoins que la moyenne de ses notes arrondies est bien conforme à ce qu’il a échafaudé. moyenne 16.0 écart-type 0.0 La moyenne égale à 16 ne le perturbe guère, il se dit que l’arrondi a été plutôt généreux. Toutefois l’écart-type nul le laisse perplexe. 1)Que signifie un écart-type nul? Quelle est l’erreur du professeur? (Elle est située à l’intérieur de la boucle foriinrange(0,81) :, ce n’est pas une erreur lors du calcul de l’écart-type ni une erreur de définition de la classe Eleve.) 2)Proposer deux solutions pour corriger ce problème, chacune d’elles revient à remplacer la ligne l.append(e). 3)On sait que la variance d’une variable aléatoire X vérifie : [?(X)]2. Cette astuce mathématique permet-elle de réduire le nombre de boucles du programme, si oui, comment? Correction 1)Un écart-type nul signifie que toutes les notes sont identiques et égales à la moyenne. Selon le programme, tous les élèves ont donc 16. L’erreur provient du fait que l’instruction l.append(e) ajoute à chaque fois la même variable de type Eleve. A la fin de la boucle, la liste l contient 81 fois le même objet Eleve ou plus exactement 81 fois la même instance de la classe Eleve. On modifie la note de cet unique objet en écrivant : e.note = int(random.gauss(15,3) + 0.5). 16 est donc la note attribuée au dernier élève, la dernière note tirée aléatoirement. 2)Les deux corrections possibles consistent à créer à chaque itération une nouvelle instance de la classe Eleve. 1. l.append(e) devient l.append(Eleve(e.note)) 2. l.append(e) devient l.append(copy.copy(e)) Une troisième solution est envisageable même si elle introduit des modifications dans la suite du programme, elle est logiquement correcte : l.append(e) devient l.append(e.note). La liste l n’est plus une liste de Eleve mais une liste d’entiers. Le programme devient : # e = Eleve (0) l = [] for i in range (0,81) : e.note = int (random.gauss (15, 3) + 0.5) if e.note >= 20 : e.note = 20 if e.note < 0 : e.note = 0 l.append (e.note) | # ligne modifiée | moy = 0 var = 0 for note in l : | # ligne modifiée | moy += note | # ligne modifiée | moy = float (moy) / len (l) for note in l : | # ligne modifiée | var += (note - moy) ** 2 var = ( float (var) ) / len (l) print "moyenne ", moy print "écart-type ", var | # ligne modifiée | 3)La formule mathématique permet de réduire le nombre de boucles à deux. Lors de la version initiale du programme, la première sert à créer la liste l, la seconde à calculer la moyenne, la troisième à calculer la variance. On regroupe les deux dernières boucles en une seule. moy = 0 var = 0 for note in l : moy += note var += note * note moy = float (moy) / len (l) var = float (var) / len (l) var = var - moy * moy var = ( float (var) ) print "moyenne ", moy print "écart-type ", var | 12.3 Troisème énoncé 12.3.1 Calcul d’un arrondi * Ecrire une fonction qui arrondit un nombre réel à 0.5 près? Ecrire une autre fonction qui arrondit à 0.125 près? Ecrire une fonction qui arrondit à r près où r est un réel? Répondre uniquement à cette dernière question suffit pour répondre aux trois questions. Correction Pour arrondi à 1 près, il suffit de prendre la partie entière de x + 0.5, arrondir x à 0.5 près revient à arrondi 2x à 1 près. def arrondi_05 (x) : return float (int (x * 2 + 0.5)) / 2 def arrondi_0125 (x) : return float (int (x * 8 + 0.5)) / 8 def arrondi (x, p) : | 12.3.2 Même programme avec 1,2,3 boucles ** Le programme suivant affiche toutes les listes de trois entiers, chaque entier étant compris entre 0 et 9. for a in range (0, 10) : for b in range (0, 10) : for c in range (0, 10) : print [a,b,c] | 1)Proposez une solution avec une boucle while et deux tests if. 2)Proposez une solution avec deux boucles while. Correction 1) Une seule boucle contrôle les indices a, b, c. Quand un indice atteint sa limite, on incrémente le suivant et on remet l’indice à 0. a,b,c = 0,0,0 while c < 10 : print [a,b,c] a += 1 if a == 10 : b += 1 a = 0 if b == 10 : c += 1 b = 1 | 2)L’avantage de cette dernière solution est qu’elle ne dépend pas du nombre d’indices. C’est cette solution qu’il faut préconiser pour écrire une fonction dont le code est adapté quelque soit la valeur de n. l = [0,0,0] while l [-1] < 10 : print l l [0] += 1 i = 0 while i < len (l)-1 and l [i] == 10 : l [i] = 0 l [i+1] += 1 i += 1 | Ce problème était mal posé : il n’est pas difficile d’introduire des tests ou des boucles redondantes ou d’enlever une des conditions d’une boucle while pour la remplacer un test relié à une sortie de la boucle. 12.3.3 Suite récurrente (Fibonacci) ** La fonction fibo retourne la valeur de la suite de Fibonacci pour tout entier n. Quel est son coût en fonction de O(n)? def fibo (n) : if n <= 2 : return 2 else : return fibo (n-1) + fibo (n-2) | Correction Lorsqu’on cherche à calculer fibo(n), on calcule fibo(n ? 1) et fibo(n ? 2) : le coût du calcul fibo(n) est égal à la somme des coûts des calculs de fibo(n ? 1) et fibo(n ? 2) plus une addition et un test. Le coût de la fonction fibo(n) est plus facile à définir par récurrence. Le coût cn du calcul de fibo(n) vérifie donc : c0 = c1 = c2 = 1 | (12.1) | cn = cn?1 + cn?2 + 2 | (12.2) nb = 0 # variable globale def fibo (n,p) : global nb if n <= 2 : nb += 1 return p # plus de récurrence else : nb += 2 return fibo (n-1,p) + fibo (n-2,p) for n in range (1, 20) : nb = 0 # remis à zéro, à chaque fois # nb est la mesure du coût print fibo(n,3)-2, nb # nombres identiques # nb vérifie la récurrence de la suite c(n) # c(n) = c(n-1) + c(n-2) + 2 | suites de la forme (12.3) : u0 = p u1 = p u2 = u0 + u1 + d = 2p + d u3 = u1 + u2 + d = 3p + 2d u4 = u2 + u3 + d = 5p + 4d u5 = u3 + u4 + d = 8p + 7d u6 = u4 + u5 + d = 13p + 12d u7 = u5 + u6 + d = 21p + 20d u8 = u6 + u7 + d = 34p + 33d Dans cet exemple, on s’aperçoit que la suite est égale à son coût. Pour le démontrer d’une façon plus théorique, on s’intéresse aux suites de la forme suivante dont la récurrence est développée ci-dessus à droite : u0 = u1 = u2 = p un = un?1 + un?2 + d (12.3) Par une astuce de calcul, on peut réduire l’écriture de la suite (un) à une somme linéaire de deux suites (Un) et (Vn). La suite (Un) est définie par U0 = 1, U1 = 1, Un = Un?1+Un?2 et la suite (Vn) définie par V0 = 0, V1 = 0, Vn = Vn?1+Vn?2+1. On en déduit que : un = Unp + Vnd Si on arrive à montrer que Un = Vn + 1, cela montrera que la fonction fibo citée dans la correction est bien égale à son coût. Or : Vn + 1 = Vn?1 + Vn?2 + 1 + 1 = (Vn?1 + 1) + (Vn?2 + 1) La suite suit bien la récurrence de la suite Un. On vérifie que les premières valeurs sont identiques et cela montre que Un = Vn + 1. On en déduit que : un = Unp + (Un ? 1)d = Un(p + d) ? d Pour conclure, une suite de Fibonacci vérifie la récurrence un = un?1 + un?2. Si on pose et . Cette suite peut s’écrire sous la forme un = 12.3.4 Calculer le résultat d’un programme * 1)Qu’affiche le code suivant : l = [0,1,2,3,4,5] g = l for i in range (0, len (l)-1) : g [i] = g [i+1] print l print g | 2)Et celui-ci : l = [0,1,2,3,4,5] g = [0,1,2,3,4,5] for i in range (0, len (l)-1) : g [i] = g [i+1] print l print g | 3)Et encore celui-là : l = [0,1,2,3,4,5] g = [0,1,2,3,4,5] for i in range (0, len (l)) : g [i] = g [(i+1)%len (l)] print l print g 4)A votre avis, quel est l’objectif du programme et que suggérez-vous d’écrire? 5)Voyez-vous un moyen d’écrire plus simplement la seconde ligne g = [0,1,2,3,4,5] tout en laissant inchangé le résultat? Correction 1)L’instruction g = l implique que ces deux variables désignent la même liste. La boucle décale les nombres vers la gauche. [1, 2, 3, 4, 5, 5] [1, 2, 3, 4, 5, 5] 2)L’instruction g = [0,1,2,3,4,5] implique que ces deux variables ne désignent plus la même liste. L’instruction printl affiche le contenu du début. [0, 1, 2, 3, 4, 5] [1, 2, 3, 4, 5, 5] 3)La boucle s’intéresse cette fois-ci au déplacement du premier élément en première position. Le programmeur a pris soin d’utiliser le modulo, n%n = 0 mais le premier élément de la liste g est devenu le second. Le résultat est donc : [0, 1, 2, 3, 4, 5] [1, 2, 3, 4, 5, 1] 4)Le programme souhaite décaler les éléments d’une liste vers la gauche, le premier élément devenant le dernier. l = [0,1,2,3,4,5] g = [0,1,2,3,4,5] for i in range (0, len (l)) : g [i] = l [(i+1)%len (l)] # ligne modifiée, g devient l print l print g | 5)La seconde ligne impose de répéter le contenu de la liste. Il existe une fonction qui permet de le faire : import copy l = [0,1,2,3,4,5] g = (l) # on pourrait aussi écrire g = list (l) # ou encore g = [ i for i in l ] for i in range (0, len (l)) : g [i] = l [(i+1)%len (l)] print l print g 1)Un individu a écrit la fonction suivante, il inclut un seul commentaire et il faut deviner ce qu’elle fait. Après exécution, le programme affiche 4. def mystere (l) : """cette fonction s’applique à des listes de nombres""" l.sort () nb = 0 for i in range (1,len (l)) : if l [i-1] != l [i] : nb += 1 return nb+1 l = [4,3,1,2,3,4] print mystere (l) # affiche 4 | 2)Après avoir écrit l’instruction printl, on s’aperçoit que la liste l a été modifiée par la fonction mystere. Qu’affiche cette instruction? 3)Comment corriger le programme pour que la liste ne soit pas modifiée? Correction 1)La fonction travaille sur une liste de nombres triés. La fonction parcourt cette liste et compte le nombre de fois que la liste triée contient deux éléments successifs différents. La fonction retourne donc le nombre d’éléments distincts d’une liste. 2)La fonction trie la liste, elle ressort donc triée de la fonction car le passage des listes à une fonction s’effectue par adresse. Le résultat de l’instruction printl est donc : 3)Il suffit d’utiliser le module copy. import copy def mystere (l) : """cette fonction s’applique à des listes de nombres""" l = (l) # ligne insérée # on peut écrire aussi l = list (l) l.sort () nb = 0 for i in range (1,len (l)) : if l [i-1] != l [i] : nb += 1 return nb+1 | 12.3.6 Héritage ** class Personne : def __init__ (self, nom) : = nom def entete (self) : return "" def __str__ (self) : s = self.entete () + return s class Homme (Personne) : def __init__ (self, nom) : Personne.__init__ (self, nom) def entete (self) : return "M. " class Femme (Personne) : def __init__ (self, nom) : Personne.__init__ (self, nom) def entete (self) : return "Melle " h = Homme ("Hector") f = Femme ("Gertrude") print h print f | | class Personne : def __init__ (self, nom, entete) : = nom self.entete = entete s = self.entete + return s h = Personne ("Hector", "M. ") f = Personne ("Gertrude", "Melle ") print h print f | Programme A Programme B 1)Les deux programmes précédents affichent-ils les mêmes choses? Qu’affichentils? 2)On souhaite ajouter une personne hermaphrodite, comment modifier chacun des deux programmes pour prendre en compte ce cas? 3)Quel programme conseilleriez-vous à quelqu’un qui doit manipuler cent millions de personnes et qui a peur de manquer de mémoire? Justifiez. Correction 1)Les programmes affichent la même chose et ils affichent : 2) class Personne : def __init__ (self, nom) : = nom def entete (self) : return "" def __str__ (self) : s = self.entete () + return s class Homme (Personne) : def __init__ (self, nom) : Personne.__init__ (self, nom) def entete (self) : return "M. " class Femme (Personne) : def __init__ (self, nom) : Personne.__init__ (self, nom) def entete (self) : return "Melle " class Hermaphrodite (Personne) : def __init__ (self, nom) : Personne.__init__ (self, nom) def entete (self) : return "Melle et M. " h = Homme ("Hector") f = Femme ("Gertrude") g = Hermaphrodite ("Marie-Jean") print h print f print g | | class Personne : def __init__ (self, nom, entete) : = nom self.entete = entete def __str__ (self) : s = self.entete + return s h = Personne ("Hector", "M. ") f = Personne ("Gertrude", "Melle ") g = Personne ("Marie-Jean", \ "Melle et M. ") print h print f print g 3)Les deux programmes définissent une personne avec un prénom et un en-tête. Dans le second programme, l’en-tête est un attribut de la classe et cette classe permet de modéliser les hommes et les femmes. Dans le premier programme, l’entête est défini par le type de la classe utilisée. Il y a moins d’information mémorisée, en contre partie, il n’est pas possible d’avoir d’autres en-têtes que M. et Melle. Pour manipuler 100 millions de personnes, il vaut mieux utiliser le premier programme avec deux classes, il sera moins gourmant en mémoire. 12.3.7 Analyser un programme *** On considère la fonction suivante qui prend comme entrée une liste d’entiers. Elle ne retourne pas de résultat car elle modifie la liste. Appeler cette fonction modifie la liste donnée comme paramètre. Elle est composée de quatre groupes d’instructions. def tri_entiers(l): """cette fonction s’applique à une liste d’entiers""" # groupe 1 | m = l [0] M = l [0] for k in range(1,len(l)): if l [k] < m : m = l [k] if l [k] > M : M = l [k] # groupe 2 p = [0 for i in range (m,M+1) ] for i in range (0, len (l)) : p [ l [i] - m ] += 1 # groupe 3 R = [0 for i in range (m,M+1) ] R [0] = p [0] for k in range (1, len (p)) : R [k] = R [k-1] + p [k] # groupe 4 pos = 0 for i in range (1, len (l)) : while R [pos] < i : pos += 1 l [i-1] = pos + m l [len (l)-1] = M | 1)Que fait le groupe 1? 2)Que fait le groupe 2? 3)Que fait le groupe 3? 4)Que fait le groupe 4? 5)Quel est la boucle contenant le plus grand nombre d’itérations? Correction 1)Le groupe 1 détermine les minimum et maximum de la liste l. 2)Dans le groupe 2, la liste p compte le nombre d’occurrences d’un élément dans la liste l. p[0] correspond au nombre de fois que le minimum est présent dans la liste, p[len(l) ? 1] correspond au nombre de fois que le maximum est présent. 4)Le dernier groupe trie la liste l. Si X est une variable aléatoire de fonction de répartition F alors F(X) suit une loi uniforme sur [0,1]. C’est le même principe ici. La figure 12.1 illustre la fonction de répartition. A chaque marche correspond un élément de la liste et à chaque marche correspond une case du tableau R. En parcourant les marches du tableau R, on retrouve les éléments de la liste l triés. La marche plus haute traite le cas de plusieurs éléments égaux. 5)Le groupe 1 inclut une boucle de n itérations où n est le nombre d’éléments de la liste l. Les groupes 2 et 3 incluent une boucle de M ? n itérations. Le groupe 4 inclut deux boucles, la première inclut n itérations, la seconde implique une variable pos qui passe de m à M. Le coût de ce bloc est en O(n + M ? n). C’est ce dernier bloc le plus long. 12.3.8 Comprendre une erreur de logique *** Chaque année, une société reçoit des cadeaux de ses clients et son patron souhaite organiser une tombola pour attribuer ces cadeaux de sorte que les plus bas salaires aient plus de chance de recevoir un cadeau. Il a donc choisi la méthode suivante : – Chaque employé considère son salaire net par mois et le divise par 1000, il obtient un nombre entier ni. – Dans une urne, on place des boules numérotées que l’on tire avec remise. – Le premier employé dont la boule est sortie autant de fois que ni gagne le lot. La société a distribué plus de 100 cadeaux en quelques années mais jamais le patron n’en a gagné un seul, son salaire n’est pourtant que cinq fois celui de sa sécrétaire. Son système est-il équitable? Il fait donc une simulation sur 1000 cadeaux en Python que voici. Sa société emploie quatre personnes dont les salaires sont [2000,3000,5000,10000]. import random return i salaire = [ 10000, 5000, 3000, 2000 ] poids = [ int (s / 1000) for s in salaire ] nombre = [ 0 for s in salaire ] for n in range (0,1000) : p = tirage (poids) nombre [p] += 1 for i in range (0, len (poids)) : print "salaire ", salaire [i], " : nb : ", nombre [i] | Les résultats sont plus que probants : salaire 10000 : nb : 0 salaire 5000 : nb : 49 salaire 3000 : nb : 301 salaire 2000 : nb : 650 Il n’a aucune chance de gagner à moins que son programme soit faux. Ne s’en sortant plus, il décide d’engager un expert car il trouve particulièrement injuste que sa secrétaire ait encore gagné cette dernière bouteille de vin. 1)Avez-vous suffisamment confiance en vos connaissances en Python et en probabilités pour déclarer le programme correct ou incorrect? Deux ou trois mots d’explications seraient les bienvenus. 2)Le patron, après une folle nuit de réflexion, décida de se débarrasser de son expert perdu dans des probabilités compliquées et modifia son programme pour obtenir le suivant : import random def tirage (poids, nb) : while True : i = random.randint (0, len (poids)-1) nb [i] += 1 if nb [i] % poids [i] == 0 : return i salaire = [ 10000, 5000, 3000, 2000 ] poids = [ int (s / 1000) for s in salaire ] nombre = [ 0 for s in salaire ] temp = [ 0 for s in salaire ] for n in range (0,1000) : p = tirage (poids, temp) nombre [p] += 1 for i in range (0, len (poids)) : print "salaire ", salaire [i], " : nb : ", nombre [i] | Et le résultat est beaucoup plus conforme à ses attentes : salaire 10000 : nb : 90 salaire 5000 : nb : 178 salaire 3000 : nb : 303 salaire 2000 : nb : 429 Quelle est la modification dans les règles qu’il a apportée? Vous paraissent-elles justes? Correction Si on revient au jeu à quatre personnes et les salaires de l’énoncé, la probabilité de gagner du patron est cette fois de 4?10, celle de sa sécrétaire est au moins supérieure à qui correspond à sa probabilité de gagner en deux tirages. Si le patron ne gagne pas, c’est que sa probabilité de gagner est beaucoup trop petite par rapport au nombre de tirages. Il en faudrait au moins 4?10 pour avoir une probabilité non négligeable que le patron gagne au moins une fois. 2)90?5 = 450 qui n’est pas très loin de 429 ou encore 178/2?5 = 445. La secrétaire paraît avoir 5 fois plus de chances de gagner que son patron et 5/2 fois plus que la personne payée 5000 euros. La fonction tirage reçoit un nouvel élément nb qui est un compteur. Le programme ne remet plus à zéro ce compteur entre deux tirages. Cela signifie que si la boule du patron est sortie trois fois alors que la secrétaire a gagné. Lors du prochain tirage, le compteur du patron partira de 3 et celui de sa secrétaire de 0. Si on ne remet jamais les compteurs à zéro, au bout de 1000 tirages (et non 1000 cadeaux), la boule du patron est sortie environ autant de fois que celle de la secrétaire, soit 250 fois puisqu’il y a quatre employés. La secrétaire aura gagné = 125 fois, le patron aura gagné = 25 soit 5 fois moins . 12.4 Quatrième énoncé 12.4.1 Suite récurrente (Fibonacci) * Réécrire la fonction u de façon à ce qu’elle ne soit plus récurrente. def u (n) : if n <= 2 : return 1 else : return u (n-1) + u (n-2) + u (n-3) | Correction def u_non_recursif (n) : if n <= 2 : return 1 u0 = 1 u1 = 1 u2 = 1 i = 3 while i <= n : u = u0 + u1 + u2 u0 = u1 u1 = u2 u2 = u i += 1 return u | 12.4.2 Calculer le résultat d’un programme * 1)Qu’affiche le programme suivant : def fonction (n) : return n + (n % 2) print fonction (10) print fonction (11) 3)Ecrire une fonction qui retourne le premier multiple de 3 supérieur à n? Correction 1)La fonction fonction ajoute 1 si n est impair, 0 sinon. Le programme affiche : 2)La fonction retourne le plus petit entier pair supérieur ou égal à n. 3)On cherche à retourner le plus petit multiple de 3 supérieur ou égal à n. Tout d’abord, on remarque que n + (n%3) n’est pas la réponse cherchée car 4 + 4%3 = 5 qui n’est pas divisible par 3. Les fonctions les plus simples sont les suivantes : def fonction3 (n) : k = 0 while k < n : k += 3 return k | Ou encore : def fonction3 (n) : if n % 3 == 0 : return n elif n % 3 == 1 : return n + 2 else : return n + 1 | Ou encore : def fonction3 (n) : if n % 3 == 0 : return n else : return n + 3 - (n % 3) | 12.4.3 Calculer le résultat d’un programme * 1)Qu’affiche le programme suivant : def division (n) : return n / 2 print division (1) print division (0.9) 2)Proposez une solution pour que le résultat de la fonction soit 0.5 lors d’une instruction printdivision ? Correction 1)1/2 est égal à zéro en Python car c’est une division de deux entiers et le résultat est égal à la partie entière. La seconde division est une division entre un réel et un entier, le résultat est réel. Le programme affiche : 2)Voici deux solutions printdivision(1.0) ou printdivision(float(1)). Il est également possible de convertir n à l’intérieur de la fonction division. 12.4.4 Ecrire un programme à partir d’un algorithme ** On considère deux listes d’entiers. La première est inférieure à la seconde si l’une des deux conditions suivantes est vérifiée : – les n premiers nombres sont égaux mais la première liste ne contient que n éléments tandis que la seconde est plus longue Par conséquent, si l est la longueur de la liste la plus courte, comparer ces deux listes d’entiers revient à parcourir tous les indices depuis 0 jusqu’à l exclu et à s’arrêter sur la première différence qui détermine le résultat. S’il n’y pas de différence, alors la liste la plus courte est la première. Il faut écrire une fonction compare_liste(p,q) qui implémente cet algorithme. Correction Cet algorithme de comparaison est en fait celui utilisé pour comparer deux chaînes de caractères. def compare_liste (p,q) : i = 0 while i < len (p) and i < len (q) : if p [i] < q [i] : return -1 # on peut décider elif p [i] > q [i] : return 1 # on peut décider i += 1 # on ne peut pas décider # fin de la boucle, il faut décider à partir des longueurs des listes if len (p) < len (q) : return -1 elif len (p) > len (q) : return 1 else : return 0 | On pourrait également écrire cette fonction avec la fonction cmp qui permet de comparer deux éléments quels qu’ils soient. def compare_liste (p,q) : i = 0 while i < len (p) and i < len (q) : c = cmp (p [i], q [i]) if c != 0 : return c # on peut décider i += 1 # on ne peut pas décider # fin de la boucle, il faut décider à partir des longueurs des listes return cmp (len (p), len (q)) | 12.4.5 Comprendre une erreur d’exécution * Le programme suivant est erroné. l = [0,1,2,3,4,5,6,7,8,9] i = 1 while i < len (l) : | Il affiche des résultats puis une erreur. 1 2 3 4 5 6 7 8 9 Traceback (most recent call last): File "", line 43, in <module> print l [i], l [i+1] IndexError: list index out of range | Pourquoi? Correction Le programme affiche les nombres par groupes de deux nombres consécutifs. Une itération de la boucle commence si la liste contient un élément suivant et non deux. Le programme est donc contraint à l’erreur car lors de la dernière itération, la liste contient un dixième nombre mais non un onzième. Le programme affiche le dixième élément (9) puis provoque une erreur listindexoutofrange. 12.4.6 Précision des calculs ** On cherche à calculer la somme des termes d’une suite géométriques de raison . On définit, on cherche donc à calculer qui une somme convergente mais infinie. Le programme suivant permet d’en calculer une valeur approchée. Il retourne, outre le résultat, le nombre d’itérations qui ont permis d’estimer le résultat. def suite_geometrique_1 (r) : x = 1.0 y = 0.0 n = 0 while x > 0 : y += x x *= r n += 1 return y,n print suite_geometrique_1 (0.5) #affiche (2.0, 1075) Un informaticien plus expérimenté a écrit le programme suivant qui retourne le même résultat mais avec un nombre d’itérations beaucoup plus petit. def suite_geometrique_2 (r) : x = 1.0 y = 0.0 n = 0 yold = y + 1 while abs (yold - y) > 0 : yold = y y += x x *= r n += 1 return y,n print suite_geometrique_2 (0.5) #affiche (2.0, 55) | Expliquez pourquoi le second programme est plus rapide tout en retournant le même résultat. Repère numérique : 2?55 ? 2,8.10?17. Correction Toutefois, il n’est pas indispensable d’aller aussi loin car l’ordinateur n’est de toute façon pas capable d’ajouter un nombre aussi petit à un nombre plus grand que 1. Par exemple, 1 + 1017 = 1,00000000000000001. Comme la précision des calculs n’est que de 15 chiffres, pour Python, 1 + 1017 = 1. Le second programme s’inspire de cette remarque : le calcul s’arrête lorsque le résultat de la somme n’évolue plus car il additionne des nombres trop petits à un nombre trop grand. L’idée est donc de comparer la somme d’une itération à l’autre et de s’arrêter lorsqu’elle n’évolue plus. Ce raisonnement n’est pas toujours applicable. Il est valide dans ce cas car la série est croissante et positive. Il est valide pour une série convergente de la forme et une suite un de module décroissant. 12.4.7 Analyser un programme ** Un chercheur cherche à vérifier qu’une suite de 0 et de 1 est aléatoire. Pour cela, il souhaite compter le nombre de séquences de n nombres successifs. Par exemple, pour la suite 01100111 et n = 3, les triplets sont 011, 110, 100, 001, 011, 111. Le triplet 011 apparaît deux fois, les autres une fois. Si la suite est aléatoire, les occurrences de chaque triplet sont en nombres équivalents. Le chercheur souhaite également faire varier n et calculer les fréquences des triplets, quadruplets, quintuplets, Pour compter ses occurrences, il hésite entre deux structures, la première à base de listes (à déconseiller) : def hyper_cube_liste (n, m = [0,0]) : if n > 1 : m [0] = [0,0] m [1] = [0,0] m [0] = hyper_cube_liste (n-1, m [0]) m [1] = hyper_cube_liste (n-1, m [1]) return m h = hyper_cube_liste (3) print h # affiche [[[0, 0], [0, 0]], [[0, 0], [0, 0]]] | La seconde à base de dictionnaire (plus facile à manipuler) : ind = [ 0 for i in range (0,n) ] while ind [0] <= 1 : cle = tuple ( ind ) # conversion d’une liste en tuple r [cle] = 0 ind [ len (ind)-1] += 1 k = len (ind)-1 while ind [ k ] == 2 and k > 0 : ind [k] = 0 ind [k-1] += 1 k -= 1 return r h = hyper_cube_dico (3) print h # affiche {(0, 1, 1): 0, (1, 1, 0): 0, (1, 0, 0): 0, (0, 0, 1): 0, # (1, 0, 1): 0, (0, 0, 0): 0, (0, 1, 0): 0, (1, 1, 1): 0} | Le chercheur a commencé à écrire son programme : def occurrence (l,n) : d = . # choix d’un hyper_cube (n) .. return d suite = [ 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1 ] h = occurrence (suite, 3) print h | Sur quelle structure se porte votre choix (a priori celle avec dictionnaire), compléter la fonction occurrence. Correction Tout d’abord, la structure matricielle est à déconseiller fortement même si un exemple d’utilisation en sera donné. La solution avec dictionnaire est assez simple : def occurrence (l,n) : d = hyper_cube_dico (n) for i in range (0, len (l)-n) : cle = tuple (l [i:i+n]) d [cle] += 1 return d | Il est même possible de se passer de la fonction hyper_cube_dico : def occurrence (l,n) : d = { } for i in range (0, len (l)-n) : cle = tuple (l [i:i+n]) if cle not in d : d [cle] = 0 d [cle] += 1 return d | La seule différence apparaît lorsqu’un n-uplet n’apparaît pas dans la liste. Avec la fonction hyper_cube_dico, ce n-uplet recevra la fréquence 0, sans cette fonction, ce n-uplet ne sera pas présent dans le dictionnaire d. Le même programme avec la structure matricielle est plus une curiosité qu’un cas utile. def occurrence (l,n) : d = hyper_cube_liste (n, [0,0]) | # * remarque voir plus bas cle = l [i:i+n] t = d | # | for k in range (0,n-1) : | # point clé de la fonction : | t = t [ cle [k] ] t [cle [ n-1] ] += 1 return d | # accès à un élément | Si on remplace la ligne marquée d’une étoile par d = hyper_cube_list(n), le programme retourne une erreur : Traceback (most recent call last): File "", line 166, in <module> h = occurrence (suite, n) File "", line 160, in occurrence t [cle [ n-1] ] += 1 TypeError: ’int’ object is not iterable | Cette erreur est assez incompréhensible puisque la modification a consisté à appeler une fonction avec un paramètre de moins qui était de toutes façons égal à la valeur par défaut associée au paramètre. Pour comprendre cette erreur, il faut exécuter le programme suivant : def fonction (l = [0,0]) : l [0] += 1 return l print fonction () # affiche [1,0] : résultat attendu print fonction () # affiche [2,0] : résultat surprenant print fonction ( [0,0]) # affiche [1,0] : résultat attendu | L’explication provient du fait que la valeur par défaut est une liste qui n’est pas recréée à chaque appel mais c’est la même liste à chaque fois que la fonction est appelée sans paramètre. Pour remédier à cela, il faudrait écrire : import copy def fonction (l = [0,0]) : l = (l) l [0] += 1 return l Dans le cas de l’hypercube, il faudrait écrire : def hyper_cube_liste (n, m = [0,0]) : m = (m) if n > 1 : m [0] = [0,0] m [1] = [0,0] m [0] = hyper_cube_liste (n-1, m [0]) m [1] = hyper_cube_liste (n-1, m [1]) return m | 12.5 Exercices supplémentaires 12.5.1 Position initiale d’un élément après un tri ** Correction L’idée est de ne pas trier la liste mais une liste de couples incluant chaque élément avec sa position. On suppose que la liste l existe. La méthode sort utilisera le premier élément de chaque couple pour tier la liste de couples. l2 = [ ( l [i], i ) for i in range (0, len (l)) ] () print l2 [0][1] # affiche la position du plus petit élément # dans le tableau initial | 12.5.2 Comprendre une erreur de logique * 1)Quel est le résultat affiché par le programme suivant : def ensemble_lettre (s) : ens = [] for i in range (0, len (s)) : c = s [i] if c in ens : ens.append (c) return ens print lettre ("baaa") | Est-ce que ce résultat change si on appelle la fonction ensemble_lettre avec un autre mot? 2)Le programme précédent n’est vraisemblablement pas fidèle aux intentions de son auteur. Celui-ci avait pour objectif de déterminer l’ensemble des lettres différentes de la chaîne de caractères passée en entrée. Que faut-il faire pour le corriger? Que sera le résultat de l’instruction ensemble_lettre(”baaa”) en tenant compte de la modification suggérée? Correction 1)L’instruction ifcinens : signifie que le caractère c est ajouté seulement s’il est déjà présent dans la liste s qui est vide au début de la fonction. Elle sera donc toujours vide à la fin de l’exécution de la fonction et sera vide quelque soit le mot s fourni en entrée comme paramètre. Le résultat ne dépend donc pas du mot. 2)Il suffit de changer ifcinens : en ifcnot inens : pour donner : def ensemble_lettre (s) : ens = [] for i in range (0, len (s)) : c = s [i] if c not in ens : ens.append (c) return ens | Le résultat pour le mot ”baaa” est [”b”,”a”]. 12.5.3 Comprendre une erreur d’exécution * k = [10,14,15,-1,6] l = [] for i in range (0,len (k)) : l.append ( k [ len (k) - i ] ) Correction Lors du premier passage dans la boucle for, le premier ajouté dans la liste l est k[len(k)] qui n’existent pas puisque les indices vont de 0 à len(k). Voici la correction : k = [10,14,15,-1,6] l = [] for i in range (0,len (k)) : l.append ( k [ len (k) - i-1 ] ) # -1 a été ajouté | 12.5.4 Précision des calculs *** On cherche à calculer ln2 grâce à la formule . Pour x = 1, cette suite est convergente et permet de calculer ln2. 1)On propose deux programmes. Le premier utilise une fonction puiss. Le second ne l’utilise pas : def puiss (x, n) : s = 1.0 for i in range (0,n) : s *= x return s def log_suite (x) : x = float (x-1) # s = 0 old = -1 n = 1 while abs (old - s) > 1e-10 : old = s po = puiss (x,n) / n if n % 2 == 0 : po = -po s += po n += 1 # return s print log_suite (2) | | # # # # # # def log_suite (x) : x = float (x-1) x0 = x s = 0 old = -1 n = 1 while abs (old - s) > 1e-10 : old = s po = x / n if n % 2 == 0 : po = -po s += po n += 1 x *= x0 return s print log_suite (2) | Quel est le programme le plus rapide et pourquoi? Quels sont les coûts des deux algorithmes? 2)Combien faut-il d’itérations pour que la fonction retourne un résultat? ? 3)On introduit la fonction racine_carree qui calcule k. Celle-ci est issue de la résolution de l’équation f(x) = 0 avec f(x) = x2 ? k à l’aide de la méthode de Newton . def racine_carree (k) : x0 = float (k)+1 x = float (k) while abs (x-x0) > 1e-10 : x0 = x x = (k-x*x) / (2 * x) + x return x | ? Cette fonction retourne un résultat pour 2 en 6 itérations. On décompose ensuite . En utilisant cette astuce et 4)Que proposez-vous pour calculer ln100? Correction 1)Le second programme est le plus rapide. A chaque boucle du premier programme, la fonction puiss calcule xn avec n multiplications, ce calcul est remplacé par une seule multiplication dans le second programme. Les deux programmes exécuteront exactement le même nombre de fois la boucle while. Soit n ce nombre d’itérations, le coût du premier programme est 1 + 2 + 3 + 4 + + n ? O(n2) car la fonction puiss fait i passages dans sa boucle for pour l’itération i. Le coût du second programme est en O(n). 2)Si on pose , la condition d’arrêt de la fonction log_suite correspond à : Comme on calcule ln(1 + 1), x = 1, la condition est donc équivalente à : La fonction log_suite fait dix milliards de passages dans la boucle while, dix milliards d’itérations. ? 3)Cette fois-ci, on cherche à calculer , d’où x = 2 ? 1. La condition d’arrêt est toujourset on cherche à majorer le nombre ? d’itérations nécessaires pour que la fonction converge vers un résultat. 2 ? 1.414 < . On majore donc x par : On utilise l’approximation donnée par l’énoncé à savoir 210n ? 103n. ? Si on ajoute 34 aux 6 itérations nécessaires pour calculer 2, on trouve 40 itérations. 4)ln(1 + x) n’est calculable que pour |x| < 1. Il faut fonc transformer ln100 pour le calculer à partir d’une racine carrée et de ln(1 + x) pour un x tel que x < 1 (dans le cas contraire, la suite ne converge pas) : C’est une décomposition, ce n’est pas la seule possible.
Index False ..35 None .. 33, 62 True 35 __builtins__ 55 __class__ 132 __doc__ 55 __file__ ..55 __init__.py ..155, 165 __main__ .152 __name__ ..55 A accent 22, 192 affectation .. 117 multiple .56, 67 aide ..75, 101 algorithme 11 analyse discriminante linéaire (ADL, FDA) ..254 analyse en composantes principales (ACP) .253 architecture .166 arrondi . 34, 302, 303, 313 ascii .. 192 asynchrone ..225 attribut __bases__ 100, 127 __class__ . 100 __dict__ . 99, 100, 122, 128 __doc__ .100 __module__ 100 __name__ 100 __slots__ . 122 statique ..110, 122 attributs croissants 130 B boucle .45, 63 break .68 continue 68 fin normale d’une boucle 69 infinie .64 itération suivante . 68 sortie prématurée . 69 byte .. 178 bytecode 12, 151 C C, C++ . 158 calcul distribué 225 parallèle 225 précision .. 290, 315, 320 call stack ..139, 298 carré magique ..254 chaîne de caractères 36 concaténation . 37, 47 conversion ..37, 54 encodage .. 191 formatage 39 manipulation 37 préfixe r . 37 préfixe u 192 chemin relatif, absolu ..153, 176 classe .. 93 ancêtre 127, 130 attribut .. 93, 96, 128 attribut implicite 100 classe incluse . 102 commentaire ..101 compilation 133 constructeur . 98, 115 copie 114, 116, 119 copie profonde 119 dériver ..125 destructeur 99, 115 héritage ..122, 288 héritage multiple .130 instance ..94 itérateur 106 liste des attributs . 99 méthode 93, 95 opérateur ..103 propriété 112 sous-classe .102 surcharge . 105, 126, 266 classes . 12 clé .. 49 commentaire ..32, 75, 101 compilation . 133 constructeur . 98 conteneur . 42 contrôle (graphique) . 195 conversion 34, 37, 54, 280 copie 117, 119 copie profonde . 119 coût d’un algorithme 247, 287, 288 crible d’Eratosthène 68 CSV .. 173 D dames 242 datamining 25 date de naissance . 191 dates . 191 debug ..165, 288 débugger 19, 165 deprecated 58 dériver 125 Design Patterns 136 dichotomie . 89, 270 dictionnaire clé .49 clé modifiable ..52 copie ..52 exemple ..51 valeur .49 division entière ..34 Dynamic Link Library (DLL) . 159 E efficacité . 242 encodage .191 énoncé apprentissage . 238, 242, 245, 254, brut . 211 exception 44, 45, 138, 139, 166, 246 héritage .143 imbrication 144 message ..142, 143 piège 148 exercice arrondi 302, 313 boucles . 302 calcul . 281, 294 classe . 298, 300 comparaison .. 314 comptage 295, 306, 316, 319 copie ..284, 300 coût d’un algorithme 287 dictionnaire .. 283, 295, 316 division .313 écriture en base 3 299 Fibonacci . 282, 303, 312 grenouille ..282 héritage ..288, 307 liste . 305 logarithme .. 293, 320 matrice . 290 moyenne . 287, 300 parcours .293, 302, 315, 320 précision des calculs290, 315, 320 probabilité ..297, 310 somme de chiffres 280 suite récurrente281, 282, 303, 312 suppression dans une boucle . 285 tri . 280, 286 tri d’entiers 308 tri, position 319 valeurs propres 290 variance ..287, 300 expressions régulières 184 assemblage .188 caractères spéciaux . 187 ensemble de caractères .186 exemple 189 groupe 186, 187 multiplicateurs 187 F FAQ 19 Fibonacci . 303, 304 fichier .169 print 171 arborescence ..175 binaire ..178 buffer 171 chemin absolu 176 copie 175 écriture, ajout 171 fermeture 170, 172 fin de ligne .173 logs ..172 ouverture 170, 172 sérialisation 181 suppression 175 tampon .171 trace .172 zip 174 fil d’exécution 225, 226 finance .24 focus ..210 fonction 71 help ..75 id . 53 len ..37, 41, 50 map .. 86, 90 max,min . 88 max 41 min 41 range .65 raw_input . 61 sum 90 type . 33, 55 xrange .. 65 zip 67 aide 75 générateurs . 83 imbriquée 80 locale . 80 portée 80 récursive .. 78, 177 spéciale .137 surcharge 75 usuelle 86 fonction de répartition .. 309 format binaire ..178 CSV .173 texte .169 zip 174 forum de discussion .. 194 G garbage collector..117, 148, 157, 159, 164 générateurs .. 83 graphe 261 H héritage ..122 multiple 130 sens . 129 histogramme 309 HTML .. 25, 267 I identificateur 31, 77 immuable . 32 indentation .. 13, 21, 59, 63, 64, 85 instance interprété . 12 intersection .. 56 itérateur ..66, 106, 146 J Java .. 158 jeu de caractères .. 191 accents 22 ascii . 192 cp1252 . 23, 192 latin-1 . 22, 192 unicode .192 utf-8 . 23, 192, 193 jeu de dames 242 L langage de programmation C, C++ 13, 158 Java 13, 157 R ..25 VBA .166 Latex ..25, 263, 268 ligne de commande 182 liste chaînée .. 42 collage 47 copie ..47 insertion . 42 suppression . 42 tri . 42 valeur par défaut 318 liste de listes . 92 logs . 166, 172 M machine learning 25 mail .. 175 Strassen 135 maximum .88 méthode ajout 111 commentaire ..101 constructeur 98 destructeur . 99 statique .108 surcharge .. 126 minimum . 88 modifiable 32, 41 module .. 150 import . 150 __init__.py .155, 165 arborescence ..155 chemin relatif .153 emplacement ..153 externe . 156 identificateur . 151 nom .. 150, 152 paquetage . 155 répertoire ..153 module externe GMPy .293 matplotlib .249, 269 mdp ..253 mutagen . 175, 189 psyco .27 pydot 262 pyparsing .262 reportlab .169 win32com ..169 wxPython ..194 module interne calendar ..191 codecs . 192 copy .. 48, 52, 114, 116, 119, 121, 285, 301, 307, 318 datetime 167, 191 getopt . 184 glob ..175, 182 HTMLParser 173 imp ..165 keyword . 58 os.path 153, 175, 176 os 175–177, 184 pickle 167, 181 Queue 234 random . 297 re . 185, 239 shutil .175, 176, 182 string . 238 struct 178, 179 sys 154, 183 threading ..226, 229 time ..227, 228 Tix 202, 218 Tkinter .. 26, 194 urllib 156, 246 xml.sax .173 zipfile 174 montant numérique, littéral 238 Monte Carlo 156 MP3 ..189 multicœurs ..225 N nœud . 261 Newton .. 321 nombre entier . 33 premier .. 68 réel 33 O objet 12, 93, 195 obsolète 58 octet ..178 opérateur comparaison 35 priorité ..34, 61 ordinateur 10 orienté objet . 12 outil Adobe Reader .268 Boost Python . 158 Boost 158 Excel . 170, 173 Graphviz .. 262 Jython ..158 Latex 268 Microsoft Visual Studio C++ 159 Mozilla Firefox .. 267 Standard Template Library (STL) . 158 TeXnicCenter 269 wiki .. 29 WinZip .174 P paquetage 155 parallélisation ..225 paramètre .71 immuable 76 modifiable .. 76 nombre variable 81 ordre . 74 passage par référence .76 passage par valeur 76 valeur par défaut ..73 valeur par défaut, modifiable . 73, 318 patron de classe 158 PDF ..268 permutations 259 pièce normale .123 très truquée 125 truquée .123 pièce jointe . 175 pile d’appels . 139, 298 pixel ..204 point d’entrée . 150, 152, 194 pointeur . 164 pointeur d’arrêt 165 port .. 225 portable . 158 portée fonctions .80 variables .79 précision des calculs . 290, 315, 320 prédécesseur 261 priorité des opérateurs 34 probabilité ..297 processeur .. 225 programmation objet ..93 programmes, exemples Button . 196 Canvas . 203 CheckButton . 199 compile .. 85, 133 copy 284, 285, 306, 307, 318 DISABLED ..196 Entry 197 eval . 54, 84 Frame 208 grid .207 id . 53 Label 195 Listbox 201 Listbox réagissant au curseur220 mainloop ..209 map 86 Menu .214 pack .206 RadioButton . 200 reload . 151 set 56 sys.path .. 153 Text .198 yield ..83, 107 zip 86 __name__ 152 __slots__ . 122 Boost Python . 168 ACP .253 arrondi . 302 attendre un thread ..228 barre de défilement . 202 barycentre . 102 boucle .. 281 bouton image .197 carré magique ..256–258 carré magique et compteur 259 carré magique et permutations 259 clés croissantes .65 comparaison .. 314 comptage . 91, 296 compteur .. 303 concaténation ..39 constructeur .. 132 coordonnées polaires . 72 copie48, 52, 74, 114, 117, 118, 120 damier 243, 244 date de naissance 191 dates 191 décomposition en base 3 .. 299 destructeur 115 deux threads secondaires ..227 dictionnaire .51 écriture condensée 46 Eratosthène .68 exception 139–141, 144–146 exemple de module . 150 exercice pour s’évaluer..271, 274, factorielle 78 FDA .254 Fibonacci . 303, 304, 312 fichier .171–173 fichier ouvert et exception .148 fichier zip ..174 fichiers binaires 179, 180 fonction isinstance 133 formatage de chaîne de caractères ..39 graphe avec matplotlib .249 grenouille 282, 283 héritage .134 héritage multiple .131 hypercube 316–318 import d’une DLL .. 165 import de module . 150–152 import dynamique de module 153 installation d’un module .. 157 intégrale de Monte Carlo ..156 interface 195 itérateur 66, 106, 146 jeu de caractères .. 192, 193 joueurs asynchrones .234 lancer un navigateur en ligne de commande 184 lancer un programme Python en ligne de commande 183 lecture d’un fichier texte .. 246 lecture d’une page HTML .156 ligne de commande 183, 184 liste ComboBox 203 liste avec barre de défilement.202 liste de fichiers .177, 182 liste des modules .154 logarithme .321 matrice 89, 92, 136, 170, 171, 173 matrice, lignes nulles 298 maximum .. 88, 89 méthode paramètre d’une fonction .. 135 méthode statique ..108, 109 module pickle et classes181, 182 moyenne, variance .. 287 opérateur ..105 opérateur addition 103, 104 partager des données 229 pièce ..123, 125 plusieurs fenêtre Toplevel 217 premier thread 226 propriété 112 racine carrée ..321 recherche 88 recherche dichotomique . 89 récupération d’un texte sur Internet .. 246 référencement d’objets . 115 sélection d’un fichier 218 séquence aléatoire .99 sérialisation 181 somme .31, 45, 58, 90 sortie HTML . 267 sortie PDF .268 suppression . 70 suppression dans une liste .286 test 61 thread et interface graphique.232 Tkinter avec des classes 221 Tkinter, bouton ..210 Tkinter, compte à rebours 219 Tkinter, fonction bind . 212 Tkinter, menu 215 Tkinter, séquence d’événements .. 223 tombola ..310, 311 tri .43, 90 tri avec positions initiales .. 91 tri d’entiers 308 tri rapide 264, 265 tri, position 319 personnalisés 224 vecteur .. 92 version graphique de la fonction raw_input 62 propriété ..112, 113 propriétés croissantes 130 py .. 21 pyc 151 Q quicksort .261 R racine carrée 321 rapport 25 recherche dichotomique . 270 récurrence .. 293 récursivité . 78, 281, 282, 312 condition d’arrêt ..78 référence circulaire . 121, 181 release 288 remarque affectation et copie ..117 ambiguïté ..110 associer un événement à tous les objets . 213 blocage d’un programme ..231 caractères spéciaux et expressions régulières . 186 code de fin de ligne . 173 commentaires .. 32 constructeur et fonction .98 conversion en nombre entier 34 coût d’un algorithme 287 déclaration d’une méthode statique .. 109 désactiver un événement .. 214 dictionnaire et clé de type tuple .. 244 division entière .34 écriture différée .. 171 événement associé à une méthode .. 210 événement spécifique 214 fenêtre intempestive ..16 fichier de traces ..172 fichiers ouverts et non fermés 170 focus 213 héritage multiple et constructeur .. 131 indentation . 59 instruction sur plusieurs lignes 32 liste et barre de défilement 202 majuscules et minuscules ..175 matrice et dictionnaire .244 mauvais événement . 213 Method resolution order .. 127 méthodes et fonctions 96 modification d’un dictionnaire dans une boucle . 52 nombres français et anglais 173 Nommer des groupes 190 paramètres contenant des espaces .. 184 passage par adresse .. 77 plusieurs erreurs . 140 plusieurs Listbox .202 préfixe r, chaîne de caractères . 36 priorité des opérations .. 61 propriété et héritage 113 recharger un module 151 réduction des accès à quelques threads 231 sortie texte et graphique 25 suppression de variables associées à la même instance 115 surcharge d’attributs 128 syntaxe et espaces 58 T-uple, opérateur [] .. 41 template C++ 162 type d’une instance .. 94 de source . 159 valeurs par défaut de type modifiable . 73 répertoire 175 représentation des données . 242 résultat d’une fonction 71 S SciTe .. 18 sérialisation .. 121, 167 somme .90 sortie graphique .25 sous-programme .71 Strassen . 135 successeur 261 surcharge .75, 105, 126 attribut .128 fonction 126 opérateur ..105 synchronisation 225, 227, 229 syntaxe 58 copy .114 deepcopy ..119 def ..71, 73, 81 for .. 64, 65 ifelifelse ..59, 61 ifelse ..59 issubclass 132 lambda .. 82 range .45 staticmethod 108 while .63 __add__ .103 __copy__ 116 __deepcopy__ .119 __iadd__ 104 __init__ .98 __str__ .105 ajout de méthode 111 appel à une méthode de l’ancêtre .. 128 attribut figé 122 attribut statique . 110 chaîne de caractères, formatage39 chaîne de caractères, formatage des nombres .. 40 classe, appel d’une méthode de l’ancêtre ..128 classe, attribut statique 110 classe, attribut, définition .. 96 classe, constructeur .. 98 classe, création d’une variable de type objet . 94 classe, définition .. 93 classe, héritage 126 classe, instance .98 classe, méthode statique .. 108 classe, méthode, appel .. 95 classe, méthode, définition . 95 classe, propriété ..112 constructeur 98 destructeur . 99 espace 58 exception .. 140 exception d’un type donné 142 exception, lancement 143 fonction, appel .72 fonction, définition 71 fonction, nombre variable de paramètres 81 fonction, nombre variable de paramètres (appel) ..81 fonction, ordre des paramètres 74 fonction, valeur par défaut . 73 fonctions et chaînes de caractères .37 formatage d’une chaîne de caractères ..39 formatage des nombres ..40 héritage .126 instance ..98 méthode . 37 méthode statique 108 propriété 112 test 59 test condensé .. 61 test enchaîné 59 T tabulations 21 taille mémoire 179, 181 template .158 Tkinter barre de défilement . 202 boucle de message .. 209 bouton ..196 bouton radio ..200 canevas .203 case à cocher . 199 case ronde . 200 compte à rebours 219 contrôle .195 événement, association .211 événement, désactivation ..214 fenêtre ..217 focus 206, 210, 213 liste . 201 menu 214 objets 195 positionnement d’objet .206 réactivité .. 225 sous-fenêtre 208 tabulation . 210 thread 225, 231 touche tabulation 206 touches muettes ..211 widget ..195 zone de saisie . 197 zone de saisie à plusieurs lignes .. 198 zone de texte . 196 traces .172 tri .. 90 entiers ..308 liste 42 position initiale 91 quicksort .. 261 typage dynamique ..13 type 31 bool ..35 complex . 41 dict . 49, 118, 295 Exception .140 False,True .35 float .33 frozenset ..56 int 33 list .. 42, 65, 118 long ..56 None . 33, 71 set 56 string .. 36 tuple .40 chaîne de caractères ..36 complexe 41 dictionnaire 49, 118, 242 ensemble .56 entier . 33 fonction ..92 immuable .. 32, 76 immutable .. 32 liste . 42, 65, 118, 242 modifiable 32, 42, 76, 93 mutable ..32 objet ..94 réel 33 rien 33 types fondamentaux 31 U unicode .. 192 union .. 56 utf-8 . 23, 192 V valeur ..49 valeur par défaut 73 objet modifiable . 318 valeur par défaut modifiable 318 van Rossum, Guido 12 variable globale . 79, 95, 96 identificateur ..31, 79 locale . 79 portée 79 statique ..227, 229 type .. 31 vecteur . 41, 92, 130 version, debug, release .165, 288 vitesse d’exécution 157 W widget 195 wiki .29
. Ecole Nationale de la Statistique et de l’Administration Economique, . L’ensemble de ces exemples est accessible depuis le site . . Un kilo-octets équivaut à 1024 = 210 octets, un Mo à 1024 Ko et un Go à 1024 Mo. . Voir le site , cette licence autorise les usages commerciaux. . Ou GNU Public Licence (voir le site ). [12] . Celui-ci est appelé bytecode. Ce mot ne signifie pas grand chose excepté que ce bytecode est seulement compréhensible par la machine virtuelle Python. . Ce point est d’ailleurs abordé au paragraphe 6.5, page 158. . Cette version est disponible à l’adresse . C’est la version utilisée pour développer les exemples présents dans ce livre. . La requête python français de nombreux sites de documentation sur Python en français. [18] . Les variables sont définies au chapitre 2. En résumé, on peut considérer une variable comme une lettre, un mot qui désigne une information. Dans cet exemple, les trois variables désignent des informations numériques qu’on peut manipuler comme si elles étaient effectivement des nombres. Les variables supprimées, les informations qu’elles désignaient ne plus accessibles et sont perdues également. [19] . Cet éditeur est accessible depuis l’adresse et téléchargeable depuis l’adresse . [20] . Il arrive fréquemment que des universités, des sociétés interdisent à leurs utilisateurs l’installation de nouveaux logiciels sur leurs machines pour des raisons de sécurité. Il faut des droits spécifiques pour contourner cette interdiction. . L’usage de la commande dos2unix peut se révéler utile. . Voir le paragraphe 1.6.5 sur les interfaces graphiques. . plus précisément à l’adresse ou encore à l’adresse . Cette extension est installée automatiquement avec Python contrairement à une extension externe. [34] . L’instruction help() saisie sur la ligne de commande Python retourne entre autres le numéro de version. . Avec la fonction fetch_historical_yahoo. [40] . Accessibles depuis l’adresse , ces informations sont disponibles depuis la page de téléchargement de biggles. . Il est facile de trouver des pages Internet décrivant cette syntaxe via un moteur de recherche. L’exercice 7, page 267 en dira plus à ce sujet. . voir l’exercice 7, page 268 . , ce compilateur présent sur toute machine Linux est porté également sur Microsoft Windows (voir le site ). . puis saisir HTML Help Workshop dans la barre de recherche . Les tests de non-régression ont pour objectif de limiter le nombre d’erreurs dans un programme. Pour une fonction retournant le résultat d’un calcul scientifique, un test de non-régression consiste à vérifier que ce calcul, pour des données bien précises, aboutit toujours au même résultat malgré les modifications apportées au programme. La détection d’une erreur débouche souvent sur un test de non-régression afin de s’assurer qu’elle ne se reproduira pas. . Les fonctions sont définies au paragraphe 3.4, plus simplement, ce sont des mini-programmes : elles permettent de découper un programme long en tâches plus petites. On les distingue des variables car leur nom est suivi d’une liste de constantes ou variables comprises entre parenthèses et séparées par une virgule. [98] . La page #operator-summary est plus complète à ce sujet. . voir également le paragraphe 3.7.1, page 88 . La différence dépend des opérations effectuées. [105] . La page décrit l’ensemble des fonctionnalités qui leur sont attachées. [106] . Les modules seront décrits au chapitre 6. Dans ce cas-ci, un module est une librairie de fonctionnalités. La syntaxe keyword.iskeyword permet d’exécuter la fonctionnalité iskeyword du module keyword. . L’instruction break est décrite au paragraphe 3.3.4.2, page 68. [108] . Le crible d’Eratosthène est un algorithme permettant de déterminer les nombres premiers. Pour un nombre premier p, il paraît plus simple de considérer tous les entiers de p ? 1 à 1 pour savoir si l’un d’eux divise p. C’est ce qu’on fait lorsqu’on doit vérifier le caractère premier d’un seul nombre. Pour plusieurs nombres à la fois, le crible d’Eratosthène est plus efficace : au lieu de s’intéresser aux diviseurs, on s’intéresse aux multiples d’un nombre. Pour un nombre i, on sait que 2i, 3i, ne sont pas premiers. On les raye de la liste. On continue avec i + 1, 2(i + 1), 3(i + 1) . voir les fonctions au paragraphe 3.4 [111] . Pour les utilisateurs experts : en langage Python, les fonctions sont également des variables, elles ont un identificateur et une valeur qui est dans ce cas un morceau de code. Cette précision explique certaines syntaxes du chapitre 8 sur les interfaces graphiques ou celle introduite en fin de chapitre au paragraphe 3.7.10. . Les matrices sont définies dans une extension externe numpy (voir le chapitre 6). . voir exercice 10.2, page 242 . L’algorithme de multiplication de deux matrices de Strassen est plus rapide qu’une multiplication classique pour de grandes matrices. Son coût est en au lieu de O(n3). . grâce à la fonction exec [120] . La page recense tous les modules disponibles avec Python. [121] . Ceci n’est pas tout-à-fait vrai pour la version 3.0 du langage dont les changements sont trop conséquents. Un changement du premier numéro de version indique souvent des changements majeurs. [124] . Il faut tenir compte dans le module en C des opérations nécessaires au garbage collector. [130] . Les template ou patron de classe sont un concept de métaprogrammation. Lorsque deux classes sont presque identiques à l’exception d’un type, le langage C++ autorise la création d’un patron unique pour ces deux classes. Le C++ est à typage statique : il faut écrire deux classes pour créer un tableau d’entiers et un tableau de réels même si le code est le même. Les template [132] . C’est facultatif mais il est également conseillé d’écrire d’écrire un code pour tester cette fonction dans le fichier exeSample/source/main.cpp. Cela réduit le risque d’erreurs d’exécution dans cette partie. [137] . D’autres formats proposent de meilleurs taux de compressions sur les fichiers textes existent, voir . [140] . Ceci explique que certains programmes aient des comportements différents selon le système d’exploitation sur lequel ils sont exécutés ou encore que certains liens Internet vers des fichiers ne débouchent sur rien car ils ont été saisis avec des différences au niveau des minuscules majuscules. . Un fichier texte compressé, au format zip par exemple, est une alternative aux fichiers binaires en terme de taille mais il allonge la lecture et l’écriture par des étapes de compression et de décompression. . La liste complète figure à l’adresse . . voir également paragraphe 3.4.10, page 81 [145] . Cette règle est toujours vrai sur des ordinateurs 32 bits. Cette taille varie sur les ordinateurs 64 bits qui commencent à se répandre. Le programme suivant donnera la bonne réponse. fromstructimportpack printlen(pack(0i0,0)) # longueur d’un entier printlen(pack(0d0,0)) # longueur d’un réel printlen(pack(0c0,”e”)) # longueur d’un caractère [146] . La description du langage HTML sort du cadre de ce livre mais la requête HTMLsyntaxe effectuée sur n’importe quel moteur de recherche Internet retournera des résultats intéressants. . La page décrit la syntaxe, la page #regex-howto décrit comment se servir des expressions régulières, les deux pages sont en anglais. . La page les recense tous. [149] . Ces caractères sont spéciaux, les plus utilisés sont \t qui est une tabulation, \n qui est une fin de ligne et qui \r qui est un retour à la ligne. . La page #regular-expression-syntax les recense tous. . Voir également la page . [153] . On confond souvent jeu de caractères et encodage. Le jeu de caractère désigne l’ensemble de caractères dont le programme a besoin, l’encodage décrit la manière dont on passe d’une séquence de caractères français, japonais, anglais à une séquence d’octets qui est la seule information manipulée par un ordinateur. . Le paragraphe 1.6.5 page 26 présente d’autres alternatives. [155] . L’adresse illustre chaque objet de Tkinter, on peut citer également . On les appelle aussi contrôle. Comme ils reçoivent des événements, en un sens, ce sont ces objets qui pilotent un programme ou qui le contrôlent. . La page recense quelques liens utiles autour de Tkinter dont la documentation officielle. . La "disparition" d’un objet est évoquée au paragraphe 8.3. . Affichée avec l’instruction help(Tkinter.Label.__init__). . Celles marquées d’une étoile (?). . Les symboles << et >> au début et à la fin du nom de l’événement sont la seule contrainte. [167] . Il a l’inconvénient de ne pas supporter les accents français mais il est possible de contourner cet obstacle en associant MatPlotLib et Latex qui permet également d’afficher des formules ma- . voir paragraphe 7.3.3 page 177 . voir paragraphe 7.6 page 184 . voir le livre Probabilités, analyse des données et statistique de Gilbert Saporta (éditions Technip). [173] . Lorsqu’on parle de coût moyen, cela signifie que le coût n’est pas constant en fonction de la dimension du problème. Ici, le coût moyen désigne le coût moyen d’un tri quicksort obtenu en faisant la moyenne du coût du même algorithme sur toutes les permutations possibles de l’ensemble de départ. [177] . Latex est un langage adapté aux publications scientifiques, également à l’écriture de livres comme celui-ci. En utilisant sa syntaxe, il permet de créer rapidement un fichier au format PDF. . , ce logiciel est volumineux et assez long à télécharger. [183] . On cherche la meilleure majoration du coût de la recherche non dichotomique en fonction de la taille n de la liste. [184] . Il faut télécharger ce fichier depuis l’adresse . Je cite ici la plus belle des réponses retournée par un élève : Les patrons qui organisent des tombolas sont toujours mariés à leur secrétaire donc la question ne se pose pas.
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | |