Algorithmique avancée en Python . . . et non pas Python avancé Denis Robilliard sept. 2014 1 Introduction Objectifs du cours • connaı̂tre un panel d’algorithmes standards : énumération, tris, backtracking, listes, etc; • avoir codé et compris ces algorithmes, sans utiliser de librairies ”toutes faites”; • consolider les bases de la conception d’applications dans le paradigme dit de ”programmation procédurale” avec analyse descendante. Avertissement Le langage support dans ce cours est le langage python 3. C’est cette version précise qui est supposée dans toute expression comme ”le langage python”, ou ”en python”, ... Attention la version 2 du langage est encore très répandue lors de la rédaction de ce cours, et n’est pas tout à fait compatible avec les codes donnés ici (quoique les modifications soient généralement mineures). 2 Les bases de Python Le plus important : le commentaire ’’’ ceci est un commentaire qui s ’ é tend sur plusieurs lignes ’ ’ ’ # ceci est un commentaire jusqu ’ à la fin de ligne Le commentaire sert à décrire l’algorithme en français (ou anglais, ou ...). Rappel : il existait des algorithmes avant les ordinateurs et le langage Python... L’affectation : (nom de) variable = expression a = 2+3 print ( a ) # affiche 5 à l ’ é cran Ici la variable a reçoit la valeur résultant du calcul de l’expression arithmétique. L’affectation peut, dans certains cas, être un peu plus compliquée que cela, nous le verrons plus loin. Entrée/sortie de base : ’’’ programme perroquet ’’’ a = input ( ’ entrez un message : ’) print ( a ) Attention, le message est mémorisé comme une chaı̂ne de caractères. Si vous voulez saisir un nombre pour faire des calculs, il faut une conversion : ’’’ programme addition ’’’ a = input ( ’ entrez un entier a : ’) a = int ( a ) # je remplace la cha ^ ı ne par un entier b = int ( input ( ’ entrez un entier b : ’)) print ( ’ a + b vaut : ’, a + b ) L’alternative : si condition alors action sinon autre action Attention, le test d’égalité se note == pour se différencier de l’affectation. a = 2+3 if a == 4: print ( ’ la variable a ’) print ( ’ contient la valeur 4 ’) else : ’’’ notez l ’ utilisation de " pour permettre d ’ inclure une apostrophe dans la cha ^ ı ne ’’’ print (" a n ’ est pas plus grand que 4") Notez l’indentation (décalage de l’alignement) du code des actions. Ce décalage doit être identique en nombre d’espace ou de tabulation pour chaque ligne de l’action dans tout votre code. Un alignement incorrect est une erreur pour Python : ’’’ ce code ne fonctionne pas ’’’ a = 2+3 if a > 4: print ( ’ a est plus grand que 4 ’) print ( ’ strictement plus grand , en fait ’) # mal indent é ! else : print (" a n ’ est pas plus grand que 4") Il n’y a pas forcément une partie sinon : ’’’ je ne m ’ int é resse qu ’ aux entiers pairs ’’’ a = int ( input ( ’ entrez un entier : ’)) if a % 2 == 0: print (" l ’ entier " , a , " est pair ") ’’’ à partir d ’ ici le reste de mon code ’’’ La boucle tant que : tant que condition faire action fintq ’’’ é num è re et affiche les entiers de 1 à 10 ’’’ i = 1 while i < 11: print ( i ) i += 1 # raccourci pour i = i + 1 ’’’ code hors boucle signal é par son indentation ’’’ print ( ’ termin é ’) La boucle pour : pour variable dans enumeration faire action finpour ’’’ é num è re et affiche les entiers de 1 à 10 ’’’ ’’’ range (a , b ) donne la s é quence des entiers de a à b ( b exclus ) ’’’ for i in range (1 ,11): print ( i ) ’’’ code hors boucle signal é par son indentation ’’’ print ( ’ termin é ’) Note : on ne doit pas changer la valeur de i dans les actions de la boucle. Le 2nd plus important : la fonction ’’’ retourne le max de ses 2 param è tres ( qui doivent ^ e tre comparables ) ’’’ def maximum (a , b ): if a > b : return a else : return b def main (): print ( maximum (2 ,12)) main () La fonction sert à organiser le code en petits blocs que l’on peut mettre au point indépendamment les uns des autres : la complexité du problème diminue. Note : le code d’une fonction peut aussi être appelé plusieurs fois, mais c’est un intérêt secondaire. Ne pas confondre retourner et afficher/”sortir à l’écran” La fonction du paragraphe précédent retourne une valeur dont on fait ce qu’on veut : on peut l’afficher bien sûr, mais aussi l’utiliser dans un calcul, la stocker en mémoire, etc. Dans l’exemple suivant la fonction affiche directement le résultat: ’’’ sortir à l ’ é cran le max de ses 2 param è tres ( qui doivent ^ e tre comparables ) ’’’ def maximum (a , b ): if a > b : print ( a ) else : print ( b ) def main (): maximum (2 ,12) main () Notez que cette fonction est plus limitée : on ne peut plus récupérer le résultat dans la fonction main() pour le stocker dans une variable. Par conséquent préférer la solution avec retour du résultat, sauf si le but de la fonction est spécifiquement d’afficher un message. 3 Types de données Les variables ne sont pas typées, mais par contre les données sont typées. ainsi les 3 affectations suivantes sont légales, mais l’opération qui suit est erronée : a a a a = = = = 2 # entier 3.14 # flottant ’ bonjour ’ # cha ^ ı ne a + 1 # erreur : pas d ’ op é ration + entre une cha ^ ı ne et un entier types de bases Les types de base classiques que l’on trouve dans la plupart des langages sont : • booléens / bool : True, False • entiers / int : 1 ; -12 • réels / float : 3.14 ; .4 ; 1.3E5 • chaı̂nes / str : ’bonjour’ 3.1 tableaux Les tableaux sont la représentation informatique de la notion de vecteurs et de matrices mathématiques. Les tableaux ne sont pas un type de base en python, mais comme c’est une structure de données très pratique, plusieurs modules ont été introduits pour les implanter, dont le module array, et le module numpy. C’est numpy que nous utiliserons. Ces tableaux seront limités à contenir des éléments de types numériques et booléens. Création de tableau Il faut importer le module numpy pour pouvoir l’utiliser. On crée un tableau sans modèle avec zeros ou ones, initialisant respectivement les éléments à 0 ou 1 (dans le cas booléen respectivement False ou True), ou encore empty qui crée un tableau non initialisé. import numpy # importer numpy tab = numpy . array ([1 ,2 ,3]) # à partir d ’ un expression mod è le autre_tab = numpy . array ( tab ) # cr é ation d ’ une copie de tab tab = numpy . empty (3 , dtype = int ) # tableau de 3 entiers non initialis é s tab = numpy . zeros (3 , dtype = int ) # tableau de 3 entiers initialis é s à 0 tab = numpy . ones (3 , dtype = int ) # tableau de 3 entiers initialis é s à 1 tab = numpy . zeros (3 , dtype = float ) # tableau de 3 r é els initialis é s à 0.0 t ab = numpy . zeros ((2 ,2) , dtype = int ) # matrice 2 x2 d ’ entiers tab = numpy . zeros ((3 ,5) , dtype = float ) # matrice 3 x5 de r é els tab = numpy . zeros ((2 ,2 ,2) , dtype = bool ) # cube 2 x2x2 de bool é ens à False Accès aux éléments de tableau Tous les tableaux sont indicés à partir de 0. import numpy # importer numpy tab = numpy . zeros (3 , dtype = int ) # tableau de 3 entiers , initialis é s à 0 tab [0] = 12 # acc è s au 1 er é l é ment tab [2] = -1 # acc è s au dernier é l é ment tab [1] = tab [0] # copie du 1 er dans le 2 è me tab_bool = numpy . zeros ((2 ,2 ,2) , dtype = bool ) tab_bool [1 ,1 ,1] = True tab_bool [1][1][1] = True # é criture alternative Attributs de tableaux La taille d’un tableau unidimensionnel peut être obtenue avec l’opérateur len, mais les tableaux numpy possèdent aussi un attribut size qui donne le nombre d’éléments notamment pour les tableaux multidimensionnels. Tranches de tableaux On peut manipuler directement un morceau de tableau où tous les éléments ont des indices successifs : une tranche (ou slice). tab = numpy . array ([1 ,2 ,3 ,4 ,5 ,6]) print ( tab [0:2]) # affiche les 2 premiers é l é ments : [1 2] print ( tab [:2]) # m ^ e me chose que le pr é c é dent print ( tab [3:]) # affiche depuis l ’ indice 3 jusque la fin du tableau tab [:3]= tab [3:] # copie les 3 derniers é l é ms dans les 3 premiers 3.2 Notion de type ”conteneur” Un tableau est un type ”conteneur” : il a un contenu (ses éléments) qui est modifiable. Par opposition; un entier n’a pas de contenu modifiable en python : il se comporte comme une valeur littérale (écrite en toutes lettres). Pour les types conteneurs, l’affectation prend un sens particulier : ce n’est pas une copie qui est créée, mais un alias (un synonyme). Si on veut une copie, il faut utiliser le constructeur numpy.array(tableau à copier) vu plus haut. tab = numpy . array ([1 ,2 ,3 ,4 ,5 ,6]) tab2 = tab # tab2 n ’ est PAS une copie de tab tab [0] = 10 # ATTENTION : modifie tab ET tab2 , qui sont le meme objet 3.3 Liste Python possède nativement un type liste, qui est un conteneur et qui se comporte comme un tableau avec des opérateurs plus souples (on peut insérer au milieu d’une liste, l’agrandir, ...). La manière dont les listes sont implantées rend moins efficace l’accès à un élément donné, mais plus efficace l’insertion et l’agrandissement. Par ailleurs les listes peuvent contenir des éléments de types différents, contrairement aux tableaux : l’utilisation des listes ou des tableaux dépend donc des besoins. L’opérateur len, l’indiçage par [] et les tranches fonctionnent aussi sur les liste. On accède au dernier élément par l’indice -1 (ce qui fonctionne aussi sur les tableau mais est peu portable dans les autres langages) Quelques uns des opérateurs spécifiques sont : • liste.append(truc) : ajoute truc en fin de liste • liste.extend(liste2) : concatène liste2 en fin de liste • liste.insert(indice, truc) : insère truc dans liste à l’indice donné (l’indice 0 insère en début de liste) • liste.remove(truc) : retire la 1ère occurrence de truc trouvée dans la liste (erreur si non trouvé) • liste.pop(indice) : retire et retourne l’élément à l’indice donné (si pas d’indice fourni retire le dernier élem). • liste.index(truc) : donne l’indice de la 1ère occurrrence de truc dans la liste l = [] # cr é ation d ’ une liste vide l = [1 , 2 , 3] # liste de 3 entiers l = [1 , ’ bonjour ’ , 3.14] # liste m é langeant des types l2 = [[2]] # cr é ation d ’ une liste contenant le 3 è me é lem de l l2 . append (2.718) # ajout d ’ un é l é ment en fin de liste l3 = l2 # ATTENTION : pas une copie mais un alias l3 = l2 [:] # cr é ation d ’ une copie ( NE fonctionne PAS avec les tableaux !) print ( l3 [ -1]) # affiche le dernier é l é ment 4 Notion de Pile Une pile est un type abstrait (mais pensez à une pile d’assiette), c’est une collection de donnée telle que : • on peut ajouter un élément à la pile (”empiler”). • on peut tester si la pile est vide (parfois aussi si elle est pleine — plus de place disponible) • on peut récupérer un élément sur la pile (”dépiler”), et c’est le dernier ajouté (principe LIFO : Last In First Out — dernier entré premier sorti). Souvent par commodité on peut consulter le dernier ajouté sans avoir à le dépiler. En python on crée facilement une pile en prenant une liste et en se limitant aux opérateurs append, et pop() sans indice. 5 Notion de File Une file est un type abstrait cousin de la pile (mais cette fois pensez à la file à la caisse d’un commerce), c’est une collection de donnée telle que : • on peut ajouter un élément à la pile (”enfiler”). • on peut tester si la file est vide (parfois aussi si elle est pleine — plus de place disponible) • on peut récupérer un élément sur la file (”défiler”), et c’est le premier ajouté (principe FIFO : First In First Out — premier entré premier sorti). Souvent par commodité on peut consulter le premier sans avoir à le défiler. Comme pour la pile, une file en python peut être créée en prenant une liste et en se limitant aux opérateurs append, et pop(0). 6 Récursivité On parle de récursivité et d’appel récursif lorsqu’une fonction s’appelle elle-même, directement ou indirectement. Un appel récursif lance une nouvelle invocation de la fonction, dont le code va être à nouveau exécuté : on obtient donc l’équivalent d’une boucle. Si chaque invocation de la fonction effectue l’appel récursif, alors la boucle est infinie et le programme est erroné. Par conséquent l’appel récursif doit se faire seulement sous condition (par exemple dans un ”if”), afin que la récursion se termine. La condition porte typiquement sur un ou plusieurs paramètres de la fonction, qui jouent un rôle similaire à celui des variables contrôlant une itération classique. Compraison récursion/itération : def itere ( N ): i = 1 while i <= N : print ( i ) i += 1 def recurse ( N ): i = 1 auxRecurse (i , N ) def auxRecurse (i , N ): if i > N : return # nota : ici nous sommes dans le " else " sans avoir à le dire ... print ( i ) auxRecurse ( i +1 , N ) # test itere (10) recurse (10) Exemple de récursion indirecte : def rec1 ( n ): print ( n ) if n <= 0: return else : rec2 (n -1) def rec2 ( n ): if n <= 0: return else : rec1 (n -1) # affiche un entier sur 2 de mani è re compliqu é e rec1 (10) Dérécursivation Lorsque l’appel récursif est placé en fin de la fonction récursive — on parle d’appel terminal —, on transforme très facilement la fonction en boucle. Si l’appel n’est pas terminal, il faut utiliser une pile. Exemple : afficher récursivement tous les codes possibles d’un cadenas à N roulettes portant les lettres de A à E. def exoREF9aux ( chaine , nb ): lettres = ’ ABCDE ’ if nb <= 0: print ( chaine ) else : for i in range ( len ( lettres )): exoREF9aux ( chaine + lettres [ i ] , nb - 1) def exoREF9 (): print ( ’ entrez le nombre de roulettes : ’) nb = int ( input ()) exoREF9aux ( ’ ’ , nb ) Attention, dans l’exemple ci-dessous l’appel récursif semble terminal mais ne l’est pas : il est dans une boucle donc d’autre instructions (ici d’autres appels récursifs) seront exécutés après lui. On ne peut pas traduire directement par une boucle, il faut ajouter une pile : def exoREF9aux ( chaine , nb ): lettres = ’ ABCDE ’ if nb <= 0: print ( chaine ) else : for i in range ( len ( lettres )): exoREF9aux ( chaine + lettres [ i ] , nb - 1) def exoREF9 (): print ( ’ entrez le nombre de roulettes : ’) nb = int ( input ()) exoREF9aux ( ’ ’ , nb ) Version dérecursivée: def ex oR EF 9d er ecu rs iv e (): print ( ’ entrez le nombre de roulettes : ’) nb = int ( input ()) pile = [] pile . append ( ’A ’) backtrack = False while len ( pile ) > 0: if not backtrack : if len ( pile ) == nb : print ( pile ) backtrack = True else : pile . append ( ’A ’) backtrack = False else : # passer à la lettre suivante sur le sommet de pile if pile [ -1] < ’E ’: pile [ -1] = chr ( ord ( pile [ -1]) + 1) backtrack = False else : # retour arri è re pile . pop () 7 Classes/structures/enregistrements La notion ”d’enregistrement” consiste à regrouper des données de types potentiellement différents dans un même ”objet” (contrairement aux tableaux qui historiquement ne pouvaient comporter que des éléments d’un même type). On accède à ces données par un nom de champ au lieu de l’indice des tableaux. En Python on crée un enregistrement par le mot clé class et on définit les champs dans une fonction d’entête init (self ). De même il est possible de définir une manière par défaut d’afficher l’enregistrement en définition la fonction de nom réservé str (self ). class Etudiant : def __init__ ( self ): self . nom = ’’ self . prenom = ’’ self . moyenne = 0.0 def __str__ ( self ): return self . nom + ’ ’+ self . prenom + ’ ’+ str ( self . moyenne ) def exoPromo (): promo = [] for i in range (1): etud = Etudiant () etud . nom = str ( input ( ’ nom é tudiant : ’)) etud . prenom = str ( input ( ’ prenom é tudiant : ’)) etud . moyenne = float ( input ( ’ moy . é tudiant : ’)) promo . append ( etud ) print ( promo [0]) # affiche le premier moy_gen = 0.0 for i in range ( len ( promo )): moy_gen += promo [ i ]. moyenne moy_gen /= len ( promo ) print ( ’ moyenne generale = ’, moy_gen ) 8 Listes à partir d’enregistrements class ListeEtud : def __init__ ( self ): self . nom = ’’ self . prenom = ’’ self . moyenne = 0.0 self . suivant = None def __str__ ( self ): return self . nom + ’ ’+ self . prenom + ’ ’+ str ( self . moyenne ) def exoPromo2 (): promo = None for i in range (3): etud = ListeEtud () etud . nom = str ( input ( ’ nom é tudiant : ’)) etud . prenom = str ( input ( ’ prenom é tudiant : ’)) etud . moyenne = float ( input ( ’ moy . é tudiant : ’)) etud . suivant = promo promo = etud tmp = promo # attention à ne pas modifier promo while tmp != None : print ( tmp ) # affiche le courant tmp = tmp . suivant