Exercice avec Python 3 . (2024)

Table of Contents
Page Web interactive 7 . Page web produite par un script Python 8 . Merci, %s ! %s " " " % (nomv, text) 22 . 23. histogr ={} 24 . for c in text : 25. histogr [c] = histogr . get (c, 0) +1 26. 27. liste = histogr. items () # conversion en une liste de tuples 28. liste. sort () # tri de la liste 29. print "Frequence de chaque caractere dans la phrase :" 30. for c, f in liste: 31. print ' le caractere "%s" apparait %s fois ' % (c, f) Les lignes 4 et 5 sont les plus importantes : Le module cgi importe a la ligne 4 assure la connexion du script Python avec l'interface CGI , laquelle permet de dialoguer avec le serveur web. A la ligne 5, la fonction FieldStorage() de ce module renvoie un objet qui contient l'ensemble des donnees transmises par le formulaire HTML. Nous placons cet objet, lequel est assez semblable a un dictionnaire classique, dans la variable form. Par rapport a un veritable dictionnaire, l'objet place dans form presente la difference essentielle qu'il faudra lui appliquer la methode valuefj pour en extraire les donnees. Les autres methodes applicables aux dictionnaires, telles la methode has_key() , par exemple, peuvent etre utilisees de la maniere habituelle. Une caracteristique importante de l'objet dictionnaire retourne par FieldStorage() est qu'iV ne possedera aucune cle pour les champs laisses vides dans le formulaire HTML correspondant. Dans notre exemple, le formulaire comporte deux champs d'entree, auxquels nous avons associe les noms « visiteur » et « phrase ». Si ces champs ont effectivement ete completes par l'utilisateur, nous trouverons leurs contenus dans l'objet dictionnaire, aux index « visiteur » et « phrase ». Par contre, si l'un ou l'autre de ces champs n'a pas ete complete, l'index correspondant n'existera tout simplement pas. Avant toute forme de traitement de valeurs, il est done indispensable de s'assurer 268. Gerard Swinnen : Apprendre a programmer avec Python de la presence de chacun des index attendus, et c'est ce que nous faisons aux lignes 7 a 15. (17) Exercice : 17.1. Pour verifier ce qui precede, vous pouvez par exemple desactiver (en les transformant en commentaires) les lignes 7, 9, 10, 12, 14 & 15 du script. Si vous testez le fonctionnement de l'ensemble, vous constaterez que tout se passe bien si l'utilisateur complete effectivement les champs qui lui sont proposes. Si l'un des champs est laisse vide, par contre, une erreur se produit. Note importante : le script etant lance par l'intermediaire d'une page web, les messages d'erreur de Python ne seront pas affiches dans cette page, mais plutot enregistres dans le journal des evenements du serveur web. Veuillez consulter l'administrateur de ce serveur pour savoir comment vous pouvez acceder a ce journal. De toute maniere, attendez-vous a ce que la recherche des erreurs dans un script CGI soit plus ardue que dans une application ordinaire. Le reste du script est assez classique. • Aux lignes 17 a 21, nous ne faisons qu'afficher les donnees transmises par le formulaire. Veuillez noter que les variables nomv et text doivent exister au prealable, ce qui rend indispensables les lignes 9, 10, 14 & 15. • Aux lignes 23, 24 & 25, nous nous servons d'un dictionnaire pour construire un histogramme simple, comme nous l'avons explique a la page 149. • A la ligne 27, nous convertissons le dictionnaire resultant en une liste de tuples, pour pouvoir trier celle-ci dans l'ordre alphabetique a la ligne 28. • La boucle for des lignes 30 et 3 1 se passe de commentaires. 17.3 Un serveur web en pur Python ! Dans les pages precedentes, nous vous avons explique quelques rudiments de programmation CGI afin que vous puissiez mieux comprendre comment fonctionne une application web. Mais si vous voulez veritablement developper une telle application (par exemple un site web personnel dote d'une certaine interactivite), vous constaterez rapidement que l'interface CGI est un outil trop sommaire. Son utilisation telle quelle dans des scripts se revele fort lourde, et il est done preferable de faire appel a des outils plus elabores. L'interet pour le developpement web est devenu tres important, et il existe done une forte demande pour des interfaces et des environnements de programmation bien adaptes a cette tache. Or, meme s'il ne peut pas pretendre a l'universalite de langages tels que C/C++, Python est deja largement utilise un peu partout dans le monde pour ecrire des programmes tres ambitieux, y compris dans le domaine des serveurs d'applications web. La robustesse et la facilite de mise en oeuvre du langage ont seduit de nombreux developpeurs de talent, qui ont realise des outils de developpement web de tres haut niveau. Plusieurs de ces applications peuvent vous interesser si vous souhaitez realiser vous-meme des sites web interactifs de differents types. Les produits existants sont pour la plupart des logiciels libres. lis permettent de couvrir une large gamme de besoins, depuis le petit site personnel de quelques pages, jusqu'au gros site commercial collaboratif, capable de repondre a des milliers de requetes journalieres, et dont les differents secteurs sont geres sans interference par des personnes de competences variees (infographistes, programmeurs, specialistes de bases de donnees, etc.). Gerard Swinnen : Apprendre a programmer avec Python 269. Le plus celebre de ces produits est le logiciel Zope, deja adopte par de grands organismes prives et publics pour le developpement d' intranets et d'extranets collaboratifs. II s'agit en fait d'un systeme serveur d'applications, tres performant, securise, presqu'entierement ecrit en Python, et que Ton peut administrer a distance a l'aide d'une simple interface web. II ne nous est pas possible de decrire l'utilisation de Zope dans ces pages : le sujet est trop vaste, et un livre entier n'y suffirait pas. Sachez cependant que ce produit est parfaitement capable de gerer de tres gros sites d'entreprise en offrant d'enormes avantages par rapport a des solutions classiques telles que PHP ou Java. D'autres outils moins ambitieux mais tout aussi interessants sont disponibles. Tout comme Zope, la plupart d'entre eux peuvent etre telecharges librement depuis l'internet. Le fait qu'ils soient ecrits en Python assure en outre leur portabilite : vous pourrez done les employer aussi bien sous Windows que sous Linux ou MacOs. Chacun d'eux peut etre utilise en conjonction avec un serveur web « classique » tel que Apache ou Xitami (e'est preferable si le site a realiser est destine a supporter une charge de connexions tres importante), mais certains d'entre eux integrent en outre leur propre serveur web, ce qui leur permet de fonctionner egalement de maniere tout a fait autonome. Cette possibilite se revele particulierement interessante au cours de la mise au point d'un site, car elle facilite la recherche des erreurs. Cette totale autonomic alliee a la grande facilite de leur mise en oeuvre fait de ces produits de fort bonnes solutions pour la realisation de sites web d'intranet specialises, notamment dans des petites et moyennes entreprises, des administrations, ou dans des ecoles. Si vous souhaitez developper une application Python qui soit accessible par l'intermediaire d'un simple navigateur web, via un intranet d'entreprise (ou meme via l'internet, si la charge previsible n'est pas trop importante), ces applications sont faites pour vous. II en existe une grande variete : Poor man's Zope, Spyce, Karrigell, Webware, Cherrypy, Quixote, Twisted, etc. Choisissez en fonction de vos besoins : vous n'aurez que l'embarras du choix. Dans les lignes qui suivent, nous allons decrire une petite application web fonctionnant a l'aide de Karrigell. Vous pouvez trouver ce systeme a l'adresse : http://karrigell.sourceforge.net. II s'agit d'une solution de developpement web simple, bien documented en anglais et en francais (son auteur, Pierre Quentel, est en effet originaire de Bretagne, tout comme le mot karrigell, d'ailleurs, lequel signifie « charrette »). 17.3.1 Installation de Karrigell L'installation de Karrigell est un jeu d'enfant : il vous suffit d'extraire dans un repertoire quelconque le fichier archive que vous aurez telecharge depuis l'internet. L'operation de desarchivage cree automatiquement un sous-repertoire nomme Karrigell-nwffieiro de version. C'est ce repertoire que nous considererons comme repertoire racine dans les lignes qui suivent. Si vous ne comptez pas utiliser le serveur de bases de donnees Gadfly 69 qui vous est fourni en complement de Karrigell lui-meme, c'est tout ! Sinon, entrez dans le sous-repertoire gadfly-1.0.0 et lancez la commande : python setup. py install (Sous Linux, il faut etre root). Vous devez effectuer cette operation si vous souhaitez visualiser la totalite de la demonstration integree. 17.3.2 Demarrage du serveur : II s'agit done bel et bien de mettre en route un serveur web, auquel vous pourrez acceder ensuite a l'aide d'un navigateur quelconque, localement ou par l'intermediaire d'un reseau. Avant de le faire demarrer, il est cependant conseille de jeter un petit coup d'oeil dans son fichier de configuration, lequel se nomme Karrigell.ini et se trouve dans le repertoire -racine. Par defaut, Karrigell attend les requetes http sur le port n° 80. Et c'est bien ce numero de port 69 Voyez le chapitre precedent : Gadfly est un serveur de bases de donnees ecrit en Python. 270. Gerard Swinnen : Apprendre a programmer avec Python que la plupart des logiciels navigateurs utilisent eux-memes par defaut. Cependant, si vous installez Karrigell sur une machine Linux dont vous n'etes pas l'administrateur, vous n'avez pas le droit d'utiliser les numeros de port inferieurs a 1024 (pour des raisons de securite). Si vous etes dans ce cas, vous devez done modifier le fichier de configuration afin que Karrigell utilise un numero de port plus eleve. En general, vous choisirez d'enlever simplement le caractere # au debut de la ligne 39, ce qui activera l'utilisation du n° de port 8080. Plus tard, vous souhaiterez peut-etre encore modifier le fichier de configuration afin de modifier l'emplacement du repertoire racine pour votre site web (par defaut, e'est le repertoire du serveur lui-meme). Une fois le fichier de configuration modifie, entrez dans le repertoire racine du serveur, si vous n'y etes pas deja, et lancez simplement la commande : python Karrigell. py C'est tout. Votre serveur Karrigell se met en route, et vous pouvez en verifier le fonctionnement tout de suite a l'aide de votre navigateur web prefere. Si vous lancez celui-ci sur la meme machine que le serveur, vous le dirigerez vers une adresse telle que : http://localhost:8080/index.html, « localhost » etant le terme consacre pour designer la machine locale, « 8080 » le numero de port choisi dans le fichier de configuration, et « index.html » le nom du fichier qui contient la page d'accueil du site. Par contre, si vous voulez acceder a cette meme page d'accueil depuis une autre machine, vous devrez (dans le navigateur de celle-ci) indiquer le nom ou l'adresse IP du serveur, en lieu et place de localhost. Avec l'adresse indiquee au paragraphe precedent 70 , vous atteindrez la page d'accueil d'un site de demonstration de Karrigell, qui est deja pre-installe dans le repertoire racine. Vous y retrouverez la documentation de base, ainsi que toute une serie d'exemples. Dans ce qui precede, il est sous-entendu que vous avez lance le serveur depuis une console texte, ou depuis une fenetre de terminal. Dans un cas comme dans l'autre, les messages de controle emis par le serveur apparaitront dans cette console ou cette fenetre. C'est la que vous pourrez rechercher des messages d'erreur eventuels. C'est la aussi que vous devrez intervenir si vous voulez arreter le serveur (avec la combinaison de touches CTRL-C). 17.3.3 Ebauche de site web Essayons a present de realiser notre propre ebauche de site web. A la difference d'un serveur web classique, Karrigell peut gerer non seulement des pages HTML statiques (fichiers .htm, .html, .gif, . jpg, .ess) mais egalement : • des scripts Python (fichiers .py) • des scripts hybrides Python Inside HTML (fichiers .pih) • des scripts hybrides HTML Inside Python (fichiers .hip) Laissons de cote les scripts hybrides, dont vous pourrez etudier vous-meme la syntaxe (par ailleurs tres simple) si vous vous lancez dans une realisation d'une certaine importance (ils pourront vous faciliter la vie). Dans le contexte limite de ces pages, nous nous contenterons de quelques experiences de base avec des scripts Python ordinaires. Comme tous les autres elements du site (fichiers .html, .gif, .jpeg, etc.), ces scripts Python devront etre places dans le repertoire racine 71 . Vous pouvez tout de suite effectuer un test 70 Si vous avez laisse en place le n° de port par defaut (80), il est inutile de le rappeler dans les adresses, puisque c'est ce n° de port qui est utilise par defaut par la plupart des navigateurs. Une autre convention consiste a considerer que la page d'accueil d'un site Web se trouve presque toujours dans un fichier nomme index. htm ou index.html, Lorsque Ton souhaite visiter un site Web en commencant par sa page d'accueil, on peut done en general omettre ce nom dans l'adresse. Karrigell respecte cette convention, et vous pouvez done vous connecter en utilisant une adresse simplifiee telle que : http: //localhost: 8080 ou meme : http:/ /localhost (si le n° de port est 80). 71 ...ou bien dans des sous-repertoires du repertoire racine, comme il est d'usage de le faire lorsque Ton cherche a Gerard Swinnen : Apprendre a programmer avec Python 211. elementaire en redigeant un petit script d'une seule ligne, tel que : print "Bienvenue sur mon site web ! " Sauvegardez ce script sous le nom hello.py dans le repertoire racine, puis entrez l'adresse : http://localhostfhello.py (ou meme : http://localhost/hello - l'extension .py peut etre omise) dans votre navigateur. Vous devriez y voir apparaitre le message. Cela signifie done que dans l'environnement Karrigell, la sortie de l'instruction print est redirigee vers la fenetre du navigateur client, plutot que la console (ou la fenetre de terminal) du serveur. Etant donne que l'affichage a lieu dans une fenetre de navigateur web, vous pouvez utiliser toutes les ressources de la syntaxe HTML afin d'obtenir un formatage determine. Vous pouvez par exemple afficher un petit tableau de 2 lignes et 3 colonnes, avec les instructions suivantes : print """ Rouge Vert Bleu 15 % 62 % 23 % TABLE > T? Tl T? Rappelons que la balise TABLE definit un tableau. Son option BORDER specifie la largeur des bordures de separation, et CELLP ADDING l'ecart a reserver autour du contenu des cellules. Les Balises TR et TD (Table Row et Table Data) definissent les lignes et les cellules du tableau. Vous pouvez bien entendu utiliser egalement toutes les ressources de Python, comme dans l'exemple ci- dessous ou nous construisons une table des sinus, cosinus et tangentes des angles compris entre 0° et 90°, a l'aide d'une boucle classique. Ligne 7 : Nous nous servons de la fonction range() pour definir la gamme d'angles a couvrir (de zero a 60 degres par pas de 10). Ligne 9 : Les fonctions trigonometriques de Python necessitent que les angles soient exprimes en radians. II faut done effectuer une conversion. Ligne 12 : Chaque ligne du tableau comporte quatre valeurs, lesquelles sont mises en forme a l'aide du systeme de formatage des chaines de caracteres decrit deja a la page 130 : le marqueur de conversion « %8.7f » force un affichage a 8 chiffres, dont 7 apres la « virgule » decimale. Le marqueur « %8.7g » fait a peu pres la meme chose, mais passe a la notation scientifique lorsque e'est necessaire. from math import sin, cos, tan, File Edit View Go Bookmarks m * #Getting Started & ® [lT@ [el 1 . 2 . 3. 4 . 5. 6. 7 . 8. 9. 10 11 Angle Sinus C osinus Tangente 0 0.0000000 1.0000000 0 10 0.1736482 0.9848078 0.176327 20 0.3420201 0.9396926 0.3639702 30 0.5000000 0.8660254 0.5773503 40 0.6427876 0.7660444 0.8390996 50 0.7660444 0.6427876 1 1.191754 60 0.8660254 0.5000000 1.732051 Done pi # Construction de l'en-tete du tableau avec les titres de colonnes : print " " " AngleSinusCosinusTangente" " " for angle in range (0, 62, 10) : # conversion des degres en radians : aRad = angle * pi / 180 # construction d'une ligne de tableau, en exploitant le formatage des # chaines de caracteres pour fignoler l'affichage : structurer convenablement le site en construction. II vous suffira dans ce cas d'inclure le nom de ces sous- repertoires dans les adresses correspondantes. 272. Gerard Swinnen : Apprendre a programmer avec Python 12 . print "%s%8 . 7f%8 . 7f%8 . 7g"\ 13. % (angle, sin(aRad), cos (aRad) , tan (aRad) ) 14. 15. print " TABLE >" A ce stade, vous vous demandez peut-etre ou se situe la difference entre ce que nous venons d'experimenter ici et un script CGI classique (tels ceux des pages 266 et suivantes). L'interet de travailler dans un environnement plus specifique tel que Karrigell apparait cependant tres vite si vous faites des erreurs. En programmation CGI classique, les messages d'erreur emis par l'interpreteur Python ne s'affichent pas dans la fenetre du navigateur. lis sont enregistres dans un fichier journal du serveur (Apache, par exemple), ce qui ne facilite pas leur consultation. Avec un outil comme Karrigell, par contre, vous disposez d'une signalisation tres efficace, ainsi que d'un outil de deboguage complet. Faites l'experience d'introduire une petite erreur dans le script ci-dessus, et relancez votre navigateur sur la page modifiee. Par exemple, en supprimant le double point a la fin de la ligne 7, nous avons obtenu nous-memes l'affichage suivant : File Edit View Go Bookmarks lools Help O 0 4p - & - & O ft I u http://localhost:8080/triglH (E2, ♦Getting Started £3 Latest Headlines Error in /trigono2.py Scrl pt /trigono2 . py SyntaxError Li ne 7 for angle in r ange (0 . 91 . 10) Traceback (most recent call last) : File "/wi ndows/D/dos_data/py thon/Kar r igell-2 . 0 . 5/Template . py " . line 186. in render exec pythonCode in ns File "", line 7 for angle in range(@,91. 10) A SyntaxError: invalid syntax Debug | Done En cliquant sur le bouton « Debug », on obtient encore une foule d'informations complementaires (affichage du script complet, variables d'environnement, etc.). 17.3.4 Prise en charge des sessions Lorsque Ton elabore un site web interactif, on souhaite frequemment que la personne visitant le site puisse s'identifier et fournir un certain nombre de renseignements tout au long de sa visite dans differentes pages (l'exemple type etant le remplissage d'un « caddy » au cours de la consultation d'un site commercial), toutes ces informations etant conservees quelque part jusqu'a la fin de sa visite. Et il faut bien entendu realiser cela independamment pour chaque client connecte. II serait possible de transmettre les informations de page en page a l'aide de champs de formulaires caches, mais ce serait complique et tres contraignant. II est preferable que le systeme serveur soit dote d'un mecanisme specifique, qui attribue a chaque client une session particuliere. Gerard Swinnen : Apprendre a programmer avec Python 273. Karrigell realise cet objectif par l'intermediaire de cookies. Lorsqu'un nouveau visiteur du site s'identifie, le serveur genere un cookie appele sessionld et l'envoie au navigateur web, qui l'enregistre. Ce cookie contient un « identifiant de session » unique, auquel correspond un objet- session sur le serveur. Lorsque le visiteur parcourt les autres pages du site, son navigateur renvoie a chaque fois le contenu du cookie au serveur, et celui-ci peut done retrouver l'objet-session correspondant, a l'aide de son identifiant. L'objet-session reste done disponible tout au long de la visite de l'internaute : il s'agit d'un objet Python ordinaire, dans lequel on memorise un nombre quelconque d' informations sous forme d'attributs. Au niveau de la programmation, voici comment cela se passe : Pour chaque page dans laquelle vous voulez consulter ou modifier une information de session, vous commencez par creer un objet de la classe Session() : ob jet_session = Session () Si vous etes au debut de la session, Karrigell genere un identifiant unique, le place dans un cookie et envoie celui-ci au navigateur web. Vous pouvez alors ajouter un nombre quelconque d'attributs a l'objet-session : ob jet_session . nom = "Jean Dupont" Dans les autres pages, vous procedez de la meme maniere, mais l'objet produit dans ce cas par la classe Session() n'est pas nouveau : e'est l'objet cree en debut de session, retrouve en interne par le serveur grace a son identifiant relu dans le cookie. Vous pouvez acceder aux valeurs de ses attributs, et aussi en ajouter de nouveaux : obj_sess = Session () # recuperer l'objet indique par le cookie om = ob j_sess . nom # retrouver la valeur d'un attribut existant obj_sess . article = 49137 # ajouter un nouvel attribut Les objets-sessions prennent aussi en charge une methode close(), qui a pour effet d'effacer l'information de session. Vous n'etes cependant pas oblige de clore explicitement les sessions : Karrigell s'assure de toute facon qu'il n'y ait jamais plus de 1000 sessions simultanees : il efface les plus anciennes quand on arrive a la 1000 eme . Exemple de mise en oeuvre : Sauvegardez les trois petit* scripts ci-dessous dans le repertoire-racine. Le premier genere un formulaire HTML similaire a ceux qui ont ete decrits plus haut. Nommez-le sessionTestl.py : I. # Affichage d'un formulaire d' inscription : 2 . 3. print """ 4. Veuillez vous identifier, SVP : 5. 6. """ Le suivant sera nomme sessionTest2.py. C'est le script mentionne dans la balise d'ouverture du formulaire ci-dessus a la ligne 6, et qui sera invoque lorsque l'utilisateur actionnera le bouton mis en place a la ligne 10. Ce script recevra les valeurs entrees par l'utilisateur dans les differents champs du formulaire, par l'intermediaire d'un dictionnaire de requete situe dans la variable 274. Gerard Swinnen : Apprendre a programmer avec Python d'environnement QUERY de Karrigell I. obSess = Session () 2. 3. obSess.nom = QUERY [ "nomClient" ] 4. obSess .prenom = QUERY [ "prenomClient" ] 5. obSess. sexe = QUERY [ "sexeClient" ] 6. 7. if obSess . sexe .upper () == "M" : 8. vedette ="Monsieur" 9. else: 10. vedette ="Madame" II. print " Bienvenue, %s %s " % (vedette, obSess.nom) 12. print "" 13. print """ 14. Suite ... " " " La premiere ligne de ce script cree l'objet-session, genere pour lui un identifiant unique, et expedie celui-ci au navigateur sous la forme d'un cookie. Dans les lignes 3, 4, 5, on recupere les valeurs entrees dans les champs du formulaire precedent, en utilisant leurs noms comme cles d'acces au dictionnaire de requetes. La ligne 14 definit un lien http pointant vers le troisieme script, nomme sessionTest3.py : 1. suiviSess = Session () # retrouver l'objet-session 2. suiviSess . article = 12345 # lui ajouter des attributs 3. suiviSess .prix = 43.67 4. 5. print """ 6. Page suivante 7. Suivi de la commande du client : %s %s 8. Article n° %s, Prix : %s 9. """ % (suiviSess . prenom, suiviSess . nom, 10. suiviSess . article, suiviSess .prix) Dirigez votre navigateur web vers l'adresse : http://localhost:8080/sessionTestl . Entrez des valeurs de votre choix dans les champs du formulaire, et cliquez sur le bouton OK : 72 Karrigell met en place un certain nombre de variables globales dont les noms sont en majuscules pour eviter un conflit eventuel avec les votres. Celle-ci joue le meme role que la fonction FieldStorageQ du module cgi. Veuillez consulter la documentation de Karrigell si vous souhaitez obtenir des explications plus detaillees. Gerard Swinnen : Apprendre a programmer avec Python 275. File Edit View Go Bookmarks Toe ♦ Getting Started B Latest Headlines Veuillez vous identifier, SVP : Votre Dom : |Dupont Votre prenoni : |Charles Votre sexe (ni/f) : |m OK Done 1 ■ <-■■ - .zl°l*l File Edit View Go Bookmarks Toe • l; • & © lu H (El ♦ Getting Started 13 Latest Headlines Bienvenue, Monsieur Dnpont Suite... Done Comme attendu, les informations entrees dans le formulaire sont transmises a la deuxieme page. A present, si vous cliquez sur le lien : « Suite... » dans celle-ci, vous dirigez encore une fois votre navigateur vers une nouvelle page, mais celle-ci n'aura fait l'objet d'aucune transmission de donnees (puisqu'on n'y accede pas par l'intermediaire d'un formulaire). Dans le script sessionTest3.py qui genere cette page, vous ne pouvez done pas utiliser la variable QUERY pour retrouver les informations entrees par le visiteur. C'est ici qu'intervient le mecanisme des objets-sessions. Lors du lancement de ce troisieme script, le cookie memorise par le navigateur est relu par le serveur, ce qui lui permet de regenerer l'objet-session cree dans le script precedent. File Edit View Go Bookmarks ♦ Getting Started B Latest Headlines Page suivante Suivi de la commande du client : Charles Dupont Article n° 12345, Prix : 43.67 Done Analysez les trois premieres lignes du script sessionTest3.py : l'objet suiviSess instancie a partit de la classe Session() est l'objet-session regenere. II contient les informations sauvegardees a la page precedente, et on peut lui en ajouter d'autres dans des attributs supplementaires. Vous aurez compris que vous pouvez desormais recuperer toutes ces informations de la meme maniere dans n'importe quelle autre page, car elles persisteront jusqu'a ce que l'utilisateur termine sa visite du site, a moins que vous ne fermiez vous-meme cette session par programme, a l'aide de la methode close() evoquee plus haut. Exercice : 17.2. Ajoutez au script precedent un lien vers une quatrieme page, et ecrivez le script qui 276. Gerard Swinnen : Apprendre a programmer avec Python generera celle-ci. Les informations devront cette fois etre affichees dans un tableau : Nom Prenom Sexe Article Prix 17.3.5 Autres developpements Nous terminons ici cette breve etude de Karrigell, car il nous semble vous avoir explique l'essentiel de ce qu'il vous faut connaitre pour demarrer. Si vous desirez en savoir davantage, il vous suffira de consulter la documentation et les exemples fournis avec le produit. Comme nous l'avons deja signale plus haut, l'installation de Karrigell inclut l'installation du systeme de bases de donnees Gadfly. Vous pouvez done tres rapidement et tres aisem*nt realiser un site interactif permettant la consultation a distance d'un ensemble de donnees quelconques, en admettant bien entendu que la charge de requetes de votre site reste moderee, et que la taille de la base de donnees elle-meme ne devienne pas gigantesque. N'esperez pas gerer a l'aide de Karrigell un site commercial susceptible de traiter plusieurs millions de requetes journalieres ! Si vous ambitionnez de realiser ce genre de choses, il vous faudra etudier d'autres offres logicielles, comme par exemple CherryPy ou Zope associes a Apache pour le systeme serveur, et SQLite, MySQL ou PostgreSQL pour le gestionnaire de bases de donnees. Gerard Swinnen : Apprendre a programmer avec Python 277. Chapitre 18 : Communications a travers un reseau Le developpement extraordinaire de l'internet a amplement demontre que les ordinateurs peuvent etre des outils de communication tres efficaces. Dans ce chapitre, nous allons experimenter la plus simple des techniques d'interconnexion de deux programmes, qui leur permette de s'echanger des informations par l'intermediaire d'un reseau. Pour ce qui va suivre, nous supposerons done que vous collaborez avec un ou plusieurs de vos condisciples, et que vos postes de travail Python sont connectes a un reseau local dont les communications utilisent le protocole TCP/IP. Le systeme d'exploitation n'a pas d'importance : vous pouvez par exemple installer l'un des scripts Python decrits ci-apres sur un poste de travail fonctionnant sous Linux, et le faire dialoguer avec un autre script mis en oeuvre sur un poste de travail confie aux bons soins d'un systeme d'exploitation different, tel que MacOS ou Windows. Vous pouvez egalement experimenter ce qui suit sur une seule et meme machine, en mettant les differents scripts en oeuvre dans des fenetres independantes. 18.1 Les sockets Le premier exercice qui va vous etre propose consistera a etablir une communication entre deux machines seulement. L'une et l'autre pourront s'echanger des messages a tour de role, mais vous constaterez cependant que leurs configurations ne sont pas symetriques. Le script installe sur l'une de ces machines jouera en effet le role d'un logiciel serveur, alors que l'autre se comportera comme un logiciel client. Le logiciel serveur fonctionne en continu, sur une machine dont l'identite est bien definie sur le reseau grace a une adresse IP specifique 73 . II guette en permanence l'arrivee de requetes expediees par les clients potentiels en direction de cette adresse, par l'intermediaire d'un port de communication bien determine. Pour ce faire, le script correspondant doit mettre en oeuvre un objet logiciel associe a ce port, que Ton appelle un socket. Au depart d'une autre machine, le logiciel client tente d'etablir la connexion en emettant une requete appropriee. Cette requete est un message qui est confie au reseau, un peu comme on confie une lettre a la Poste. Le reseau pourrait en effet acheminer la requete vers n'importe quelle autre machine, mais une seule est visee : pour que la destination visee puisse etre atteinte, la requete contient dans son en-tete l'indication de l'adresse IP et du port de communication destinataires. Lorsque la connexion est etablie avec le serveur, le client lui assigne lui-meme l'un de ses propres ports de communication. A partir de ce moment, on peut considerer qu'un canal privilegie relie les deux machines, comme si on les avait connectees l'une a l'autre par l'intermediaire d'un fil (les deux ports de communication respectifs jouant le role des deux extremites de ce fil). L'echange d'informations proprement dit peut commencer. Pour pouvoir utiliser les ports de communication reseau, les programmes font appel a un ensemble de procedures et de fonctions du systeme d'exploitation, par l'intermediaire d'objets interfaces que Ton appelle des sockets. Ceux-ci peuvent mettre en oeuvre deux techniques de communication differentes et complementaires : celle des paquets (que Ton appelle aussi des datagrammes), tres largement utilisee sur l'internet, et celle de la connexion continue, ou stream socket, qui est un peu plus simple. 73 Une machine particuliere peut egalement etre designee par un nom plus explicite, mais a la condition qu'un mecanisme ait ete mis en place sur le reseau (DNS) pour traduire automatiquement ce nom en adresse IP. Veuillez consulter votre cours sur les systemes d'exploitation et les reseaux pour en savoir davantage. 278. Gerard Swinnen : Apprendre a programmer avec Python 18.2 Construction d'un serveur elementaire Pour nos premieres experiences, nous allons utiliser la technique des stream sockets. Celle-ci est en effet parfaitement appropriee lorsqu'il s'agit de faire communiquer des ordinateurs interconnects par l'intermediaire d'un reseau local. C'est une technique particulierement aisee a mettre en oeuvre, et elle permet un debit eleve pour l'echange de donnees. L'autre technologie (celle des paquets) serait preferable pour les communications expediees via l'internet, en raison de sa plus grande fiabilite (les memes paquets peuvent atteindre leur destination par differents chemins, etre emis ou re-emis en plusieurs exemplaires si cela se revele necessaire pour corriger les erreurs de transmission), mais sa mise en ceuvre est un peu plus complexe. Nous ne l'etudierons pas dans ce cours. Le script ci-dessous met en place un serveur capable de communiquer avec un seul client. Nous verrons un peu plus loin ce qu'il faut lui aj outer afin qu'il puisse prendre en charge en parallele les connexions de plusieurs clients. 1. # Definition d'un serveur reseau rudimentaire 2. # Ce serveur attend la connexion d'un client, pour entamer un dialogue avec lui 3. 4. import socket, sys 5. 6. HOST = '192.168.14.152' 7. PORT = 50000 8. 9. #1) creation du socket : 10. mySocket = socket . socket (socket .AF_INET, socket . SOCK_STREAM) 11. 12. #2) liaison du socket a une adresse precise : 13. try: 14. mySocket. bind ( (HOST, PORT)) 15. except socket . error : 16. print "La liaison du socket a 1' adresse choisie a echoue . " 17 . sys .exit () 18. 19. while 1: 20. #3) Attente de la requete de connexion d'un client : 21. print "Serveur pret, en attente de requetes ..." 22. mySocket . listen (5) 23. 24. #4) Etablissem*nt de la connexion : 25. connexion, adresse = mySocket . accept ( ) 26. print "Client connecte, adresse IP %s, port %s" % (adresse [0], adresse [1]) 27. 28. #5) Dialogue avec le client : 29. connexion . send ( "Vous etes connecte au serveur Marcel. Envoyez vos messages.") 30. msgClient = connexion . recv (1024) 31. while 1: 32. print "C>", msgClient 33. if msgClient . upper () == "FIN" or msgClient =="": 34 . break 35. msgServeur = raw_input ( "S> ") 36. connexion . send (msgServeur) 37. msgClient = connexion . recv (1024) 38. 39. #6) Fermeture de la connexion : 40. connexion . send ( "Au revoir !") 41. print "Connexion interrompue . " 42. connexion . close ( ) 43. 44. ch = raw_input ( "ecommencer erminer ? ") 45. if ch. upper () == ' T ' : 46. break Gerard Swinnen : Apprendre a programmer avec Python 279. Commentaires : • Ligne 4 : Le module socket contient toutes les fonctions et les classes necessaires pour construire des programmes communiquants. Comme nous allons le voir dans les lignes suivantes, l'etablissem*nt de la communication comporte six etapes. • Lignes 6 & 7 : Ces deux variables definissent l'identite du serveur, telle qu'on l'integrera au socket. HOST doit contenir une chaine de caracteres indiquant l'adresse IP du serveur sous la forme decimale habituelle, ou encore le nom DNS de ce meme serveur (mais a la condition qu'un mecanisme de resolution des noms ait ete mis en place sur le reseau). PORT doit contenir un entier, a savoir le numero d'un port qui ne soit pas deja utilise pour un autre usage, et de preference une valeur superieure a 1024 (Cfr. votre cours sur les services reseau). • Lignes 9 & 10 : Premiere etape du mecanisme d'interconnexion. On instancie un objet de la classe socket(), en precisant deux options qui indiquent le type d'adresses choisi (nous utiliserons des adresses de type « internet ») ainsi que la technologie de transmission (datagrammes ou connexion continue {stream) : nous avons decide d'utiliser cette derniere). • Lignes 12 a 17 : Seconde etape. On tente d'etablir la liaison entre le socket et le port de communication. Si cette liaison ne peut etre etablie (port de communication occupe, par exemple, ou nom de machine incorrect), le programme se termine sur un message d'erreur. Remarque : la methode bind() du socket attend un argument du type tuple, raison pour laquelle nous devons enfermer nos deux variables dans une double paire de parentheses. • Ligne 19 : Notre programme serveur etant destine a fonctionner en permanence dans l'attente des requetes de clients potentiels, nous le lancons dans une boucle sans fin. • Lignes 20 a 22 : Troisieme etape. Le socket etant relie a un port de communication, il peut a present se preparer a recevoir les requetes envoyees par les clients. C'est le role de la methode listen(). L'argument qu'on lui transmet indique le nombre maximum de connexions a accepter en parallele. Nous verrons plus loin comment gerer celles-ci. • Lignes 24 a 26 : Quatrieme etape. Lorsqu'on fait appel a sa methode accept(), le socket attend indefiniment qu'une requete se presente. Le script est done interrompu a cet endroit, un peu comme il le serait si nous faisions appel a une fonction input() pour attendre une entree clavier. Si une requete est receptionnee, la methode accept() renvoie un tuple de deux elements : le premier est la reference d'un nouvel objet de la classe socket() 74 , qui sera la veritable interface de communication entre le client et le serveur, et le second un autre tuple contenant les coordonnees de ce client (son adresse IP et le n° de port qu'il utilise lui-meme). Lignes 28 a 30 : Cinquieme etape. La communication proprement dite est etablie. Les methodes send() et recv() du socket servent evidemment a remission et a la reception des messages, qui doivent etre de simples chaines de caracteres. Remarques : la methode send() renvoie le nombre d'octets expedies. L'appel de la methode recv 0 doit comporter un argument entier indiquant le nombre maximum d'octets a receptionner en une fois (Les octets surnumeraires sont mis en attente dans un tampon. lis sont transmis lorsque la meme methode recv() est appelee a nouveau). 74 Nous verrons plus loin l'utilite de creer ainsi un nouvel objet socket pour prendre en charge la communication, plutot que d'utiliser celui qui a deja cree a la ligne 10. En bref, si nous voulons que notre serveur puisse prendre en charge simultanement les connexions de plusieurs clients, il nous faudra disposer d'un socket distinct pour chacun d'eux, independamment du premier que Ton laissera fonctionner en permanence pour receptionner les requetes qui continuent a arriver en provenance de nouveaux clients. 280. Gerard Swinnen : Apprendre a programmer avec Python • Lignes 31 a 37 : Cette nouvelle boucle sans fin maintient le dialogue jusqu'a ce que le client decide d'envoyer le mot « fin » ou une simple chaine vide. Les ecrans des deux machines afficheront chacune revolution de ce dialogue. • Lignes 39 a 42 : Sixieme etape. Fermeture de la connexion. 18.3 Construction d'un client rudimentaire Le script ci-dessous definit un logiciel client complementaire du serveur decrit dans les pages precedentes. On notera sa grande simplicite. 1. # Definition d'un client reseau rudimentaire 2 . # Ce client dialogue avec un serveur ad hoc 3. 4. import socket, sys 5. 6. HOST = '192.168.14.152' 7. PORT = 50000 8. 9. #1) creation du socket : 10. mySocket = socket . socket (socket .AF_INET, socket . SOCK_STREAM) 11. 12. #2) envoi d'une requete de connexion au serveur : 13. try: 14. mySocket. connect ( (HOST, PORT)) 15. except socket . error : 16. print "La connexion a echoue." 17. sys. exit () 18. print "Connexion etablie avec le serveur." 19. 20. #3) Dialogue avec le serveur : 21. msgServeur = mySocket . recv (1024) 22 . 23. while 1: 24. if msgServeur . upper () == "FIN" or msgServeur =="": 25. break 26. print "S>", msgServeur 27. msgClient = raw_input("C> ") 28. mySocket . send (msgClient) 29. msgServeur = mySocket . recv (1024) 30. 31. #4) Fermeture de la connexion : 32. print "Connexion interrompue . " 33. mySocket . close () Commentaires : • Le debut du script est similaire a celui du serveur. L'adresse IP et le port de communication doivent etre ceux du serveur. • Lignes 12 a 18 : On ne cree cette fois qu'un seul objet socket, dont on utilise la methode connect () pour envoyer la requete de connexion. • Lignes 20 a 33 : Une fois la connexion etablie, on peut dialoguer avec le serveur en utilisant les methodes send() et recv() deja decrites plus haut pour celui-ci. Gerard Swinnen : Apprendre a programmer avec Python 281. 18.4 Gestion de plusieurs taches en parallele a I'aide des threads Le systeme de communication que nous avons elabore dans les pages precedentes est vraiment tres rudimentaire : d'une part il ne met en relation que deux machines seulement, et d'autre part il limite la liberte d'expression des deux interlocuteurs. Ceux-ci ne peuvent en effet envoyer des messages que chacun a leur tour. Par exemple, lorsque l'un d'eux vient d'emettre un message, son systeme reste bloque tant que son partenaire ne lui a pas envoye une reponse. Lorsqu'il vient de recevoir une telle reponse, son systeme reste incapable d'en receptionner une autre, tant qu'il n'a pas entre lui-meme un nouveau message, ... et ainsi de suite. Tous ces problemes proviennent du fait que nos scripts habituels ne peuvent s'occuper que d'une seule chose a la fois. Lorsque le flux d'instructions rencontre une fonction input(), par exemple, il ne se passe plus rien tant que l'utilisateur n'a pas introduit la donnee attendue. Et meme si cette attente dure tres longtemps, il n'est habituellement pas possible que le programme effectue d'autres taches pendant ce temps. Ceci n'est toutefois vrai qu'au sein d'un seul et meme programme : vous savez certainement que vous pouvez executer d'autres applications entretemps sur votre ordinateur, car les systemes d' exploitation modernes sont « multi-taches ». Les pages qui suivent sont destinees a vous expliquer comment vous pouvez introduire cette fonctionnalite multi-taches dans vos programmes, afin que vous puissiez developper de veritables applications reseau, capables de communiquer simultanement avec plusieurs partenaires. Veuillez a present considerer le script de la page precedents Sa fonctionnalite essentielle reside dans la boucle while des lignes 23 a 29. Or, cette boucle s'interrompt a deux endroits : • a la ligne 27, pour attendre les entrees clavier de l'utilisateur (fonction raw_input()) ; • a la ligne 29, pour attendre l'arrivee d'un message reseau. Ces deux attentes sont done successives, alors qu'il serait bien plus interessant qu'elles soient simultanees. Si e'etait le cas, l'utilisateur pourrait expedier des messages a tout moment, sans devoir attendre a chaque fois la reaction de son partenaire. II pourrait egalement recevoir n'importe quel nombre de messages, sans l'obligation d'avoir a repondre a chacun d'eux pour recevoir les autres. Nous pouvons arriver a ce resultat si nous apprenons a gerer plusieurs sequences d'instructions en parallele au sein d'un meme programme. Mais comment cela est-il possible ? Au cours de l'histoire de l'informatique, plusieurs techniques ont ete mises au point pour partager le temps de travail d'un processeur entre differentes taches, de telle maniere que celles-ci paraissent etre effectuees en meme temps (alors qu'en realite le processeur s'occupe d'un petit bout de chacune d'elles a tour de role). Ces techniques sont implementees dans le systeme d'exploitation, et il n'est pas necessaire de les detailler ici, meme s'il est possible d'acceder a chacune d'elles avec Python. Dans les pages suivantes, nous allons apprendre a utiliser celle de ces techniques qui est a la fois la plus facile a mettre en oeuvre, et la seule qui soit veritablement portable (elle est en effet supportee par tous les grands systemes d'exploitation) : on l'appelle la technique des processus legers ou threads 75 . Dans un programme d'ordinateur, les threads sont des flux d'instructions qui sont menes en parallele (quasi-simultanement), tout en partageant le meme espace de noms global. En fait, le flux d'instructions de n'importe quel programme Python suit toujours au moins un thread : le thread principal. A partir de celui-ci, d'autres threads « enfants » peuvent etre amorces, 75 Dans un systeme d'exploitation de type Unix (comme Linux), les differents threads d'un meme programme font partie d'un seul processus. II est egalement possible de gerer differents processus a I'aide d'un meme script Python (operation fork), mais l'explication de cette technique depasse largement le cadre de ce cours. 282. Gerard Swinnen : Apprendre a programmer avec Python qui seront executes en parallele. Chaque thread enfant se termine et disparait sans autre forme de proces lorsque toutes les instructions qu'il contient ont ete executees. Par contre, lorsque le thread principal se termine, il faut parfois s'assurer que tous ses threads enfants « meurent » avec lui. 18.5 Client gerant remission et la reception simultanees Nous allons maintenant mettre en pratique la technique des threads pour construire un systeme de « chat » 76 simplifie. Ce systeme sera constitute d'un seul serveur et d'un nombre quelconque de clients. Contrairement a ce qui se passait dans notre premier exercice, personne n'utilisera le serveur lui-meme pour communiquer, mais lorsque celui-ci aura ete mis en route, plusieurs clients pourront s'y connecter et commencer a s'echanger des messages. Chaque client enverra tous ses messages au serveur, mais celui-ci les re-expediera immediatement a tous les autres clients connectes, de telle sorte que chacun puisse voir l'ensemble du trafic. Chacun pourra a tout moment envoyer ses messages, et recevoir ceux des autres, dans n'importe quel ordre, la reception et remission etant gerees simultanement, dans des threads separes. Le script ci-apres definit le programme client. Le serveur sera decrit un peu plus loin. Vous constaterez que la partie principale du script (ligne 38 et suivantes) est similaire a celle de l'exemple precedent. Seule la partie « Dialogue avec le serveur » a ete remplacee. Au lieu d'une boucle while, vous y trouvez a present les instructions de creation de deux objets threads (aux lignes 49 et 50), dont on demarre la fonctionnalite aux deux lignes suivantes. Ces objets threads sont crees par derivation, a partir de la classe Thread() du module threading. lis s'occuperont independamment de la reception et le remission des messages. Les deux threads « enfants » sont ainsi parfaitement encapsules dans des objets distincts, ce qui facilite la comprehension du mecanisme. 1. # Definition d'un client reseau gerant en parallele 1' emission 2. # et la reception des messages (utilisation de 2 THREADS). 3. 4. host = '192.168.0.235' 5. port = 40000 6. 7. import socket, sys, threading 8. 9. class ThreadReception (threading. Thread) : 10. """objet thread gerant la reception des messages""" 11. def init (self, conn) : 12 . threading . Thread. init (self) 13. self . connexion = conn # ref. du socket de connexion 14. 15. def run (self): 16. while 1: 17. message_recu = self . connexion . recv (1024) 18. print "*" + message_recu + "*" 19. if message_recu =='' or message_recu. upper () == "FIN": 20. break 21. # Le thread se termine ici . 22. # On force la fermeture du thread : 23 . th_E . _Thread stop ( ) 24. print "Client arrete. Connexion interrompue . " 25. self . connexion . close ( ) 26. 27. class ThreadEmission (threading . Thread) : 28. """objet thread gerant 1' emission des messages""" 29. def init (self, conn) : 76 Le « chat » est l'occupation qui consiste a « papoter » par l'intermediaire d'ordinateurs. Les canadiens francophones ont propose le terme de clavardage pour designer ce « bavardage par claviers interposes ». Gerard Swinnen : Apprendre a programmer avec Python 283. 30 . threading . Thread . init (self) 31. self . connexion = conn # ref. du socket de connexion 32. 33. def run (self): 34 . while 1 : 35. message_emis = raw_input() 36 . self . connexion . send (message_emis) 37 . 38. # Programme principal - Etablissem*nt de la connexion : 39. connexion = socket . socket (socket . AF_INET, socket . SOCK_STREAM) 40. try: 41. connexion . connect ( (host, port)) 42. except socket . error : 43. print "La connexion a echoue." 44. sys.exit() 45. print "Connexion etablie avec le serveur." 46. 47 . # Dialogue avec le serveur : on lance deux threads pour gerer 48. # independamment 1' emission et la reception des messages : 49. th_E = ThreadEmiss ion (connexion) 50. th_R = ThreadReception (connexion) 51. th_E. start () 52. th_R. start () Commentaires : • Remarque generate : Dans cet exemple, nous avons decide de creer deux objets threads independants du thread principal, afin de bien mettre en evidence les mecanismes. Notre programme utilise done trois threads en tout, alors que le lecteur attentif aura remarque que deux pourraient suffire. En effet : le thread principal ne sert en definitive qu'a lancer les deux autres ! II n'y a cependant aucun interet a limiter le nombre de threads. Au contraire : a partir du moment ou Ton decide d'utiliser cette technique, il faut en profiter pour compartimenter l'application en unites bien distinctes. • Ligne 7 : Le module threading contient la definition de toute une serie de classes interessantes pour gerer les threads. Nous n'utiliserons ici que la seule classe Thread(), mais une autre sera exploitee plus loin (la classe Lock()), lorsque nous devrons nous preoccuper de problemes de synchronisation entre differents threads concurrents. • Lignes 9 a 25 : Les classes derivees de la classe Thread() contiendront essentiellement une methode run(). C'est dans celle-ci que Ton placera la portion de programme specifiquement confiee au thread. II s'agira souvent dune boucle repetitive, comme ici. Vous pouvez parfaitement considerer le contenu de cette methode comme un script independant, qui s'execute en parallele avec les autres composants de votre application. Lorsque ce code a ete completement execute, le thread se referme. • Lignes 16 a 20: Cette boucle gere la reception des messages. A chaque iteration, le flux d'instructions s'interrompt a la ligne 17 dans l'attente d'un nouveau message, mais le reste du programme n'est pas fige pour autant : les autres threads continuent leur travail independamment. • Ligne 19 : La sortie de boucle est provoquee par la reception d'un message 'fin' (en majuscules ou en minuscules), ou encore d'un message vide (c'est notamment le cas si la connexion est coupee par le partenaire). Quelques instructions de « nettoyage » sont alors executees, et puis le thread se termine. • Ligne 23 : Lorsque la reception des messages est terminee, nous souhaitons que le reste du programme se termine lui aussi. II nous faut done forcer la fermeture de l'autre objet thread, celui que nous avons mis en place pour gerer remission des messages. Cette fermeture forcee peut etre 284. Gerard Swinnen : Apprendre a programmer avec Python obtenue a l'aide de la methode _Thread stop() 77 . • Lignes 27 a 36 : Cette classe definit done un autre objet thread, qui contient cette fois une boucle de repetition perpetuelle. II ne se pourra done se terminer que contraint et force par methode decrite au paragraphe precedent. A chaque iteration de cette boucle, le flux d'instructions s'interrompt a la ligne 35 dans l'attente d'une entree clavier, mais cela n'empeche en aucune maniere les autres threads de faire leur travail. • Lignes 38 a 45 : Ces lignes sont reprises a l'identique des scripts precedents. • Lignes 47 a 52 : Instanciation et demarrage des deux objets threads « enfants ». Veuillez noter qu'il est recommande de provoquer ce demarrage en invoquant la methode integree start(), plutot qu'en faisant appel directement a la methode run() que vous aurez definie vous-meme. Sachez egalement que vous ne pouvez invoquer start() qu'une seule fois (une fois arrete, un objet thread ne peut pas etre redemarre). 18.6 Serveur gerant les connexions de plusieurs clients en par allele Le script ci-apres cree un serveur capable de prendre en charge les connexions d'un certain nombre de clients du meme type que ce que nous avons decrit dans les pages precedentes. Ce serveur n'est pas utilise lui-meme pour communiquer : ce sont les clients qui communiquent les uns avec les autres, par l'intermediaire du serveur. Celui-ci joue done le role d'un relais : il accepte les connexions des clients, puis attend l'arrivee de leurs messages. Lorsqu'un message arrive en provenance d'un client particulier, le serveur le re-expedie a tous les autres, en lui ajoutant au passage une chaine d'identification specifique du client emetteur, afin que chacun puisse voir tous les messages, et savoir de qui ils proviennent. 1. # Definition d'un serveur reseau gerant un systeme de CHAT simplifie . 2. # Utilise les threads pour gerer les connexions clientes en parallele . 3. 4. HOST = '192.168.0.235' 5. PORT = 40000 6. 7. import socket, sys, threading 8. 9. class ThreadClient (threading. Thread) : 10. 11 'derivation d'un objet thread pour gerer la connexion avec un client' ' ' 11. def init (self, conn): 12 . threading. Thread. init (self) 13. self . connexion = conn 14. 15. def run (self) : 16. # Dialogue avec le client : 17. nom = self .getName () # Chaque thread possede un nom 18. while 1: 19. msgClient = self . connexion . recv (1024 ) 20. if msgClient . upper () == "FIN" or msgClient =="": 21. break 22. message = "%s> %s" % (nom, msgClient) 23 . print message 24 . # Faire suivre le message a tous les autres clients : 25. for cle in conn_client : 26. if cle != nom: # ne pas le renvoyer a 1 ' emetteur 27. conn_client [cle] . send (message) 28. 77 Que les puristes veuillent bien me pardonner : j'admets volontiers que cette astuce pour forcer l'arret d'un thread n'est pas vraiment recommandable. Je me suis autorise ce raccourci afin de ne pas trop alourdir ce texte, qui se veut seulement une initiation. Le lecteur exigeant pourra approfondir cette question en consultant l'un ou l'autre des ouvrages de reference mentionnes dans la bibliographie (voir page 8) Gerard Swinnen : Apprendre a programmer avec Python 285. 29. # Fermeture de la connexion : 30. self . connexion . close ( ) # couper la connexion cote serveur 31 . del conn_client [nom] # supprimer son entree dans le dictionnalre 32. print "Client %s deconnecte." % nom 33 . # Le thread se termine ici 34. 35. # Initialisation du serveur - Mise en place du socket : 36. mySocket = socket . socket (socket . AF_INET, socket . SOCK_STREAM) 37. try: 38. my Socket. bind ( (HOST, PORT)) 39. except socket. error: 40. print "La liaison du socket a l'adresse choisie a echoue." 41. sys.exit() 42. print "Serveur pret, en attente de requetes ..." 43. mySocket . listen (5) 44. 45. # Attente et prise en charge des connexions demandees par les clients : 46. conn_client = {} # dictionnalre des connexions clients 47 . while 1 : 48. connexion, adresse = mySocket . accept ( ) 49. # Creer un nouvel objet thread pour gerer la connexion : 50. th = ThreadClient (connexion) 51. th. start () 52 . # Memoriser la connexion dans le dictionnalre : 53. it = th.getName() # identifiant du thread 54. conn_client [it] = connexion 55. print "Client %s connecte, adresse IP %s, port %s." %\ 56. (it, adresse [0], adresse [1]) 57 . # Dialogue avec le client : 58. connexion . send ( "Vous etes connecte. Envoyez vos messages.") Commentaires : • Lignes 35 a 43 : L'initialisation de ce serveur est identique a celle du serveur rudimentaire decrit au debut du present chapitre. • Ligne 46 : Les references des differentes connexions doivent etre memorisees. Nous pourrions les placer dans une liste, mais il est plus judicieux de les placer dans un dictionnaire, pour deux raisons : La premiere est que nous devrons pouvoir aj outer ou enlever ces references dans n'importe quel ordre, puisque les clients se connecteront et se deconnecteront a leur guise. La seconde est que nous pouvons disposer aisem*nt d'un identifiant unique pour chaque connexion, lequel pourra servir de cle d'acces dans un dictionnaire. Cet identifiant nous sera en effet fourni automatiquement par La classe ThreadO- • Lignes 47 a 5 1 : Le programme commence ici une boucle de repetition perpetuelle, qui va constamment attendre l'arrivee de nouvelles connexions. Pour chacune de celles-ci, un nouvel objet ThreadClient() est cree, lequel pourra s'occuper d'elle independamment de toutes les autres. • Lignes 52 a 54 : Obtention d'un identifiant unique a l'aide de la methode getName(). Nous pouvons profiter ici du fait que Python attribue automatiquement un nom unique a chaque nouveau thread : ce nom convient bien comme identifiant (ou cle) pour retrouver la connexion correspondante dans notre dictionnaire. Vous pourrez constater qu'il s'agit d'une chaine de caracteres, de la forme : « Thread-N » (N etant le numero d'ordre du thread). • Lignes 15 a 17 : Gardez bien a l'esprit qu'il se creera autant d'objets ThreadClient() que de connexions, et que tous ces objets fonctionneront en parallele. La methode getName() peut alors etre utilisee au sein de l'un quelconque de ces objets pour retrouver son identite particuliere. Nous utiliserons cette information pour distinguer la connexion courante de toutes les autres (voir ligne 26). • Lignes 18 a 23 : L'utilite du thread est de receptionner tous les messages provenant d'un client particulier. II faut done pour cela une boucle de repetition perpetuelle, qui ne s'interrompra qu'a 286. Gerard Swinnen : Apprendre a programmer avec Python la reception du message specifique : « fin », ou encore a la reception d'un message vide (cas ou la connexion est coupee par le partenaire). • Lignes 24 a 27 : Chaque message recu d'un client doit etre re-expedie a tous les autres. Nous utilisons ici une boucle for pour parcourir l'ensemble des cles du dictionnaire des connexions, lesquelles nous permettent ensuite de retrouver les connexions elles-memes. Un simple test (a la ligne 26) nous evite de re-expedier le message au client dont il provient. • Ligne 3 1 : Lorsque nous fermons un socket de connexion, il est preferable de supprimer sa reference dans le dictionnaire, puisque cette reference ne peut plus servir. Et nous pouvons faire cela sans precaution particuliere, car les elements d'un dictionnaire ne sont pas ordonnes (nous pouvons en ajouter ou en enlever dans n'importe quel ordre). 18.7 Jeu des bombardes, version reseau Iclient Thread- 3 connects, adresse IP 192. 168. 0. 23S, port 3493S. \X Client Thread-4 connecte, adresse IP 192. 168 0. 23S, port 34936. Client Thread-S connecte, adresse IP 192. 168 0, 23S, port 34937. Client Thread-6 connecte, adresse IP 192. 168. 0. 23S, port 34938. I— 1 Client Thread-7 connecte, adresse IP 192. 168. 0. 23S, port 34939. / EThread-3 ■ Thread-S ■ Thread-7 ■ Thread-S | Thread-4 points points points M points 45 M P oints 45 M -5 1 Z5H 1 U 4 U -3 _ Feu I | i , -ii ; | r.-.j ■ [ Feu ! Feu I | Thread-2 points -1 Au chapitre 15, nous avons commente le developpement d'un petit jeu de combat dans lequel des joueurs s'affrontaient a l'aide de bombardes. L'interet de ce jeu reste toutefois fort limite, tant qu'il se pratique sur un seul et meme ordinateur. Nous allons done le perfectionner, en y integrant les techniques que nous venons d'apprendre. Comme le systeme de « chat » decrit dans les pages precedentes, l'application complete se composera desormais de deux programmes distincts : un logiciel serveur qui ne sera mis en fonctionnement que sur une seule machine, et un logiciel client qui pourra etre lance sur toute une serie d' autres. Du fait du caractere portable de Python, il vous sera meme possible d'organiser des combats de bombardes entre ordinateurs geres par des systemes d'exploitation differents (MacOS <> Linux <> Windows !). Gerard Swinnen : Apprendre a programmer avec Python 287. 18.7.1 Programme serveur : vue d'ensemble Les programmes serveur et client exploitent la meme base logicielle, elle-meme largement recuperee de ce qui avait deja ete mis au point tout au long du chapitre 15. Nous admettrons done pour la suite de cet expose que les deux versions precedentes du jeu ont ete sauvegardees dans les fichiers-modules canon03.py et canon04.py, installes dans le repertoire courant. Nous pouvons en effet reutiliser une bonne partie du code qu'ils contiennent, en nous servant judicieusem*nt de I'importation et de I'heritage de classes. Du module canon04, nous allons reutiliser la classe Canon() telle quelle, aussi bien pour le logiciel serveur que pour le logiciel client. De ce meme module, nous importerons egalement la classe AppBombardes(), dont nous ferons deriver la classe maitresse de notre application serveur : AppServeur(). Vous constaterez plus loin que celle-ci produira elle-meme la sous-classe AppClient(), toujours par heritage. Du module canon03, nous recupererons la classe Pupitre() dont nous tirerons une version plus adaptee au « controle a distance ». Enfin, deux nouvelles classes viendront s'ajouter aux precedentes, chacune specialisee dans la creation d'un objet thread : la classe ThreadClients(), dont une instance surveillera en permanence le socket destine a receptionner les demandes de connexion de nouveaux clients, et la classe ThreadConnexion(), qui servira a creer autant d'objets sockets que necessaire pour assurer le dialogue avec chacun des clients deja connectes. Ces nouvelles classes seront inspirees de celles que nous avions developpees pour notre serveur de « chat » dans les pages precedentes. La principale difference par rapport a celui-ci est que nous devrons activer un thread specifique pour le code qui gere l'attente et la prise en charge des connexions clientes, afin que l'application principale puisse faire autre chose pendant ce temps. A partir de la, notre plus gros travail consistera a developper un protocole de communication pour le dialogue entre le serveur et ses clients. De quoi est-il question ? Tout simplement de definir la teneur des messages que vont s'echanger les machines connectees. Rassurez-vous : la mise au point de ce « langage » peut etre progressive. On commence par etablir un dialogue de base, puis on y ajoute petit a petit un « vocabulaire » plus etendu. L'essentiel de ce travail peut etre accompli en s 'aidant du logiciel client developpe precedemment pour le systeme de « chat ». On se sert de celui-ci pour envoyer des « ordres » au serveur en cours de developpement, et on corrige celui-ci jusqu'a ce qu'il « obeisse » : en clair, les procedures que Ton met en place progressivement sur le serveur sont testees au fur et a mesure, en reponse aux messages correspondants emis « a la main » a partir du client. 18.7.2 Protocole de communication II va de soi que le protocole decrit ci-apres est tout a fait arbitraire. II serait parfaitement possible de choisir d'autres conventions completement differentes. Vous pouvez bien evidemment critiquer les choix effectues, et vous souhaiterez peut-etre meme les remplacer par d'autres, plus efficients ou plus simples. Vous savez deja que les messages echanges sont de simples chaines de caracteres. Prevoyant que certains de ces messages devront transmettre plusieurs informations a la fois, nous avons decide que chacun d'eux pourrait comporter plusieurs champs, que nous separerons a l'aide de virgules. Lors de la reception de l'un quelconque de ces messages, nous pourrons alors aisem*nt recuperer tous ses composants dans une liste, a l'aide de la methode integree split(). Voici un exemple de dialogue type, tel qu'il peut etre suivi du cote d'un client. Les messages 288. Gerard Swinnen : Apprendre a programmer avec Python entre asterisques sont ceux qui sont recus du serveur ; les autres sont ceux qui sont emis par le client lui-meme : 1 . *serveur OK* 2 . client OK 3. *canons, Thread-3; 104; 228; 1; dark red, Thread-2 ; 454 ; 166; -1 ; dark blue,* 4 . OK 5. *nouveau_canon, Thread-4, 481, 245, -1, dark green, le_votre* 6. orienter,25, 7. feu 8 . *mouvement_de, Thread-4, 549, 280, * 9. feu 10 . *mouvement_de , Thread-4 , 504 , 278 , * 11 . *scores , Thread-4 ; 1 , Thread-3 ; -1 , Thread-2 ; 0 , * 12. *angle, Thread-2, 23, * 13. * angle, Thread-2, 20, * 14. *tir_de, Thread-2, * 1 5 . *mouvement_de , Thread-2 ,407,191,* 16 . *depart_de, Thread-2* 17. *nouveau_canon, Thread-5, 502, 27 6, -1, dark green* Lorsqu'un nouveau client demarre, il envoie une requete de connexion au serveur, lequel lui expedie en retour le message : « serveur OK ». A la reception de ce dernier, le client repond alors en envoyant lui-meme : « client OK ». Ce premier echange de politesses n'est pas absolument indispensable, mais il permet de verifier que la communication passe bien dans les deux sens. Etant done averti que le client est pret a travailler, le serveur lui expedie alors une description des canons deja presents dans le jeu (eventuellement aucun) : identifiant, emplacement sur le canevas, orientation et couleur (ligne 3). En reponse a l'accuse de reception du client (ligne 4), le serveur installe un nouveau canon dans l'espace de jeu, puis il signale les caracteristiques de cette installation non seulement au client qui l'a provoquee, mais egalement a tous les autres clients connectes. Le message expedie au nouveau client comporte cependant une difference (car e'est lui le proprietaire de ce nouveau canon) : en plus des caracteristiques du canon, qui sont fournies a tout le monde, il comporte un champ supplementaire contenant simplement « le votre » (comparez par exemple la ligne 5 avec la ligne 17, laquelle signale la connexion d'un autre joueur). Cette indication supplementaire permet au client proprietaire du canon de distinguer parmi plusieurs messages similaires eventuels, celui qui contient l'identifiant unique que lui a attribue le serveur. Les messages des lignes 6 et 7 sont des commandes envoyees par le client (reglage de la hausse et commande de tir). Dans la version precedente du jeu, nous avions deja convenu que les canons se deplaceraient quelque peu (et au hasard) apres chaque tir. Le serveur effectue done cette operation, et s'empresse ensuite d'en faire connaitre le resultat a tous les clients connectes. Le message recu du serveur a la ligne 8 est done l'indication d'un tel deplacement (les coordonnees fournies sont les coordonnees resultantes pour le canon concerne). La ligne 1 1 reproduit le type de message expedie par le serveur lorsqu'une cible a ete touchee. Les nouveaux scores de tous les joueurs sont ainsi communiques a tous les clients. Les messages serveur des lignes 12, 13 et 14 indiquent les actions entreprises par un autre joueur (reglage de hausse suivi d'un tir). Cette fois encore, le canon concerne est deplace au hasard apres qu'il ait tire (ligne 15). Lignes 16 et 17 : lorsque l'un des clients coupe sa connexion, le serveur en avertit tous les autres, afin que le canon correspondant disparaisse de l'espace de jeu sur tous les postes. A l'inverse, de nouveaux clients peuvent se connecter a tout moment pour participer au jeu. Remarques complementaires : Le premier champ de chaque message indique sa teneur. Les messages envoyes par le client sont tres simples : ils correspondent aux differentes actions entreprises par le joueur (modifications de Gerard Swinnen : Apprendre a programmer avec Python 289. Tangle de tir et commandes de feu). Ceux qui sont envoyes par le serveur sont un peu plus complexes. La plupart d'entre eux sont expedies a tous les clients connectes, afin de les tenir informes du deroulement du jeu. En consequence, ces messages doivent mentionner l'identifiant du joueur qui a commande une action ou qui est concerne par un changement quelconque. Nous avons vu plus haut que ces identifiants sont des noms generes automatiquement par le gestionnaire de threads du serveur, chaque fois qu'un nouveau client se connecte. Certains messages concernant l'ensemble du jeu contiennent plusieurs informations par champ. Dans ce cas, les differents « sous-champs » sont separes par des points-virgules (lignes 3 et 1 1). 18.7.3 Programme serveur : premiere partie Vous trouverez dans les pages qui suivent le script complet du programme serveur. Nous vous le presentons en trois morceaux successifs afin de rapprocher les commentaires du code correspondant, mais la numerotation de ses lignes est continue. Bien qu'il soit deja relativement long et complexe, vous estimerez probablement qu'il merite d'etre encore perfectionne, notamment au niveau de la presentation generale. Nous vous laisserons le soin d'y ajouter vous-meme tous les complements qui vous sembleront utiles (par exemple, une proposition de choisir les coordonnees de la machine hote au demarrage, une barre de menus, etc.) : 1 . ####################################################### 2 . # Jeu des bombardes - partie serveur # 3. # (C) Gerard Swinnen, Liege (Belgique)- Juillet 2004 # 4. # Licence : GPL # 5. # Avant d'executer ce script, verifiez que l'adresse # 6 . # IP ci-dessous soit bien celle de la machine hote . # 7. # Vous pouvez choisir un numero de port different, ou # 8. # changer les dimensions de 1 ' espace de jeu. # 9. # Dans tous les cas, verifiez que les memes choix ont # 10. # ete effectues pour chacun des scripts clients. # 11 . ####################################################### 12 . 13. host, port = '192.168.0.235', 35000 14. largeur, hauteur = 700, 400 # dimensions de l'espace de jeu 15 . 16. from Tkinter import * 17. import socket, sys, threading, time 18. import canon03 19. from canon04 import Canon, AppBombardes 20. 21. class Pupitre (canon03 .Pupitre) : 22. """Pupitre de pointage ameliore""" 23. def init (self, boss, canon): 24. canon03. Pupitre. init (self, boss, canon) 25. 26. def tirer (self) : 27. "declencher le tir du canon associe" 28 . self . appli . tir_canon (self . canon . id) 29. 30. def orienter (self , angle): 31. "ajuster la hausse du canon associe" 32. self . appli . orienter_canon (self . canon . id, angle) 33 . 34. def valeur_score (self , sc =None) : 35. "imposer un nouveau score , ou lire le score existant" 36. if sc == None: 37. return self. score 38. else: 39. self. score =sc 40. self .points . config (text = ' %s ' % self. score) 41. 42. def inactiver (self ) : 43. "desactiver le bouton de tir et le systeme de reglage d' angle" 290. Gerard Swinnen : Apprendre a programmer avec Python 44. self .bTir.config (state =DISABLED) 45. self . regl . config (state =DISABLED) 46. 47. def activer (self ) : 48. "activer le bouton de tir et le systeme de reglage d' angle" 49. self .bTir.config (state =NORMAL) 50. self . regl . config (state =NORMAL ) 51. 52. def reglage (self , angle): 53. "changer la position du curseur de reglage" 54. self .regl. config (state =NORMAL) 55. self . regl . set (angle) 56. self . regl . config (state =DISABLED) 57. La classe PupitreO est constitute par derivation de la classe de meme nom importee du modune canon03. Elle herite done toutes les caracteristiques de celle-ci, mais nous devons surcharged ses methodes tirer() et orienter() : Dans la version monoposte du logiciel, en effet, chacun des pupitres pouvait commander directement l'objet canon correspondant. Dans cette version reseau, par contre, ce sont les clients qui controlent a distance le fonctionnement des canons. Par consequent, les pupitres qui apparaissent dans la fenetre du serveur ne peuvent etre que de simples repetiteurs des manoeuvres effectuees par les joueurs sur chaque client. Le bouton de tir et le curseur de reglage de la hausse sont done desactives, mais les indications fournies obeissent aux injunctions qui leur sont adressees par l'application principale. Cette nouvelle classe Pupitre() sera egalement utilisee telle quelle dans chaque exemplaire du programme client. Dans la fenetre de celui-ci comme dans celle du serveur, tous les pupitres seront affiches comme des repetiteurs, mais l'un d'entre eux cependant sera completement fonctionnel : celui qui correspond au canon du joueur. Toutes ces raisons expliquent egalement l'apparition des nouvelles methodes : activer(), desactiver(), reglage() et valeur_score(), qui seront elles aussi invoquees par l'application principale, en reponse aux messages-instructions echanges entre le serveur et ses clients. La classe ThreadConnexion() ci-dessous sert a instancier la serie d'objets threads qui s'occuperont en parallele de toutes les connexions lancees par les clients. Sa methode run() contient la fonctionnalite centrale du serveur, a savoir la boucle d'instructions qui gere la reception des messages provenant d'un client particulier, lesquels entrainent chacun toute une cascade de reactions. Vous y trouverez la mise en oeuvre concrete du protocole de communication decrit dans les pages precedentes (certains messages etant cependant generes par les methodes depl_aleat_canon() et goal() de la classe AppServeur() decrite plus loin). 58. class ThreadConnexion (threading . Thread) : 59. """objet thread gestionnaire d'une connexion client""" 60. def init (self, boss, conn): 61 . threading. Thread. init (self) 62. self . connexion = conn # ref. du socket de connexion 63. self.app = boss # ref. de la fenetre application 64 . 65. def run (self): 66. "actions entreprises en reponse aux messages recus du client" 67. nom = self . getName ( ) # id. du client = nom du thread 68. while 1: 69. msgClient = self . connexion . recv (1024) 70. print "**%s** de %s" % (msgClient, nom) 71. deb = msgClient. split (',') [0] 78 Rappel : dans une classe derivee, vous pouvez definir une nouvelle methode avec le meme nom qu'une methode de la classe parente, afm de modifier sa fonctionnalite dans la classe derivee. Cela s'appelle surcharger cette methode (voir aussi page 167). Gerard Swinnen : Apprendre a programmer avec Python 291. 72. if deb == "fin" or deb =="": 73. self . app . enlever_canon (nom) 74 . # signaler le depart de ce canon aux autres clients : 75. self . app . verrou . acquire ( ) 76. for cli in self . app . conn_client : 77. if cli != nom: 78. message = "depart_de, %s" % nom 79. self .app. conn_client [cli] . send (message) 80. self .app. verrou. release () 81. # fermer le present thread : 82 . break 83. elif deb =="client OK": 84 . # signaler au nouveau client les canons de ja enregistres : 85. msg ="canons," 86. for g in self . app . guns : 87. gun = self . app . guns [g] 88. msg =msg +"%s; %s; %s; %s; %s, " % \ 89. (gun. id, gun.xl, gun.yl, gun. sens, gun.coul) 90. self . app . verrou . acquire ( ) 91. self . connexion . send (msg) 92. # attendre un accuse de reception ('OK') : 93. self . connexion . recv (100) 94. self .app. verrou. release () 95. # ajouter un canon dans 1 ' espace de jeu serveur. 96. # la methode invoquee renvoie les caract . du canon cree : 97. x, y, sens, coul = self . app . a jouter_canon (nom) 98 . # signaler les caract . de ce nouveau canon a tous les 99. # clients deja connectes : 100. self . app . verrou . acquire ( ) 101. for cli in self . app . conn_client : 102. msg ="nouveau_canon, %s, %s, %s, %s, %s" % \ 103. (nom, x, y, sens, coul) 104. # pour le nouveau client, ajouter un champ indiquant 105. # que le message concerne son propre canon : 106. if cli == nom: 107. msg =msg +",le_votre" 108 . self . app . conn_client [cli] . send (msg) 109. self .app. verrou. release () 110. elif deb =='feu': 111. self . app . tir_canon (nom) 112 . # Signaler ce tir a tous les autres clients : 113. self . app . verrou . acquire ( ) 114. for cli in self . app . conn_client : 115. if cli != nom: 116. message = "tir_de,%s," % nom 117. self .app. conn_client [cli] . send (message) 118. self .app. verrou. release () 119. elif deb =="orienter" : 120. t =msgClient . split (',' ) 121. # on peut avoir recu plusieurs angles, utiliser le dernier: 122. self . app . orienter_canon (nom, t[-2]) 123. # Signaler ce changement a tous les autres clients : 124. self . app . verrou . acquire ( ) 125. for cli in self . app . conn_client : 126. if cli != nom: 127. # virgule terminale, car messages parfois groupes : 128. message = "angle, %s, %s, " % (nom, t[-2]) 129. self .app. conn_client [cli] . send (message) 130. self .app. verrou. release () 131. 132 . # Fermeture de la connexion : 133. self . connexion . close () # couper la connexion 134. del self . app . conn_client [nom] # suppr. sa ref. dans le dictionn. 135. self . app. afficher ( "Client %s deconnecte . \n" % nom) 136. # Le thread se termine ici 137 . 292. Gerard Swinnen : Apprendre a programmer avec Python 18.7.4 Synchronisation de threads concurrents a I'aide de « verrous » (thread locks) Au cours de votre examen du code ci-dessus, vous aurez certainement remarque la structure particuliere des blocs destructions par lesquelles le serveur expedie un meme message a tous ses clients. Considerez par exemple les lignes 74 a 80 : La ligne 75 active la methode acquire() d'un objet « verrou » qui a ete cree par le constructeur de l'application principale (voir plus loin). Cet objet est une instance de la classe Lock(), laquelle fait partie du module threading que nous avons importe en debut de script. Les lignes suivantes (76 a 79) provoquent l'envoi d'un message a tous les clients connectes (sauf un). Ensuite, l'objet « verrou » est a nouveau sollicite, cette fois pour sa methode release(). A quoi cet objet « verrou » peut-il done bien servir ? Puisqu'il est produit par une classe du module threading, vous pouvez deviner que son utilite concerne les threads. En fait, de tels objets « verrous » servent a synchroniser les threads concurrents. De quoi s'agit-il ? Vous savez que le serveur demarre un thread different pour chacun des clients qui se connecte. Ensuite, tous ces threads fonctionnent en parallele. II existe done un risque que de temps a autre, deux ou plusieurs de ces threads essaient d'utiliser une ressource commune en meme temps. Dans les lignes de code que nous venons de discuter, par exemple, nous avons affaire a un thread qui souhaite exploiter quasiment toutes les connexions presentes pour poster un message. II est done parfaitement possible que pendant ce temps, un autre thread tente d'exploiter lui aussi l'une ou l'autre de ces connexions, ce qui risque de provoquer un dysfonctionnement (en l'occurrence, la superposition chaotique de plusieurs messages). Un tel probleme de concurrence entre threads peut etre resolu par l'utilisation d'un objet-verrou {thread lock). Un tel objet n'est cree qu'en un seul exemplaire, dans un espace de noms accessible a tous les threads concurrents. II se caracterise essentiellement par le fait qu'il se trouve toujours dans l'un ou l'autre de deux etats : soit verrouille, soit deverrouille. Son etat initial est l'etat deverrouille. Utilisation : Lorsqu'un thread quelconque s'apprete a acceder a une ressource commune, il active d'abord la methode acquire() du verrou. Si celui-ci etait dans l'etat deverrouille, il se verrouille, et le thread demandeur peut alors utiliser la ressource commune, en toute tranquillite. Lorsqu'il aura fini d'utiliser la ressource, il s'empressera cependant d'activer la methode release() du verrou, ce qui le fera repasser dans l'etat deverrouille. En effet : Si un autre thread concurrent active lui aussi la methode acquire() du verrou, alors que celui-ci est dans l'etat verrouille, la methode « ne rend pas la main », provoquant le blocage de ce thread, lequel suspend done son activite jusqu'a ce que le verrou repasse dans l'etat deverrouille. Ceci l'empeche done d'acceder a la ressource commune durant tout le temps ou un autre thread s'en serf Lorsque le verrou est deverrouille, l'un des threads en attente (il peut en effet y en avoir plusieurs) reprend alors son activite, et ainsi de suite. L'objet verrou memorise les references des threads bloques, de maniere a n'en debloquer qu'un seul a la fois lorsque sa methode release() est invoquee. II faut done toujours veiller a ce que chaque thread qui active la methode acquire() du verrou avant d'acceder a une ressource, active egalement sa methode release() peu apres. Pour autant que tous les threads concurrents respectent la meme procedure, cette technique simple empeche done qu'une ressource commune soit exploitee en meme temps par plusieurs d'entre eux. On dira dans ce cas que les threads ont ete synchronises. Gerard Swinnen : Apprendre a programmer avec Python 293. 18.7.5 Programme serveur : suite et fin Les deux classes ci-dessous completent le script serveur. Le code implements dans la classe ThreadClients() est assez similaire a celui que nous avions developpe precedemment pour le corps d'application du logiciel de « Chat ». Dans le cas present, toutefois, nous le placons dans une classe derivee de Thread(), parce que devons faire fonctionner ce code dans un thread independant de celui de l'application principale. Celui-ci est en effet deja completement accapare par la boucle mainloop() de l'interface graphique 79 . La classe AppServeur() derive de la classe AppBombardes() du module canon04. Nous lui avons ajoute un ensemble de methodes complementaires destinees a executer toutes les operations qui resulteront du dialogue entame avec les clients. Nous avons deja signal e plus haut que les clients instancieront chacun une version derivee de cette classe (afin de profiter des memes definitions de base pour la fenetre, le canevas, etc.). 138. class ThreadClients (threading. Thread) : 139. """objet thread gerant la connexion de nouveaux clients""" 140. def init (self, boss, connex) : 141 . threading. Thread. init (self) 142. self. boss = boss # ref. de la fenetre application 143. self. connex = connex # ref. du socket initial 144 . 145. def run (self): 146. "attente et prise en charge de nouvelles connexions clientes" 147. txt ="Serveur pret, en attente de requetes ...\n" 148. self .boss . afficher (txt) 149. self . connex . listen (5) 150. # Gestion des connexions demandees par les clients : 151. while 1: 152. nouv_conn, adresse = self . connex . accept ( ) 153. # Creer un nouvel objet thread pour gerer la connexion : 154. th = ThreadConnexion (self . boss , nouv_conn) 155. th. start () 156. it = th.getName() # identifiant unique du thread 157 . # Memoriser la connexion dans le dictionnaire : 158. self . boss . enregistrer_connexion (nouv_conn, it) 159. # Afficher : 160. txt = "Client %s connecte, adresse IP %s, port %s.\n" %\ 161. (it, adresse [0], adresse [1]) 162. self .boss . afficher (txt) 163. # Commencer le dialogue avec le client : 164. nouv_conn . send ( "serveur OK") 165. 166. class AppServeur (AppBombardes) : 167. """fenetre principale de l'application (serveur ou client)""" 168. def init (self, host, port, larg_c, haut_c) : 169. self. host, self. port = host, port 170. AppBombardes. init (self, larg_c, haut_c) 171. self. active =1 # temoin d'activite 172 . # veiller a quitter proprement si 1 ' on ref erme la fenetre : 173 . self . bind ( ' ' , self . fermer_threads) 174 . 175. def specif icites (self ) : 176. "preparer les objets specifiques de la partie serveur" 177. self .master, title ( '«< Serveur pour le jeu des bombardes »> ' ) 178. 179. # widget Text, associe a une barre de defilement : 180. st =Frame(self) 181. self. avis =Text(st, width =65, height =5) 182. self . avis .pack (side =LEFT) 183. scroll =Scrollbar (st, command =self . avis . yview) 184. self . avis . configure (yscrollcommand =scroll.set) 79 Nous detaillerons cette question quelques pages plus loin, car elle ouvre quelques perspectives interessantes. Voir : Optimiser les animations a l'aide des threads, page 300. 294. Gerard Swinnen : Apprendre a programmer avec Python 185. scroll. pack (side =RIGHT, fill =Y) 186. st. pack () 187. 188. # partie serveur reseau : 189. self . conn_client = {} # dictionn. des connexions clients 190. self.verrou =threading . Lock ( ) # verrou pour synchroniser threads 191. # Initialisation du serveur - Mise en place du socket : 192. connexion = socket . socket (socket .AF_INET, socket . SOCK_STREAM) 193. try: 194. connexion. bind ( (self .host, self .port)) 195. except socket . error : 196. txt ="La liaison du socket a l'hote %s, port %s a echoue.\n" %\ 197. (self. host, self. port) 198. self . avis . insert (END, txt) 199. self.accueil =None 200. else: 201. # demarrage du thread guettant la connexion des clients : 202. self.accueil = ThreadClients (self , connexion) 203. self . accueil . start () 204. 205. def depl_aleat_canon (self , id): 206. "deplacer aleatoirement le canon " 207. x, y = AppBombardes . depl_aleat_canon (self , id) 208. # signaler ces nouvelles coord, a tous les clients : 209. self .verrou. acquire () 210. for cli in self . conn_client : 211. message = "mouvement_de, %s, %s, %s, " % (id, x, y) 212. self . conn_client [cli] . send (message) 213. self .verrou. release () 214. 215. def goal(self, i, j) : 216. "le canon signale qu'il a atteint 1 ' adversaire " 217. AppBombardes . goal (self , i, j) 218. # Signaler les nouveaux scores a tous les clients : 219. self .verrou. acquire () 220. for cli in self . conn_client : 221. msg ='scores,' 222. for id in self.pupi: 223. sc = self .pupi [id] . valeur_score () 224. msg = msg +"%s;%s," % (id, sc) 225. self . conn_client [cli] .send (msg) 226. time . sleep (. 5) # pour mieux separer les messages 227. self .verrou. release () 228. 229. def a jouter_canon (self , id): 230. "instancier un canon et un pupitre de nom dans 2 dictionn." 231. # on alternera ceux des 2 camps : 232. n = len (self . guns) 233. if n %2 ==0: 234. sens = -1 235. else: 236. sens = 1 237. x, y = self . coord_aleat (sens) 238. coul =('dark blue', 'dark red', 'dark green', 'purple', 239. 'dark cyan', 'red', 'cyan', 'orange', 'blue', 'violet') [n] 240. self . guns [id] = Canon (self . jeu, id, x, y, sens, coul) 241. self .pupi [id] = Pupitre (self , self . guns [id] ) 242. self .pupi [id] . inactiver () 243. return (x, y, sens, coul) 244. 245. def enlever_canon (self , id): 246. "retirer le canon et le pupitre dont 1 ' identif iant est " 247. if self. active ==0: # la fenetre a ete refermee 248. return 249. self .guns [id] .ef facer () 250. del self .guns [id] 251. self .pupi [id] . destroy () 252. del self .pupi [id] 253. 254. def orienter_canon (self , id, angle): Gerard Swinnen : Apprendre a programmer avec Python 295. 255 . "regler la hausse du canon a la valeur " 256 . self . guns [id] . orienter (angle) 257 . self .pupi [id] . reglage (angle) o c o 258 . 259 . def tir_canon (self , id): 260 . "declencher le tir du canon " 261 . self .guns [id] .feu() 262 . 263 . def enregistrer_connexion (self , conn, it): "Memoriser la connexion dans un dictionnaire" 264 . 265 . self . conn_client [it] = conn 266 . 267 . def af f icher (self , txt) : 268 . "afficher un message dans la zone de texte" 269 . self . avis . insert (END, txt) 270 . 271 . def fermer_threads (self , evt) : 272 . "couper les connexions existantes et fermer les threads" 273 . # couper les connexions etablies avec tous les clients : 274 . for id in self . conn_client : 275 . self . conn_client [id] . send ( ' fin ' ) 276 . # forcer la terminaison du thread serveur qui attend les requetes Oil £.11. if self.accueil != None: 278. self . accueil . _Thread stop ( ) 279. self. active =0 # empecher acces ulterieurs a Tk 280. 281. if name == ' main ' : 282 . AppServeur (host, port, largeur, hauteur) .mainloop () Commentaires : • Ligne 173 : II vous arrivera de temps a autre de vouloir « intercepter » l'ordre de fermeture de l'application que l'utilisateur declenche en quittant votre programme, par exemple parce que vous voulez forcer la sauvegarde de donnees importantes dans un fichier, ou fermer aussi d'autres fenetres, etc. II suffit pour ce faire de detecter l'evenement , comme nous le faisons ici pour forcer la terminaison de tous les threads actifs. • Lignes 179 a 186 : Au passage, voici comment vous pouvez associer une barre de defilement {widget Scrollbar) a un widget Text (vous pouvez faire de meme avec un widget Canvas), sans faire appel a la bibliotheque Pmw 80 . • Ligne 190 : Instanciation de l'obet « verrou » permettant de synchroniser les threads. • Lignes 202, 203 : Instanciation de l'objet thread qui attendra en permanence les demandes de connexion des clients potentiels. • Lignes 205 a213,215a 227 : Ces methodes surchargent les methodes de meme nom heritees de leur classe parente. Elles commencent par invoquer celles-ci pour effectuer le meme travail (lignes 207, 217), puis ajoutent leur fonctionnalite propre, laquelle consiste a signaler a tout le monde ce qui vient de se passer. • Lignes 229 a 243 : Cette methode instancie un nouveau poste de tir, chaque fois qu'un nouveau client se connecte. Les canons sont places alternativement dans le camp de droite et dans celui de gauche, procedure qui pourrait bien evidemment etre amelioree. La liste des couleurs prevues limite le nombre de clients a 10, ce qui devrait suffire. 80 Voir : Python Mega Widgets, page 207. Gerard Swinnen : Apprendre a programmer avec Python 296. 18.7.6 Programme client Le script correspondant au logiciel client est reproduit ci-apres. Comme celui qui correspond au serveur, il est relativement court, parce qu'il utilise lui aussi l'importation de modules et l'heritage de classes. Le script serveur doit avoir ete sauvegarde dans un fichier-module nomme canon_serveur.py. Ce fichier doit etre place dans le repertoire courant, de meme que les fichiers- modules canon03.py et canon04.py qu'il utilise lui-meme. De ces modules ainsi importes, le present script utilise les classes Canon() et Pupitre() a l'identique, ainsi qu'une forme derivee de la classe AppServeur(). Dans cette derniere, de nombreuses methodes ont ete surchargees, afin d'adapter leur fonctionnalite. Considerez par exemple les methodes goal() et depl_aleat_canon(), dont la variante surchargee ne fait plus rien du tout (instruction pass), parce que le calcul des scores et le repositionnement des canons apres chaque tir ne peuvent etre effectues que sur le serveur seulement. C'est dans la methode run() de la classe ThreadSocket() (lignes 86 a 126) que se trouve le code traitant les messages echanges avec le serveur. Nous y avons d'ailleurs laisse une instruction print (a la ligne 88) afin que les messages recus du serveur apparaissent sur la sortie standard. Si vous realisez vous-meme une forme plus definitive de ce jeu, vous pourrez bien evidemment supprimer cette instruction. 1 . ####################################################### 2 . # Jeu des bombardes - partie cliente # 3. # (C) Gerard Swinnen, Liege (Belgique) - Juillet 2004 # 4. # Licence : GPL # 5. # Avant d'executer ce script, verifiez que l'adresse, # 6. # le numero de port et les dimensions de l'espace de # 7 . # jeu indiquees ci-dessous correspondent exactement # 8. # a ce qui a ete defini pour le serveur. # 9 . ####################################################### 10. 11. from Tkinter import * 12. import socket, sys, threading, time 13. from canon_serveur import Canon, Pupitre, AppServeur 14. 15. host, port = '192.168.0.235', 35000 16. largeur, hauteur = 700, 400 # dimensions de l'espace de jeu 17. 18. class AppClient (AppServeur) : 19. def init (self, host, port, larg_c, haut_c) : 20. AppServeur. init (self, host, port, larg_c, haut_c) 21. 22. def specif icites (self ) : 23. "preparer les objets specif iques de la partie client" 24. self .master, title (' <<< Jeu des bombardes >»') 25. self.connex =ThreadSocket (self , self. host, self .port) 26. self .connex. start () 27. self. id =None 28. 29. def a jouter_canon (self , id, x, y, sens, coul) : 30. "instancier 1 canon et 1 pupitre de nom dans 2 dictionnaires " 31. self . guns [id] = Canon (self . jeu, id, int (x) , int (y) , int (sens) , coul) 32. self .pupi [id] = Pupitre (self , self . guns [id] ) 33. self .pupi [id] . inactiver () 34. 35. def activer_pupitre_personnel (self , id): 36. self. id =id # identifiant recu du serveur 37. self .pupi [id] .activer () 38 . 39. def tir_canon (self , id): 40. r = self . guns [id] . feu () # renvoie False si enraye 41. if r and id == self. id: 42. self . connex . signaler_tir ( ) Gerard Swinnen : Apprendre a programmer avec Python 297. 43. 44. def imposer_score (self , id, sc) : 45. self .pupi [id] . valeur_score (int (sc) ) 46. 47. def deplacer_canon (self , id, x, y) : 48. "note: les valeurs de x et y sont recues en tant que chaines" 49. self .guns [id] .deplacer (int (x) , int (y) ) 50. 51. def orienter_canon (self , id, angle): 52. "regler la hausse du canon a la valeur " 53. self . guns [id] . orienter (angle) 54. if id == self. id: 55 . self . connex . signaler_angle (angle) 56. else: 57. self .pupi [id] . reglage (angle) 58. 59. def fermer_threads (self , evt) : 60. "couper les connexions existantes et refermer les threads" 61. self . connex . terminer () 62. self. active =0 # empecher acces ulterieurs a Tk 63. 64. def depl_aleat_canon (self , id): 65 . pass # => methode inoperante 66. 67. def goal (self, a, b) : 68 . pass # => methode inoperante 69. 70. 71. class ThreadSocket (threading. Thread) : 72. """objet thread gerant l'echange de messages avec le serveur""" 73. def init (self, boss, host, port): 74 . threading. Thread. init (self) 75. self.app = boss # ref. de la fenetre application 76. # Mise en place du socket - connexion avec le serveur : 77. self . connexion = socket . socket (socket .AF_INET, socket . SOCK_STREAM) 78. try: 79. self .connexion. connect ( (host, port)) 80 . except socket . error : 81. print "La connexion a echoue . " 82. sys.exit() 83. print "Connexion etablie avec le serveur." 84. 85 . def run (self ) : 86. while 1: 87. msg_recu = self . connexion . recv (1024) 88. print "*%s*" % msg_recu 89. # le message recu est d'abord convert! en une liste : 90. t =msg_recu . split (',' ) 91. if t[0] =="" or t[0] =="fin": 92 . # f ermer le present thread : 93 . break 94. elif t[0] ==" serveur OK": 95. self . connexion . send ( "client OK") 96. elif t[0] =="canons": 97. self . connexion . send ( "OK" ) # accuse de reception 98 . # eliminons le ler et le dernier element de la liste . 99. # ceux qui restent sont eux-memes des listes : 100. lc = t[l:-l] 101. # chacune est la description complete d'un canon : 102. for g in lc: 103. s = g.splitC ; ') 104. self . app . a jouter_canon (s [0] , s[l], s[2], s[3], s[4]) 105. elif t[0] =="nouveau_canon" : 106. self .app.ajouter_canon(t[l] , t[2], t[3], t[4], t[5]) 107. if len(t) >6: 108 . self . app . activer_pupitre_personnel (t [1] ) 109. elif t[0] =='angle': 110. # il se peut que l'on ait recu plusieurs infos regroupees . 111. # on ne considere alors que la premiere : 112. self . app . orienter_canon (t [1] , t[2]) 298. Gerard Swinnen : Apprendre a programmer avec Python 113. elif t[0] =="tir_de": 114. self .app. tir_canon (t [1] ) 115. elif t[0] =="scores": 116. # eliminons le ler et le dernier element de la liste. 117 . # ceux qui restent sont eux-memes des listes : 118. Ic = t[l:-l] 119. # chaque element est la description d'un score : 120. for g in lc: 121. s = g. split ('; ') 122. self .app. imposer_score (s [0] , s[l]) 123. elif t[0] =="mouvement_de" : 124 . self .app. deplacer_canon (t[l],t[2],t[3]) 125. elif t[0] =="depart_de" : 126. self . app . enlever_canon (t [1] ) 127. 128. # Le thread se termine ici . 129. print "Client arrete. Connexion interrompue . " 130. self . connexion . close () 131. 132. def signaler_tir (self ) : 133. self . connexion . send (' feu ' ) 134. 135. def signaler_angle (self , angle): 136. self . connexion . send (' orienter, %s, ' % angle) 137. 138. def terminer (self ) : 139. self . connexion . send (' fin ' ) 140. 141. # Programme principal 142. if name == ' main ' : 143. AppClient (host, port, largeur, hauteur) .mainloop ( ) 144. Commentaires : • Lignes 15, 16 : Vous pouvez vous-meme perfectionner ce script en lui ajoutant un formulaire qui demandera ces valeurs a l'utilisateur au cours du demarrage. • Lignes 19 a 27: Le constructeur de la classe parente se termine en invoquant la methode specificites(). On peut done placer dans celle-ci ce qui doit etre construit differemment dans le serveur et dans les clients. (Le serveur instancie notamment un widget text qui n'est pas repris dans les clients ; l'un et l'autre demarrent des objets threads differents pour gerer les connexions). • Lignes 39 a 42 : Cette methode est invoquee chaque fois que l'utilisateur enfonce le bouton de tir. Le canon ne peut cependant pas effectuer des tirs en rafale. Par consequent, aucun nouveau tir ne peut etre accepte tant que l'obus precedent n'a pas termine sa trajectoire. C'est la valeur « vraie » ou « fausse » renvoyee par la methode feu() de l'objet canon qui indique si le tir a ete accepte ou non. On utilise cette valeur pour ne signaler au serveur (et done aux autres clients) que les tirs qui ont effectivement eu lieu. • Lignes 105 a 108 : Un nouveau canon doit etre ajoute dans l'espace de jeu de chacun (e'est-a-dire dans le canevas du serveur, et dans le canevas de tous les clients connectes), chaque fois qu'un nouveau client se connecte. Le serveur envoie done a ce moment un meme message a tous les clients pour les informer de la presence de ce nouveau partenaire. Mais le message envoye a celui-ci en particulier comporte un champ supplemental (lequel contient simplement la chaine « le votre »), afin que ce partenaire sache que ce message concerne son propre canon, et qu'il puisse done activer le pupitre correspondant, tout en memorisant l'identifiant qui lui a ete attribue par le serveur (voir egalement les lignes 35 a 37). Gerard Swinnen : Apprendre a programmer avec Python 299. Conclusions et perspectives : Cette application vous a ete presentee dans un but didactique. Nous y avons deliberement simplifie un certain nombre de problemes. Par exemple, si vous testez vous-meme ces logiciels, vous constaterez que les messages echanges sont souvent rassembles en « paquets », ce qui necessiterait d'affiner les algorithmes mis en place pour les interpreter. De meme, nous avons a peine esquisse le mecanisme fondamental du jeu : repartition des joueurs dans les deux camps, destruction des canons touches, obstacles divers, etc. II vous reste bien des pistes a explorer ! (18) Exercices : 18.1. Simplifiez le script correspondant au client de « chat » decrit a la page 283, en supprimant l'un des deux objets threads. Arrangez-vous par exemple pour traiter remission de messages au niveau du thread principal. 18.2. Modifiez le jeu des bombardes (version monoposte) du chapitre 15 (voir pages 227 et suivantes), en ne gardant qu'un seul canon et un seul pupitre de pointage. Ajoutez-y une cible mobile, dont le mouvement sera gere par un objet thread independant (de maniere a bien separer les portions de code qui controlent l'animation de la cible et celle du boulet). 18.8 Utilisation de threads pour optimiser les animations. Le dernier exercice propose a la fin de la section precedente nous suggere une methodologie de developpements d'applications qui peut se reveler particulierement interessante, dans le cas de jeux video impliquant plusieurs animations simultanees. En effet : si vous programmez les differents elements animes d'un jeu comme des objets independants fonctionnant chacun sur son propre thread, alors non seulement vous vous simplifiez la tache et vous ameliorez la lisibilite de votre script, mais encore vous augmentez la vitesse d' execution et done la fluidite de ces animations. Pour arriver a ce resultat, vous devrez abandonner la technique de temporisation que vous avez exploitee jusqu'ici, mais celle que vous allez utiliser a sa place est finalement plus simple ! 18.8.1 Temporisation des animations a I'aide de after() Dans toutes les animations que nous avons decrites jusqu'a present, le « moteur » etait constitute a chaque fois par une fonction contenant la methode after(), laquelle est associee d'office a tous les widgets Tkinter. Vous savez que cette methode permet d'introduire une temporisation dans le deroulement de votre programme : un chronometre interne est active, de telle sorte qu'apres un intervalle de temps convenu, le systeme invoque automatiquement une fonction quelconque. En general, e'est la fonction contenant after() qui est elle-meme invoquee : on realise ainsi une boucle recursive, dans laquelle il reste a programmer les deplacements des divers objets graphiques. Vous devez bien comprendre que pendant l'ecoulement de l'intervalle de temps programme a I'aide de la methode after(), votre application n'est pas du tout « figee ». Vous pouvez par exemple pendant ce temps : cliquer sur un bouton, redimensionner la fenetre, effectuer une entree clavier, etc. Comment cela est-il rendu possible ? Nous avons mentionne deja a plusieurs reprises le fait que les applications graphiques modernes comportent toujours une sorte de moteur qui « tourne » continuellement en tache de fond : ce dispositif se met en route lorsque vous activez la methode mainloop() de votre fenetre principale. Comme son nom l'indique fort bien, cette methode met en oeuvre une boucle repetitive perpetuelle, du meme type que les boucles while que vous connaissez bien. De nombreux mecanismes sont integres a ce « moteur ». L'un d'entre eux consiste a receptionner tous les evenements qui se 300. Gerard Swinnen : Apprendre a programmer avec Python produisent, et a les signaler ensuite a l'aide de messages appropries aux programmes qui en font la demande (voir : Programmes pilotes par des evenements, page 85), d'autres controlent les actions a effectuer au niveau de l'affichage, etc. Lorsque vous faites appel a la methode after() d'un widget, vous utilisez en fait un mecanisme de chronometrage qui est integre lui aussi a mainloop(), et c'est done ce gestionnaire central qui declenche l'appel de fonction que vous souhaitez, apres un certain intervalle de temps. La technique d'animation utilisant la methode after() est la seule possible pour une application fonctionnant toute entiere sur un seul thread, parce que c'est la boucle mainloop() qui dirige l'ensemble du comportement d'une telle application de maniere absolue. C'est notamment elle qui se charge de redessiner tout ou partie de la fenetre chaque fois que cela s'avere necessaire. Pour cette raison, vous ne pouvez pas imaginer de construire un moteur d'animation qui redefinirait les coordonnees d'un objet graphique a l'interieur d'une simple boucle while, par exemple, parce que pendant tout ce temps l'execution de mainloop() resterait suspendue, ce qui aurait pour consequence que pendant tout ce temps aucun objet graphique ne serait redessine (en particulier celui que vous souhaitez mettre en mouvement !). En fait, toute l'application apparaitrait figee, aussi longtemps que la boucle while ne serait pas interrompue. Puisqu'elle est la seule possible, c'est done cette technique que nous avons utilisee jusqu'a present dans tous nos exemples d'applications mono-thread. Elle comporte cependant un inconvenient genant : du fait du grand nombre d'operations prises en charge a chaque iteration de la boucle mainloop(), la temporisation que Ton peut programmer a l'aide de after() ne peut pas etre tres courte. Par exemple, elle ne peut guere descendre en dessous de 15 ms sur un PC typique (processeur de type Pentium IV, f = 1,5 GHz). Vous devez tenir compte de cette limitation si vous souhaitez developper des animations rapides. Un autre inconvenient lie a l'utilisation de la methode after() reside dans la structure de la boucle d'animation (a savoir une fonction ou une methode « recursive », e'est-a-dire qui s'appelle elle- meme) : il n'est pas toujours simple en effet de bien maitriser ce genre de construction logique, en particulier si Ton souhaite programmer l'animation de plusieurs objets graphiques independants, dont le nombre ou les mouvements doivent varier au cours du temps. 18.8.2 Temporisation des animations a l'aide de time.sleepQ Vous pouvez ignorer les limitations de la methode after() evoquees ci-dessus, si vous en confiez l'animation de vos objets graphiques a des threads independants. En procedant ainsi, vous vous liberez de la tutelle de mainloop(), et il vous est permis alors de construire des procedures d'animation sur la base de structures de boucles plus « classiques », utilisant l'instruction while ou l'instruction for par exemple. Au coeur de chacune de ces boucles, vous devez cependant toujours veiller a inserer une temporisation pendant laquelle vous « rendez la main » au systeme d' exploitation (afin qu'il puisse s'occuper des autres threads). Pour ce faire, vous ferez appel a la fonction sleep() du module time. Cette fonction permet de suspendre l'execution du thread courant pendant un certain intervalle de temps, pendant lequel les autres threads et applications continuent a fonctionner. La temporisation ainsi produite ne depend pas de mainloop(), et par consequent, elle peut etre beaucoup plus courte que celle que vous autorise la methode after(). Attention : cela ne signifie pas que le rafraichissem*nt de l'ecran sera lui-meme plus rapide, car ce rafraichissem*nt continue a etre assure par mainloop(). Vous pourrez cependant accelerer fortement les differents mecanismes que vous installez vous-meme dans vos procedures d'animation. Dans un logiciel de jeu, par exemple, il est frequent d'avoir a comparer periodiquement les positions de deux mobiles (tels qu' un projectile et une cible), afin de pouvoir entreprendre une Gerard Swinnen : Apprendre a programmer avec Python 301. action lorsqu'ils se rejoignent (explosion, ajout de points a un score, etc.)- Avec la technique d'animation decrite ici, vous pouvez effectuer beaucoup plus souvent ces comparaisons et done esperer un resultat plus precis. De meme, vous pouvez augmenter le nombre de points pris en consideration pour le calcul d'une trajectoire en temps reel, et done affiner celle-ci. Remarque : Lorsque vous utilisez la methode afterQ, vous devez lui indiquer la temporisation souhaitee en millisecondes, sous la forme d'un argument entier. Lorsque vous faites appel a la fonction sleep(), par contre, I'argument que vous transmettez doit etre exprime en secondes, sous la forme d'un reel (float). Vous pouvez cependant utiliser des tres petites valeurs (0.0003 par ex.). 18.8.3 Exemple concret Le petit script reproduit ci-dessous illustre la mise en oeuvre de cette technique, dans un exemple volontairement minimaliste. II s'agit d'une petite application graphique dans laquelle une figure se deplace en cercle a l'interieur d'un canevas. Son « moteur » mainloop() est lance comme d'habitude sur le thread principal. Le constructeur de l'application instancie un canevas contenant le dessin d'un cercle, un bouton et un objet thread. C'est cet objet thread qui assure l'animation du dessin, mais sans faire appel a la methode after() d'un widget. II utilise plutot une simple boucle while tres classique, installee dans sa methode run(). 1 . from Tkinter import * 2 . from math import sin, cos 3. import time, threading 4 . 5. class App (Frame): 6. def init (self): 7. Frame. init (self) 8. self. pack () 9. can =Canvas (self , width =400, height =400, 10. bg ='ivory', bd =3, relief =SUNKEN) 11. can .pack (padx =5, pady =5) 302. Gerard Swinnen : Apprendre a programmer avec Python 12. cercle = can . create_oval (185, 355, 215, 385, fill ='red') 13. tb = Thread_balle (can, cercle) 14. Button(self, text ='Marche', command =tb. start) .pack (side =LEFT) 15. # Button(self, text ='Arret', command =tb. stop) .pack (side =RIGHT) 16. # arreter 1' autre thread si l'on ferme la fenetre : 17. self .bind ( '' , tb.stop) 18. 19. class Thread_balle (threading . Thread) : 20. def init (self, canevas, dessin) : 21 . threading. Thread. init (self) 22. self. can, self. dessin = canevas, dessin 23. self.anim =1 24. 25. def run (self) : 26. a = 0.0 27. while self.anim == 1: 28. a += .01 29. x, y = 200 + 170*sin(a), 200 +170*cos (a) 30. self .can. coords (self .dessin, x-15, y-15, x+15, y+15) 31. time . sleep (0 . 010) 32. 33. def stop (self, evt =0) : 34. self.anim =0 35. 36. App () .mainloop () Commentaires : • Lignes 13 & 14 : Afin de simplifier notre exemple au maximum, nous creons l'objet thread charge de l'animation, directement dans le constructeur de Implication principale. Cet objet thread ne demarrera cependant que lorsque l'utilisateur aura clique sur le bouton « Marche », qui active sa methode start() (rappelons ici que c'est cette methode integree qui lancera elle-meme la methode run() ou nous avons installe notre boucle d'animation). • Ligne 15 : Vous ne pouvez par redemarrer un thread qui s'est termine. De ce fait, vous ne pouvez lancer cette animation qu'une seule fois (tout au mo ins sous la forme presentee ici). Pour vous en convaincre, activez la ligne n° 15 en enlevant le caractere # situe au debut (et qui fait que Python considere qu'il s'agit d'un simple commentaire) : lorsque l'animation est lancee, un clic de souris sur le bouton ainsi mis en place provoque la sortie de la boucle while des lignes 27-3 1 , ce qui termine la methode run(). L'animation s'arrete, mais le thread qui la gerait s'est termine lui aussi. Si vous essayez de le relancer a l'aide du bouton « Marche », vous n'obtenez rien d'autre qu'un message d'erreur. • Lignes 26 a 3 1 : Pour simuler un mouvement circulaire uniforme, il suffit de faire varier continuellement la valeur d'un angle a. Le sinus et le cosinus de cet angle permettent alors de calculer les coordonnees x et y du point de la circonference qui correspond a cet angle 81 . A chaque iteration, Tangle ne varie que d'un centieme de radian seulement (environ 0,6°), et il faudra done 628 iterations pour que le mobile effectue un tour complet. La temporisation choisie pour ces iterations se trouve a la ligne 31 : 10 millisecondes. Vous pouvez accelerer le mouvement en diminuant cette valeur, mais vous ne pourrez guere descendre en dessous de 1 milliseconde (0.001 s), ce qui n'est deja pas si mal. 81 Vous pouvez trouver quelques explications complementaires a ce sujet, a la page 230. Gerard Swinnen : Apprendre a programmer avec Python 303. Chapitre 19 : Annexes 19.1 Installation de Python Si vous souhaitez essayer Python sur votre ordinateur personnel, n'hesitez pas : ['installation est tres facile (et parfaitement reversible). 19.2 Sous Windows Sur le site web officiel de Python : http://www.python.org , vous trouverez dans la section « Download » des logiciels d'installation automatique pour les differentes versions de Python. Vous pouvez en confiance choisir la derniere version « de production ». Par exemple, au 30/9/03, il s'agit de la version 2.3. 1 - Fichier a telecharger : Python-2 .3.1. exe Copiez ce fichier dans un repertoire temporaire de votre machine, et executez-le. Python s'installera par defaut dans un repertoire nomme « Python** » (** indiquant les deux premiers chiffres du n° de version), et des icones de lancement seront mises en place automatiquement. Lorsque Installation est terminee, vous pouvez effacer le contenu du repertoire temporaire. 19.3 Sous Linux Vous avez probablement installe votre systeme Linux a l'aide d'une distribution commerciale telle que SuSE, RedHat ou Mandrake. Installez simplement les paquetages Python qui en font partie, en n'omettant pas Tkinter (parfois installe en meme temps que la Python imaging library). 19.4 Sous MacOS Vous trouverez differentes versions de Python pour MacOS 9 et Mac OS X sur le site web de Jack Jansen : http://homepages.cwi.nl/~jack/macpython Remarque importante concernant les versions recentes de Python Depuis l'apparition de la version 2.3, il est vivement recommande aux francophones que nous sommes d'inclure l'un des pseudo-commentaires suivant au debut de tous nos scripts Python (a la l e ou 2 e ligne) : # -*- coding :Latin-l -*- Ou bien : # -*- coding :Utf-8 -*- Vous trouverez l'explication de cette necessite a la page 40. 19.5 Installation de SciTE (Scintilla Text Editor) SciTE est un excellent logiciel editeur, capable d'effectuer la coloration syntaxique, l'auto- completion et surtout le repliement de code (code folding), c'est a dire le masquage a volonte de differents blocs d'instructions (contenu d'une classe, d'une fonction, d'une boucle, etc.) : cette fonctionnalite se revele extremement pratique lorsque vos scripts commencent a s'allonger ... II integre egalement une fenetre de terminal ainsi qu'un raccourci pour lancement des scripts. Cet editeur est disponible pour Windows et pour Linux. Veuillez consulter le site web : http://www.scintilla.org/SciTE.html 304. Gerard Swinnen : Apprendre a programmer avec Python 19.5.1 Installation sous Linux : L'editeur Scintilla fait dorenavant partie des paquetages fournis d'office avec les distributions recentes de Linux. Sinon, telechargez-le au depart du site web mentionne ci-dessus. Ensuite : • Telecharger l'archive gscite***.tgz puis l'extraire avec tar. • Installer 1' executable SciTE dans /usr/local/bin • Installer tout le reste (fichiers * .properties) dans /usr/share/scite (et non /usr/share/gscite !) 19.5.2 Installation sous Windows : • Telecharger l'archive wscite***.zip puis l'extraire dans \Program files • Installer une icone de lancement pour l'executable SciTe.exe 19.5.3 Pour les deux versions : On peut personnaliser beaucoup de choses (polices, etc.) en editant le fichier des proprietes globales (Menu Options — > Open global options file) Par exemple, pour activer de jolis symboles pour replier/deplier, dans la marge de gauche : fold. symbols =2 # pour de belles icones + et - cerclees fold. on. open =1 # ainsi tout est plie au depart margin . width =0 # pour supprimer la marge inutile Pour forcer le remplacement automatique des tabulations par des groupes de 4 espaces : tabsize = 4 indent. size = 4 use. tabs = 0 19.6 Installation des Python mega-widgets Visitez le site web : http://pmw.sourceforge.net et cliquez sur le lien : Download Pmwl2tar.gz pour telecharger le fichier correspondant. Decomprimez ce fichier archive dans un repertoire temporaire, a l'aide d'un logiciel de decompression tel que tar, Winzip, Info-Zip, unzip .... Recopiez l'integralite du sous-repertoire Pmw qui s'est cree automatiquement, dans le repertoire ou se trouve deja l'essentiel de votre installation de Python. Sous Windows, il s'agira par exemple de c : \Python23 Sous Linux, il s'agira vraisemblablement de /usr/lib/python Gerard Swinnen : Apprendre a programmer avec Python 305. 19.7 Installation de Gadfly (systeme de bases de donnees) Depuis le site http://sourceforge.net/projects/gadfly . telecharger le paquetage gadfly-l.O.O.tar.gz II s'agit d'un fichier archive comprime. Copiez ce fichier dans un repertoire temporaire. 19.8 Sous Windows : Dans un repertoire temporaire quelconque, decomprimez le fichier archive a l'aide d'un logiciel tel que Winzip. Ouvrez une fenetre DOS et entrez dans le sous-repertoire qui s'est cree automatiquement. Lancez la commande : python setup. py install C'esttout. Vous pouvez eventuellement ameliorer les performances, en ajoutant l'operation suivante : Dans le sous-repertoire qui s'est cree, ouvrez le sous-repertoire kjbuckets, puis le sous-repertoire qui correspond a votre version de Python. Recopiez le fichier *.pyd qui s'y trouve dans le repertoire racine de votre installation de Python. Lorsque tout est termine, effacez le contenu de votre repertoire temporaire. 19.8.1 Sous Linux : En tant qu'administrateur (root), choisissez un repertoire temporaire quelconque et decomprimez-y le fichier archive a l'aide de l'utilitaire tar, qui fait certainement partie de votre distribution. Entrez simplement la commande : tar -xvzf gadfly- 1 .0.0. tar . gz Entrez dans le sous-repertoire qui s'est cree automatiquement : cd gadfly- 1 . 0 . 0 Lancez la commande : python setup. py install C'esttout. Si votre systeme Linux comporte un compilateur C, vous pouvez ameliorer les performances de Gadfly en recompilant la bibliotheque kjbuckets. Pour ce faire, entrez encore les deux commandes suivantes : cd kjbuckets python setup. py install Lorsque tout est termine, effacez tout le contenu du repertoire temporaire. 306. Gerard Swinnen : Apprendre a programmer avec Python 19.9 Solutions a ux exercices Pour quelques exercices, nous ne fournissons pas de solution. Efforcez-vous de les trouver sans aide, meme si cela vous semble difficile. C'est en effet en vous acharnant sur de tels problemes que vous apprendrez le mieux. Exercice 4.2 : »> c = 0 »> while c < 20: c = c +1 print c, "x 7 =", c*7 ou encore : »> c = 1 »> while c <= 20: print c, "x 7 =", c*7 c = c +1 Exercice 4.3 : »> s = l »> while s <= 16384: print s, "euro(s) =", s *1.65, "dollar (s) " s = s *2 Exercice 4.4 : »> a, c = 1, 1 »> while c < 13: print a, ... a, c = a *3, c+1 Exercice 4.6 : # Le nombre de secondes est fourni au depart : # (un grand nombre s ' impose ! ) nsd = 12345678912 # Nombre de secondes dans une journee : nspj = 3600 * 24 # Nombre de secondes dans un an (soit 365 jours - # on ne tiendra pas compte des annees bissextiles) : nspa = nspj * 365 # Nombre de secondes dans un mois (en admettant # pour chague mois une duree identique de 30 jours) : nspm = nspj * 30 # Nombre d' annees contenues dans la duree fournie : na = nsd / nspa # division nsr = nsd % nspa # n. de sec. restantes # Nombre de mois restants : nmo = nsr / nspm # division nsr = nsr % nspm # n. de sec. restantes # Nombre de jours restants : nj = nsr / nspj # division nsr = nsr % nspj # n. de sec. restantes # Nombre d'heures restantes : nh = nsr / 3600 # division nsr = nsr % 3600 # n. de sec. restantes # Nombre de minutes restantes : Gerard Swinnen : Apprendre a programmer avec Python 307. nmi = nsr /60 # division nsr = nsr % 60 # n. de sec. restantes print "Nombre de secondes a convertir : " , nsd print "Cette duree correspond a", na, "annees de 365 jours, plus" print nmo, "mois de 30 jours,", print nj, "jours,", print nh, "heures,", print nmi, "minutes et", print nsr, "secondes." Exercice 4.7 : # affichage des 20 premiers termes de la table par 7, # avec signalement des multiples de 3 : i = 1 # compteur : prendra successivement les valeurs de 1 a 20 while i < 21: # calcul du terme a afficher : t = i * 7 # affichage sans saut a la ligne (utilisation de la virgule) : print t, # ce terme est-il un multiple de 3 ? (utilisation de l'operateur modulo) : if t % 3 == 0: print "*", # affichage d'une asterisque dans ce cas i = i + 1 # incrementation du compteur dans tous les cas Exercice 5.1 : # Conversion degres -> radians # Rappel : un angle de 1 radian est un angle qui correspond a une portion # de circonference de longueur egale a celle du rayon. # Puisque la circonference vaut 2 pi R, un angle de 1 radian correspond # a 360° / 2 pi , ou encore a 180° / pi # Angle fourni au depart en degres, minutes, secondes : deg, min, sec = 32, 13, 49 # Conversion des secondes en une fraction de minute : # (le point decimal force la conversion du resultat en un nombre reel) fm = sec/ 60 . # Conversion des minutes en une fraction de degre : fd = (min + fm) /60 # Valeur de 1' angle en degres "decimalises" : ang = deg + fd # Valeur de pi : pi = 3.14159265359 # Valeur d'un radian en degres : rad = 180 / pi # Conversion de 1 ' angle en radians : arad = ang / rad # Affichage : print deg, min, " "' , sec, "' =', arad, "radian (s) " 308. Gerard Swinnen : Apprendre a programmer avec Python Exercice 5.3 : # Conversion °Fahrenheit <-> °Celsius # A) Temperature fournie en °C : tempC =25 # Conversion en °Fahrenheit : tempF = tempC * 1.8 + 32 # Affichage : print tempC, "°C =", tempF, "°F" # B) Temperature fournie en °F : tempF = 25 # Conversion en °Celsius : tempC = (tempF - 32) / 1.8 # Affichage : print tempF, "°F =", tempC, "°C" Exercice 5.5 »> a, b = 1, 1 »> while b<65: print b, a a,b = a*2, b+1 # variante b = 1. Exercice 5.6 : # Recherche d'un caractere particulier dans une chaine # Chaine fournie au depart : ch = "Monty python flying circus" # Caractere a rechercher : cr = "e" # Recherche proprement dite : lc = len(ch) # nombre de caracteres a tester i = 0 # indice du caractere en cours d'examen t = 0 # "drapeau" a lever si le caractere recherche est present while i < lc: if ch[i] == cr: t = 1 i = i + 1 # Affichage : print "Le caractere", cr, if t == 1: print "est present", else : print "est inrouvable", print "dans la chaine", ch Exercice 5.8 : # Insertion d'un caractere d'espacement dans une chaine # Chaine fournie au depart : ch = "Gaston" # Caractere a inserer : cr = "*" # Le nombre de caracteres a inserer est inferieur d'une unite au Gerard Swinnen : Apprendre a programmer avec Python 309. # nombre de caracteres de la chaine . On traitera done celle-ci a # partir de son second caractere (en omettant le premier) . lc = len(ch) # nombre de caracteres total 1=1 # indice du premier caractere a examiner (le second, en fait) nch = ch[0] # nouvelle chaine a construire (contient deja le premier car.) while i < lc: nch = nch + cr + ch [ i ] i = i + 1 # Affichage : print nch Exercice 5.9 : # Inversion d'une chaine de caracteres # Chaine fournie au depart : ch = "zorglub" lc = len(ch) # nombre de caracteres total i = lc - 1 # le traitement commencera a partir du dernier caractere nch = "" # nouvelle chaine a construire (vide au depart) while i >= 0 : nch = nch + ch [ i ] i = i - 1 # Affichage : print nch Exercice 5.11 : # Combinaison de deux listes en une seule # Listes fournies au depart : tl = [31,28,31,30,31,30,31,31,30,31,30,31] t2 = [ ' Janvier ' , ' Fevrier ' , ' Mars ' , ' Avril ' , ' Mai ' , ' Juin ' , ' Juillet ' , ' Aout ' , ' Septembre ' , ' Octobre ' , ' Novembre ' , ' Decembre ' ] # Nouvelle liste a construire (vide au depart) : t3 = [] # Boucle de traitement : i = 0 while i < len(tl) : t3. append (t2 [i] ) t3. append (tl [i] ) i = i + 1 # Affichage : print t3 Exercice 5.12 : # Affichage des elements d'une liste # Liste fournie au depart : t2 = [ ' Janvier ' , ' Fevrier ' , ' Mars ' , ' Avril ' , ' Mai ' , ' Juin ' , ' Juillet ' , ' Aout ' , ' Septembre ' , ' Octobre ' , ' Novembre ' , ' Decembre ' ] # Affichage : i = 0 while i < len(t2): print t2 [i] , i = i + 1 310. Gerard Swinnen : Apprendre a programmer avec Python Exercice 5.13 : # Recherche du plus grand element d'une liste # Liste fournie au depart : tt = [32, 5, 12, 8, 3, 75, 2, 15] # Au fur et a mesure du traitement de la liste, on memo riser a dans # la variable ci-dessous la valeur du plus grand element deja trouve : max = 0 # Examen de tous les elements : i = 0 while i < len(tt) : if tt [i] > max : max = tt[i] # memorisation d'un nouveau maximum i = i + 1 # Affichage : print "Le plus grand element de cette liste a la valeur", max Exercice 5.14 : # Separation des nombres pairs et impairs # Liste fournie au depart : tt = [32, 5, 12, 8, 3, 75, 2, 15] pairs = [] impairs = [] # Examen de tous les elements : i = 0 while i < len(tt) : if tt[i] % 2 == 0: pairs . append (tt [i] ) else : impairs . append ( tt [ i ] ) i = i + 1 # Affichage : print "Nombres pairs : " , pairs print "Nombres impairs : " , impairs Exercice 6.1 : # Conversion de miles/heure en km/h et m/s print "Veuillez entrer le nombre de miles parcourus en une heure : ", ch = raw_input ( ) # en general preferable a input ( ) mph = float (ch) # conversion de la chaine entree en nombre reel mps = mph * 1609 / 3600 # conversion en metres par seconde kmph = mph * 1.609 # conversion en km/h # affichage : print mph, "miles/heure =", kmph, "km/h, ou encore", mps, "m/s" Exercice 6.2 : # Perimetre et Aire d'un triangle quelconque from math import sqrt print "Veuillez entrer le cote a : " a = float (raw_input () ) print "Veuillez entrer le cote b : " b = float (raw_input () ) Gerard Swinnen : Apprendre a programmer avec Python 311. print "Veuillez entrer le cote c : " c = float (raw_input () ) d = (a + b + c)/2 # demi s = sqrt (d* (d-a) * (d-b) * (d-c) ) # aire print "Longueur des cotes =", a, b, c print "Perimetre =", d*2, "Aire =", s Exercice 6.4 : # Entree d' elements dans une liste tt = [] # Liste a completer (vide au depart) ch = "start" # valeur quelconque (mais non nulle) while ch ! = " " : print "Veuillez entrer une valeur : " ch = raw_input ( ) if ch != "": tt . append (float (ch) ) # variante : tt . append (ch) # affichage de la liste : print tt -perimetre (suivant formule) Exercice 6.8 : # Traitement de nombres entiers compris entre deux limites print "Veuillez entrer la limite inf erieure : " , a = input () print "Veuillez entrer la limite superieure : " , b = input () s = 0 # some recherchee (nulle au depart) # Parcours de la serie des nombres compris entre a et b : n = a # nombre en cours de traitement while n <= b: if n % 3 ==0 and n % 5 ==0: # variante : 'or' au lieu de 'and' s = s + n n = n + 1 print "La somme recherchee vaut", s Exercice 6.9 : # Annees bissextiles print "Veuillez entrer 1 ' annee a tester : " , a = input ( ) if a % 4 != 0: # a n'est pas divisible par 4 -> annee non bissextile bs = 0 else : if a % 400 ==0: # a divisible par 400 -> annee bissextile bs = 1 elif a % 100 ==0: # a divisible par 100 -> annee non bissextile bs = 0 else : 312. Gerard Swinnen : Apprendre a programmer avec Python # autres cas ou a est divisible par 4 -> annee bissextile bs = 1 if bs ==1: ch = "est" else : ch = "n'est pas" print "L' annee", a, ch, "bissextile." Variante (proposee par Alex Misbah ) : a=input ( ' entree une annee : ' ) if (a%4==0) and ((a%100!=0) or (a%400==0) ) : print a, "est une annee bissextile" else : print a, "n'est pas une annee bissextile" Exercice 6.11 : Calculs de triangles from sys import exit # module contenant des fonctions systeme print """ Veuillez entrer les longueurs des 3 cotes (en separant ces valeurs a l'aide de virgules) a, b, c = input () # II n'est possible de construire un triangle que si chaque cote # a une longueur inferieure a la somme des deux autres : if a < (b+c) and b < (a+c) and c < (a+b) : print "Ces trois longueurs determinent bien un triangle." else : print "II est impossible de construire un tel triangle !" exit() # ainsi 1 ' on n ' ira pas plus loin. f = 0 if a == b and b == c : print "Ce triangle est equilateral." f = 1 elif a == b or b == c or c == a : print "Ce triangle est isocele . " f = 1 if a*a + b*b == c*c or b*b + c*c == a*a or c*c + a*a == b*b : print "Ce triangle est rectangle." f = 1 if f == 0 : print "Ce triangle est quelconque." Gerard Swinnen : Apprendre a programmer avec Python 313. Exercice 6.15 : # Notes de travaux scolaires notes = [] # liste a construire n = 2 # valeur positive quelconque pour initier la boucle while n >= 0 : print "Entrez la note suivante, s.v.p. : ", n = f loat (raw_input ( ) ) # conversion de 1' entree en un nombre reel if n < 0 : print "OK. Termine . " else : notes . append (n) # ajout d'une note a la liste # Calculs divers sur les notes deja entrees : # valeurs minimale et maximale + total de toutes les notes . min = 500 # valeur superieure a toute note max, tot, i = 0, 0, 0 nn = len (notes) # nombre de notes deja entrees while i < nn : if notes [i] > max: max = notes [i] if notes [i] < min: min = notes [i] tot = tot + notes [i] moy = tot/nn i = i + 1 print nn, "notes entrees. Max =", max, "Min =", min, "Moy =", moy Exercice 7.3 : from math import pi def surfCercle (r) : "Surface d'un cercle de rayon r" return pi * r**2 # test : print surfCercle (2 . 5) Exercice 7.4 : def volBoite(xl, x2, x3) : "Volume d'une boite parallelipipedique" return xl * x2 * x3 # test : print volBoite (5 . 2, 7.7, 3.3) 314. Gerard Swinnen : Apprendre a programmer avec Python Exercice 7.5 : def maximum (nl , n2, n3) : "Renvoie le plus grand de trois nombres" if nl >= n2 and nl >= n3 : return nl elif n2 >= nl and n2 >= n3: return n2 else : return n3 # test : print maximum (4. 5, 5.7, 3.9) Exercice 7.9 : def compteCar (ca, ch) : "Renvoie le nombre de caracteres ca trouves dans la chaine ch" i, tot =0, 0 while i < len(ch) : if ch[i] == ca: tot = tot + 1 i = i + 1 return tot # test : print compteCar ("e" , "Cette chaine est un exemple") Exercice 7.10 : def indexMax(tt) : "renvoie 1 ' indice du plus grand element de la liste tt" i, max =0, 0 while i < len(tt) : if tt[i] > max : max , imax = tt [ i ] , i i = i + 1 return imax # test : serie = [5, 8, 2, 1, 9, 3, 6, 4] print indexMax (serie) Exercice 7.11 : def nomMois (n) : "renvoie le nom du n-ieme mois de l'annee" mois = ['Janvier,', 'Fevrier', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Aout', 'Septembre', 'Octobre', 'Novembre', 'Decembre'] return mois [n -1] # les indices sont numerotes a partir de zero # test : print nomMois (4) Gerard Swinnen : Apprendre a programmer avec Python 315. Exercice 7.14 : def volBoite(xl =10, x2 =10, x3 =10): "Volume d'une boite parallelipipedique" return xl * x2 * x3 # test : print volBoite() print volBoite (5 . 2) print volBoite (5 . 2, 3) Exercice 7.15 : def volBoite (xl =-1, x2 "Volume d'une boite if xl == -1 : return xl elif x2 == -1 : return xl**3 elif x3 == -1 : return xl*xl*x2 else : return xl*x2*x3 # test : print volBoite () print volBoite (5 . 2) print volBoite (5 . 2, 3) print volBoite (5 . 2, 3, 7.4) Exercice 7.16 : def changeCar (ch, cal, ca2, debut =0, fin =-1): "Remplace tous les caracteres cal par des ca2 dans la chaine ch" if fin == -1: fin = len(ch) nch, i = "", 0 # nch : nouvelle chaine a construire while i < len(ch) : if i >= debut and i <= fin and ch[i] == cal: nch = nch + ca2 else : nch = nch + ch[i] i = i + 1 return nch # test : print changeCar ( "Ceci est une print changeCar ("Ceci est une print changeCar ( "Ceci est une =-1, x3 =-1) : parallelipipedique" # aucun argument n ' a ete f ourni # un seul argument -> boite cubique # deux arguments -> boite prismatique toute petite phrase", " ", "*") toute petite phrase", " ", "*", 8, 12) toute petite phrase", " ", "*", 12) 316. Gerard Swinnen : Apprendre a programmer avec Python Exercice 7.17 : def eleMax(lst, debut =0, fin =-1): "renvoie le plus grand element de la liste 1st" if fin == -1: fin = len(lst) max, i = 0, 0 while i < len(lst) : if i >= debut and i <= fin and lst[i] > max: max = lst[i] i = i + 1 return max # test : serie = [9, 3, 6, 1, 7, 5, 4, 8, 2] print eleMax (serie) print eleMax (serie, 2) print eleMax (serie, 2, 5) Exercice 8.7 : from Tkinter import * # Coordonnees X,Y des 5 anneaux : coord = [[20,30], [120,30], [220, 30], [70,80], [170,80]] # Couleurs des 5 anneaux : coul = ["red", "yellow", "blue", "green", "black"] base = Tk() can = Canvas (base, width =335, height =200, bg ="white") can. pack () bou = Button (base, text =" Quitter", command =base.quit) bou. pack (side = RIGHT) # Dessin des 5 anneaux : i = 0 while i < 5 : xl, yl = coord [i] [0], coord [i] [1] can . create_oval (xl, yl, xl+100, yl +100, width =2, outline =coul[i]) i = i +1 base . mainloop ( ) Variante : from Tkinter import * # Dessin des 5 anneaux : def dessineCercle (i) : xl, yl = coord [i] [0], coord [i] [1] can . create_oval (xl, yl, xl+100, yl +100, width =2, outline =coul[i]) def al () : dessineCercle (0) def a2 ( ) : dessineCercle (1) def a3 () : dessineCercle (2) def a4 () : dessineCercle (3) Gerard Swinnen : Apprendre a programmer avec Python 317. def a5 () : dessineCercle (4) # Coordonnees X,Y des 5 anneaux : coord = [[20,30], [120,30], [220, 30], [70,80], [170,80]] # Couleurs des 5 anneaux : coul = ["red", "yellow", "blue", "green", "black"] base = Tk() can = Canvas (base, width =335, height =200, bg ="white") can . pack ( ) bou = Button (base, text ="Quitter", command =base.quit) bou .pack (side = RIGHT) # Installation des 5 boutons : Button (base, text='l', command = al) Button (base, text='2', command = a2) Button (base, text='3', command = a3) Button (base, text='4', command = a4) Button (base, text='5', command = a5) base . mainloop ( ) .pack (side =LEFT) .pack (side =LEFT) .pack (side =LEFT) .pack (side =LEFT) .pack (side =LEFT) 318. Gerard Swinnen : Apprendre a programmer avec Python Exercices 8.9 et 8.10 : # Dessin d'un damier, avec placement de pions au hasard from Tkinter import * from random import randrange def damier ( ) : "dessiner dix lignes de carres y = 0 while y < 10: if y % 2 == 0: x = 0 else : x = 1 ligne_de_carres (x*c, y*c) y += 1 def ligne_de_carres (x, y) : "dessiner une ligne de carres, i = 0 while i < 10: can . create_rectangle (x, y, i += 1 x += c*2 # generateur de nombres aleatoires avec decalage alterne" # une fois sur deux, on # commencera la ligne de # carres avec un decalage # de la taille d'un carre en part ant de x, y" x+c, y+c, fill='navy') # espacer les carres def cercle (x, y, r, coul) : "dessiner un cercle de centre x,y et de rayon r" can . create_oval (x-r, y-r, x+r, y+r, fill=coul) def a jouter_pion () : "dessiner un pion au hasard sur le damier" # tirer au hasard les coordonnees du pion : x = c/2 + randrange (10) * c y = c/2 + randrange (10) * c cercle (x, y, c/3, 'red') ##### Programme principal : ############ # Tachez de bien "parametrer" vos programmes, comme nous 1 ' avons # fait dans ce script . Celui-ci peut en ef fet tracer des damiers # de n ' importe quelle taille en changeant seulement la valeur # d'une seule variable, a savoir la dimension des carres : c = 30 # taille des carres fen = Tk() can = Canvas (fen, width =c*10, height =c*10, bg =' ivory') can. pack (side =TOP, padx =5, pady =5) bl = Button (fen, text =' damier', command =damier) bl. pack (side =LEFT, padx =3, pady =3) b2 = Button (fen, text = 'pions', command =a jouter_pion) b2. pack (side =RIGHT, padx =3, pady =3) fen . mainloop ( ) # Gerard Swinnen : Apprendre a programmer avec Python 319. Exercice 8.12 : # Simulation du phenomene de gravitation universelle from Tkinter import * from math import sqrt def distance (xl, yl, x2, y2) : "distance separant les points xl,yl et x2,y2" d = sqrt ( (x2-xl) **2 + (y2-yl)**2) # theoreme de Pythagore return d def forceG(ml, m2, di) : "force de gravitation s ' exergant entre ml et m2 pour une distance di" return ml*m2*6 . 67e-ll/di**2 # loi de Newton def avance (n, gd, hb) : "deplacement de l'astre n, de gauche a droite ou de haut en bas" global x, y, step # nouvelles coordonnees : x[n], y[n] = x[n] +gd, y[n] +hb # deplacement du dessin dans le canevas : can . coords (astre [n] , x[n]-10, y[n]-10, x[n]+10, y[n]+10) # calcul de la nouvelle interdistance : di = distance (x[0] , y[0], x[l], y[l]) # conversion de la distance "ecran" en distance "astronomique" : diA = di*le9 # (1 pixel => 1 million de km) # calcul de la force de gravitation correspondante : f = f orceG (ml , m2 , diA) # affichage des nouvelles valeurs de distance et force : valDis . configure (text="Distance = " +str(diA) +" m") valFor. configure (text=" Force = " +str(f) +" N") # adaptation du "pas" de deplacement en fonction de la distance : step = di/10 def gauchel () : avance (0, -step, 0) def droitel () : avance (0, step, 0) def hautl () : avance (0, 0, -step) def basl () : avance (0, 0, step) def gauche2 () : avance (1, -step, 0) def droite2 () : avance (1, step, 0) def haut2 () : avance (1, 0, -step) def bas2 () : avance (1, 0, step) # Masses des deux astres : ml = 6e24 # (valeur de la masse de la terre, en kg) m2 = 6e24 # 320. Gerard Swinnen : Apprendre a programmer avec Python astre = [0]*2 x =[50. , 350. ] y =[100., 100.] step =10 # liste servant a memoriser les references des dessins # liste des coord. X de chaque astre (a l'ecran) # liste des coord. Y de chaque astre # "pas" de deplacement initial +str(ml) +" kg") +str(m2) +" kg") # Construction de la fenetre : fen = Tk() fen.title(' Gravitation universelle suivant Newton') # Libelles : valMl = Label (fen, text="Ml = valMl . grid (row =1, column =0) valM2 = Label (fen, text="M2 = valM2 . grid (row =1, column =1) valDis = Label (fen, text="Distance" ) valDis . grid (row =3, column =0) valFor = Label (fen, text="Force" ) valFor . grid (row =3, column =1) # Canevas avec le dessin des 2 astres: can = Canvas (fen, bg ="light yellow", width =400, height =200) can. grid (row =2, column =0, columnspan =2) astre[0] = can. create_oval (x [0] -10, y[0]-10, x[0]+10, y[0]+10, fill ="red", width =1) astre[l] = can. create_oval (x [1] -10, y[l]-10, x[l]+10, y[l]+10, fill ="blue", width =1) # 2 groupes de 4 boutons, chacun installe dans un cadre (frame) : fral = Frame (fen) fral . grid (row =4, column =0, sticky =W, padx =10) Button(fral, text="<-", fg =' red ', command =gauchel) .pack (side =LEFT) Button(fral, text="->", fg ='red', command =droitel) . pack (side =LEFT) Button(fral, text=" A ", fg ='red', command =hautl) .pack (side =LEFT) Button(fral, text="v", fg ='red', command =basl) .pack (side =LEFT) fra2 = Frame (fen) fra2 . grid (row =4, column =1, sticky =E, padx =10) Button(fra2, text="<-", fg ='blue', command =gauche2) .pack (side =LEFT) Button(fra2, text="->", fg ='blue', command =droite2) .pack (side =LEFT) Button(fra2, text=" A ", fg ='blue', command =haut2) .pack (side =LEFT) Button(fra2, text="v", fg ='blue', command =bas2) .pack (side =LEFT) fen . mainloop ( ) Gravitation universelle suivant Newton M1 = Ge+024 kg M2 = 6e+024 kg Distance = 56232995801 .7 m < l I I 'I Force = 7.5935681 0742e+01 7 N <- Gerard Swinnen : Apprendre a programmer avec Python 321. Exercice 8.16 : # Conversions de temperatures Fahrenheit <=> Celsius from Tkinter import * def convFar (event) : "valeur de cette temperature, exprimee en degres Fahrenheit" tF = eval ( champTC . get ( ) ) varTF.set (str (tF*l . 8 +32)) def convCel (event) : "valeur de cette temperature, exprimee en degres Celsius" tC = eval (champTF . get ( ) ) varTC . set (str ( (tC-32) /l . 8) ) fen = Tk() fen . title ( ' Fahrenheit/Celsius ' ) Label(fen, text='Temp. Celsius :' # "variable Tkinter" associee au > # assure 1 ' interface entre TCL et varTC =StringVar() champTC = Entry (fen, textvariable champTC . bind ( " " , convFar ) champTC . grid (row =0, column =1) # Initialisation du contenu de la varTC. set ("100.0") .grid (row =0, column =0) ihamp d' entree. Cet "ob jet -variable" Python (voir notes, page 165) : =varTC) variable Tkinter : Label(fen, text= ' Temp . Fahrenheit :').grid(row =1, column =0) varTF =StringVar() champTF = Entry (fen, textvariable =varTF) champTF .bind("" , convCel) champTF . grid (row =1, column =1) varTF.set ("212.0") fen . mainloop ( ) Fahrenheit/Celsius Temp. Celsius : 25.0 Temp. Fahrenheit : 1 77 0 njx] Exercice 8.18 a 8.20 : # Cercles et courbes de Lissajous from Tkinter import * from math import sin, cos def move ( ) : global ang, x, y # on memorise les coord, precedentes avant de calculer les nouvelles : xp, yp = x, y # rotation d ' un angle de 0.1 radian : ang = ang + . 1 # sinus et cosinus de cet angle => coord, d'un point du cercle trigone x, y = sin (ang), cos (ang) # Variante determinant une courbe de Lissajous avec fl/f2 = 2/3 : # x, y = sin(2*ang), cos(3*ang) # mise a l'echelle (120 = rayon du cercle, (150,150) = centre du canevas) 322. Gerard Swinnen : Apprendre a programmer avec Python x, y = x*120 + 150, y*120 + 150 can . coords (balle, x-10, y-10, x+10, y+10) can . create_line (xp, yp, x, y, fill ="blue") ang, x, y = 0., 150., 270. fen = Tk() f en. title ( 'Courbes de Lissajous') can = Canvas (fen, width =300, height=300, bg="white") can . pack ( ) balle = can. create_oval (x-10, y-10, x+10, y+10, fill='red') Button (fen, text='Go', command =move) . pack ( ) fen . mainloop ( ) Courbes de Lissajous Go Exercice 8.27 : # Chutes et rebonds from Tkinter import * def move ( ) : global x, y, v, dx, dv, flag xp, yp = x, y # memorisation des coord, precedentes # deplacement horizontal : if x > 385 or x < 15 : # rebond sur les parois laterales : dx = -dx # on inverse le deplacement x = x + dx # variation de la vitesse verticale (toujours vers le bas) : v = v + dv Gerard Swinnen : Apprendre a programmer avec Python 323. # deplacement vertical (proportionnel a la vitesse) y = y + v if y > 240: # niveau du sol a 240 pixels : y = 240 # defense d'aller + loin ! v = -v # rebond : la vitesse s ' inverse # on repositionne la balle : can . coords (balle, x-10, y-10, x+10, y+10) # on trace un bout de trajectoire : can . create_line (xp, yp, x, y, fill =' light grey') # ... et on remet 9a jusqu'a plus soif : if flag > 0 : fen . after (50, move) def start () : global flag flag = flag +1 if flag == 1: move ( ) def stop ( ) : global flag flag =0 # initialisation des coordonnees, des vitesses et du temoin d' animation : x, y, v, dx, dv, flag = 15, 15, 0, 6, 5, 0 fen = Tk() fen.title(' Chutes et rebonds ' ) can = Canvas (fen, width =400, height=250, bg="white") can . pack ( ) balle = can. create_oval (x-10, y-10, x+10, y+10, fill='red') Button(fen, text= ' Start ' , command =start) .pack (side =LEFT, padx =10) Button(fen, text='Stop', command =stop) .pack (side =LEFT) Button(fen, text= ' Quitter ' , command =f en . quit) .pack (side =RIGHT, padx =10) fen . mainloop ( ) 324. Gerard Swinnen : Apprendre a programmer avec Python Exercice 9.1 (editeur simple, pour lire et ecrire dans un fichier 'texte') : def sansDC (ch) : "cette fonction renvoie la chaine ch amputee de son dernier caractere" nouv = i, j = 0, len(ch) -1 while i < j : nouv = nouv + ch [ i ] i = i + 1 return nouv def ecrireDansFichier ( ) : of = open(nomF, 'a') while 1 : ligne = raw_input ( "entrez une ligne de texte (ou ) : ") if ligne == ' ' : break else : of .write (ligne + '\n') of . close () def lireDansFichier () : of = open(nomF, 'r') while 1 : ligne = of . readline () if ligne == " " : break # afficher en omettant le dernier caractere (= fin de ligne) : print sansDC (ligne) of . close () nomF = raw_input ( ' Nom du fichier a traiter : ') choix = raw_input (' Entrez "e" pour ecrire, "c" pour consulter les donnees : ') if choix == ' e ' : ecrireDansFichier ( ) else : lireDansFichier ( ) Exercice 9.3 (generation des tables de multiplication de 2 a 30) : def tableMulti (n) : # Fonction generant la table de multiplication par n (20 termes) # La table sera renvoyee sous forme d'une chaine de caracteres : i, ch = 0, "" while i < 20: i = i + 1 ch = ch + str(i * n) + " " return ch NomF = raw_input ( "Nom du fichier a creer : " ) fichier = open (NomF, 'w') # Generation des tables de 2 a 30 : table = 2 while table < 31 : fichier. write (tableMulti (table) + '\n') table = table + 1 fichier . close ( ) Gerard Swinnen : Apprendre a programmer avec Python 325. Exercice 9.4 : # Triplement des espaces dans un f ichier texte . # Ce script montre egalement comment modifier le contenu d'un f ichier # en le transferant d'abord tout entier dans une liste, puis en # reenregistrant celle-ci apres modifications def triplerEspaces (ch) : "fonction qui triple les espaces entre mots dans la chaine ch" i, nouv =0, "" while i < len(ch) : if ch[i] == " " : nouv = nouv + " " else : nouv = nouv + ch [ i ] i = i +1 return nouv NomF = raw_input ( "Norn du f ichier : ") f ichier = open (NomF, ' r+ ' ) # ' r+ ' = mode read/write lignes = f ichier . readlines () # lire toutes les lignes n=0 while n < len (lignes) : lignes [n] = triplerEspaces (lignes [n] ) n =n+l f ichier . seek (0) # retour au debut du fichier f ichier . writelines (lignes) # reenregistrement fichier . close ( ) Exercice 9.5 : # Mise en forme de donnees numeriques . # Le fichier traite est un fichier texte dont chaque ligne contient un nombre # reel (sans exposants et encode sous la forme d'une chaine de caracteres) def valArrondie (ch) : "representation arrondie du nombre presente dans la chaine ch" f = float (ch) # conversion de la chaine en un nombre reel e = int(f + .5) # conversion en entier (On ajoute d'abord # 0.5 au reel pour 1 ' arrondir correctement) return str(e) # reconversion en chaine de caracteres fiSource = raw_input ( "Nom du fichier a traiter : ") fiDest = raw_input ( "Nom du fichier destinataire : ") fs = open (fiSource, 'r') fd = open (fiDest, 'w') while 1 : ligne = f s . readline () # lecture d'une ligne du fichier if ligne == "" or ligne == "\n": break ligne = valArrondie (ligne) fd. write (ligne +"\n") fd. close () fs . close () 326. Gerard Swinnen : Apprendre a programmer avec Python Exercice 9.6 : # Comparaison de deux fichiers, caractere par caractere : fichl = raw_input ( "Nom du premier fichier : ") fich2 = raw_input ( "Nom du second fichier : ") fil = open (fichl, 'r') fi2 = open(fich2, 'r' ) c, f = 0, 0 while 1 : c = c + 1 carl = fil. read (1) car2 = fi2.read(l) if carl =="" or car2 break if carl != car2 : f = 1 break fil . close () fi2 .close () print "Ces 2 fichiers", if f ==1: print "different a partir du caractere n°", c else : print "sont identiques . " Exercice 9.7 : # Combinaison de deux fichiers texte pour en faire un nouveau fichA = raw_input ( "Nom du premier fichier : ") fichB = raw_input ( "Nom du second fichier : ") fichC = raw_input ( "Nom du fichier destinataire : ") fiA = open (fichA, 'r') fiB = open (fichB, 'r') fiC = open(fichC, 'w') while 1 : ligneA = fiA. readline () ligneB = fiB. readline () if ligneA =="" and ligneB =="": break # On est arrive a la fin des 2 fichiers if ligneA != "" : fiC .write (ligneA) if ligneB != "" : fiC .write (ligneB) fiA. close () fiB. close () fiC. close () # compteur de caracteres et "drapeau" # lecture d'un caractere dans chacun # des deux fichiers # difference trouvee Gerard Swinnen : Apprendre a programmer avec Python 327. Exercice 9.8 : # Enregistrer les coordonnees des membres d'un club def encodage ( ) : "renvoie la liste des valeurs entrees, ou une liste vide" print "*** Veuillez entrer les donnees (ou pour terminer) :" while 1 : nom = raw_input ( "Norn : ") if nom == "" : return [ ] prenom = raw_input ( "Prenom : ") rueNum = raw_input ( "Adresse (N° et rue) : ") cPost = raw_input ("Code postal : ") local = raw_input ( "Localite : ") tel = raw_input ( "N° de telephone : ") print nom, prenom, rueNum, cPost, local, tel ver = raw_input ( "Entrez si c'est correct, sinon ") if ver == "" : break return [nom, prenom, rueNum, cPost, local, tel] def enregistrer (liste) : "enregistre les donnees de la liste en les separant par des <#>" i = 0 while i < len (liste) : of .write (liste [i] + "#") i = i + 1 of .write ("\n") # caractere de fin de ligne nomF = raw_input ( ' Nom du fichier destinataire : ') of = open (nomF, 'a') while 1 : tt = encodage ( ) if tt == [] : break enregistrer (tt) of .close () 328. Gerard Swinnen : Apprendre a programmer avec Python Exercice 9.9 : # Ajouter des informations dans le fichier du club def traduire(ch) : " convert ir une ligne du dn = tt = [] i = 0 while i < len(ch) : if ch[i] == "#": tt . append (dn) dn ="" else : dn = dn + ch [ i ] i = i + 1 return tt fichier source en liste de donnees" # chaine temporaire pour extraire les donnees # la liste a produire # on ajoute la donnee a la liste, et # on reinitialise la chaine temporaire def encodage (tt) : "renvoyer la liste tt, completee avec la date de naissance et le sexe" print "*** Veuillez entrer les donnees (ou pour terminer) : " # Affichage des donnees deja presentes dans la liste : i = 0 while i < len(tt) : print tt [i] , i = i +1 print while 1 : daNai = raw_input ( "Date de naissance : ") sexe = raw_input ( "Sexe (m ou f) : ") print daNai , sexe ver = raw_input ( "Entrez si c'est correct, sinon ") if ver == " " : break tt . append (daNai) tt . append ( sexe ) return tt def enregistrer (tt) : "enregistrer les donnees de la liste tt en les separant par des <#>" i = 0 while i < len(tt) : fd. write (tt [i] + "#") i = i + 1 fd. write ("\n" ) # caractere de fin de ligne f Source = raw_input ( ' Norn du fichier source : ') fDest = raw_input ( ' Nom du fichier destinataire : ') fs = open (f Source, 'r') f d = open ( fDest , ' w ' ) while 1 : ligne = f s . readline () # lire une ligne du fichier source if ligne =="" or ligne =="\n": break liste = traduire (ligne) # la convertir en une liste liste = encodage (liste) # y ajouter les donnees supplementaires enregistrer (liste) # sauvegarder dans fichier dest . fd. close () f s . close () Gerard Swinnen : Apprendre a programmer avec Python 329. Exercice 9.10 : # Recherche de lignes particulieres dans un fichier texte def chercheCP (ch) : "recherche dans ch i, f, ns = 0, 0, 0 cc = "" while i < len(ch) : if ch[i] =="#": ns = ns +1 if ns ==3 : f = 1 elif ns ==4 : break elif f ==1: cc = cc + ch[i] i = i +1 return cc la portion # de chaine contenant le code postal" ns est un compteur de codes # chaine a construire le CP se trouve apres le 3e code # variable "drapeau" (flag) inutile de lire apres le 4e code # le caractere lu fait partie CP recherche -> on memorise du nomF = raw_input ( "Norn du fichier a traiter : ") codeP = raw_input ( "Code postal a rechercher : ") fi = open (nomF, 'r') while 1 : ligne = f i . readline ( ) if ligne =="": break if chercheCP (ligne) == codeP : print ligne fi. close () Exercice 10.2 (decoupage d'une chaine en fragments) : def decoupe (ch, n) : "decoupage de la chaine ch en une liste de fragments de n caracteres" d, f = 0, n tt = [] while d < len(ch) : if f > len(ch) : f = len(ch) fr = ch[d:f] tt . append (fr) d, f = f, f +n return tt # indices de debut et de fin de fragment # liste a construire # on ne peut pas decouper au-dela de la fin # decoupage d'un fragment # ajout du fragment a la liste # indices suivants def inverse(tt): "rassemble les elements de la liste tt dans l'ordre inverse" ch = "" # chaine a construire i = len(tt) # on commence par la fin de la liste while i > 0 : i = i - 1 # le dernier element possede 1 ' indice n -1 ch = ch + tt[i] return ch # Test : ch = " abcde f ghi j klmnopqr st uvwxy z 123456789" liste = decoupe (ch, 5) print liste print inverse (liste) 330. Gerard Swinnen : Apprendre a programmer avec Python Exercice 10.3 : # Rechercher l'indice d'un caractere dans une chaine def trouve (ch, car, deb=0) : "trouve 1 ' indice du caractere car dans la chaine ch" i = deb while i < len(ch) : if ch[i] == car: return i # le caractere est trouve -> on termine i = i + 1 return -1 # toute la chaine a ete scannee sans succes # Tests : print trouve ( "Coucou c'est moi", "z") print trouve ("Juliette & Romeo", "&") print trouve ("Cesar & Cleopatre", "r", 5) Exercice 10.6 : prefixes, suffixe = " JKLMNOP " , "ack" for p in prefixes : print p + suffixe Exercice 10.7 : def compteMots (ch) : "comptage du nombre if len(ch) ==0: return 0 nm = 1 for c in ch: if c == " " : nm = nm + 1 return nm # Test : print compteMots ("Les petit* ruisseaux font les grandes rivieres") Exercice 10.8 : def ma juscule (car) : "renvoie si car est une majuscule" if car in "ABCDEFGHI JKLMNOPQRSTUVWXYZ" : return 1 else : return 0 de mots dans la chaine ch" # la chaine comporte au moins un mot # il suffit de compter les espaces Gerard Swinnen : Apprendre a programmer avec Python 331. Exercice 10.10 : def chaineListe (ch) : "convertit la chaine ch en une liste de mots" liste, ct = [], "" # ct est une chaine temporaire for c in ch: if c == " " : liste . append (ct) # ajouter la ch. temporaire a la liste ct = "" # re-initialiser la ch. temporaire else : ct = ct + c if ct != "": liste . append (ct) # ne pas oublier le dernier mot return liste # Test : print chaineListe ( "Une hirondelle ne fait pas le printemps") print chaineListe ("" ) Exercice 10.11 (utilise les deux fonctions definies dans les exercices precedents) : txt = "Le nom de ce Monsieur est Alphonse" 1st = chaineListe (txt) # convertir la phrase en une liste de mots for mot in 1st: # analyser chacun des mots de la liste if ma juscule (mot [0] ) : # tester le premier caractere du mot print mot Exercice 10.12 : def majuscule (car) : "renvoie si car est une majuscule" if car >= "A" and car <= "Z" : return 1 else : return 0 def minuscule (car) : "renvoie si car est une minuscule" if car >= "a" and car <= "z": return 1 else : return 0 def alphab(car): "renvoie si car est un caractere alphabet ique" if ma juscule (car) or minuscule (car) : return 1 else : return 0 332. Gerard Swinnen : Apprendre a programmer avec Python Exercice 10.15 (utilise deux fonctions definies dans les exercices precedents) : def compteMa j (ch) : "comptage des mots debutant par une majuscule dans la chaine ch" c = 0 1st = chaineListe (ch) # convertir la phrase en une liste de mots for mot in 1st : # analyser chacun des mots de la liste if ma juscule (mot [0] ) : # tester le premier caractere du mot c = c +1 return c # Test : print compteMa j ("Les filles Tidgout se nomment Justine et Corinne") Exercice 10.16 (table des codes ASCII) : c = 32 # Premier code ASCII while c < 128 : # caracteres non accentues seulement print "Code", c, chr(c), " ", c = c + 1 Exercice 10.18 (Convertir majuscules -> minuscules et inversem*nt) : def convMa jMin (ch) : "echange les majuscules et les minuscules dans la chaine ch" nouvC = " " # chaine a construire for car in ch: code = ord(car) if car >= "A" and car <= "Z": code = code + 32 elif car >= "a" and car <= "z": code = code - 32 nouvC = nouvC + chr(code) return nouvC # Test : print convMa jMin ("Ferdinand-Charles de CAMARET") Exercice 10.20 (Comptage de voyelles) : def voyelle (car) : "teste si car est une voyelle" if car in "AEIOUYaeiouy" : return 1 else : return 0 def compteVoyelles (ch) : "compte les voyelles presentes dans la chaine ch" n = 0 for c in ch: if voyelle (c) : n = n + 1 return n # Test : print compteVoyelles ( "Monty Python Flying Circus") Gerard Swinnen : Apprendre a programmer avec Python 333. Exercice 10.22 : # Comptage du nombre de mots dans un texte fiSource = raw_input ( "Nom du fichier a traiter : ") fs = open (fiSource, 'r') n = 0 # variable compteur while 1 : ch = f s . readline ( ) if ch == "" : break # conversion de la chaine lue en une liste de mots : li = ch. split () # totalisation des mots : n = n + len(li) fs .close () print "Ce fichier texte contient un total de %s mots" % (n) Exercice 10.23 : # Conversion en majuscule du premier caractere de chaque ligne fiSource = raw_input ( "Nom du fichier a traiter : ") fiDest = raw_input ( "Nom du fichier destinataire : ") fs = open (fiSource, 'r') fd = open (fiDest, 'w') while 1 : ch = f s . readline ( ) if ch == " " : break if ch[0] >= "A" and ch[0] <= "Z": # le premier car. est une majuscule. On passe, pass else : # Reconstruction de la chaine: pc = ch[0] .upper () # Premier caractere convert! rc = ch[l:] # toute le reste de la chaine ch = pc + rc # fusion # variante utilisant une methode encore plus integree : # ch = ch. capitalize () # Transcription : fd. write (ch) fd. close () fs .close () Exercice 10.24 : # Fusion de lignes pour former des phrases fiSource = raw_input ( "Nom du fichier a traiter : ") fiDest = raw_input ( "Nom du fichier destinataire : ") fs = open (fiSource, 'r') fd = open (fiDest, 'w') 334. Gerard Swinnen : Apprendre a programmer avec Python # On lit d ' abord la premiere ligne : chl = f s . readline () # On lit ensuite les suivantes, en les fusionnant si necessaire : while 1 : ch2 = f s . readline ( ) if ch2 == "" : break # Si la chaine lue commence par une majuscule, on transcrit # la precedente dans le fichier destinataire, et on la # remplace par celle que 1 ' on vient de lire : if ch2[0] >= "A" and ch2[0] <= "Z" : fd. write (chl) chl = ch2 # Sinon, on la fusionne avec la precedente : else : chl = chl [ :-l] + " " + ch2 # (veiller a enlever de chl le caractere de fin de ligne) fd. write (chl) # ne pas oublier de transcrire la derniere ! fd. close () f s . close () Exercice 10.25 (caracteristiques de spheres) : # Le fichier de depart est un fichier dont chaque ligne contient # un nombre reel (encode sous la forme d'une chaine de caracteres) from math import pi def caractSphere (d) : "renvoie les caracteristiques d'une sphere de diametre d" d = f loat (d) # conversion de 1' argument (=chaine) en reel r = d/2 # rayon ss = pi*r**2 # surface de section se = 4*pi*r**2 # surface exterieure v = 4./3*pi*r**3 # volume (! la le division doit etre reelle !) # Le marqueur de conversion %8.2f utilise ci-dessous formate le nombre # affiche de maniere a occuper 8 caracteres au total, en arrondissant # de maniere a conserver deux chiffres apres la virgule : ch = "Diam. %6.2f cm Section = %8.2f cm 2 " % (d, ss) ch = ch +"Surf. = %8.2f cm 2 . Vol. = %9.2f cm 3 " % (se, v) return ch fiSource = raw_input ( "Nom du fichier a traiter : ") fiDest = raw_input ( "Nom du fichier destinataire : ") fs = open (fiSource, 'r') fd = open (fiDest, 'w') while 1 : diam = f s . readline ( ) if diam == "" or diam == "\n": break fd. write (caractSphere (diam) + "\n") # enregistrement fd. close () f s . close () Gerard Swinnen : Apprendre a programmer avec Python 335. Exercice 10.26 : # Mise en forme de donnees numeriques # Le fichier traite est un fichier dont chaque ligne contient un nombre # reel (sans exposants et encode sous la forme d'une chaine de caracteres) def arrondir (reel) : "representation arrondie a . 0 ou .5 d'un nombre reel" ent = int(reel) # partie entiere du nombre fra = reel - ent # partie fractionnaire if fra < .25 : fra = 0 elif fra < .75 : fra = .5 else : fra = 1 return ent + fra fiSource = raw_input ( "Nom du fichier a traiter : ") fiDest = raw_input ( "Nom du fichier destinataire : ") fs = open (fiSource, 'r') fd = open (fiDest, 'w') while 1 : ligne = f s . readline () if ligne == "" or ligne == "\n": break n = arrondir (float (ligne) ) # conversion en , puis arrondi fd. write (str (n) + "\n") # enregistrement fd. close () fs .close () Exercice 10.29 : # Affichage de tables de multiplication nt = [2, 3, 5, 7, 9, 11, 13, 17, 19] def tableMulti (m, n) : "renvoie n termes de la table de multiplication par m" ch for i in range (n) : v = m * (i+1) # calcul d'un des termes ch = ch + "%4d" % (v) # formatage a 4 caracteres return ch for a in nt : print tableMulti (a, 15) # 15 premiers termes seulement Exercice 10.30 (simple parcours d'une liste) : 1st = ['Jean-Michel', 'Marc', 'Vanessa', 'Anne', 'Maximilien ' , ' Alexandre-Benoit ' , 'Louise'] for e in 1st : print "%s : %s caracteres" % (e, len(e)) 336. Gerard Swinnen : Apprendre a programmer avec Python Exercice 10.31 : # Elimination de doublons 1st = [9, 12, 40, 5, 12, 3, 27, 5, 9, 3, 8, 22, 40, 3, 2, 4, 6, 25] lst2 = [] for el in 1st : if el not in lst2 : lst2 . append (el) lst2 . sort () print lst2 Exercice 10.33 (afficher tous les jours d'une annee) : ## Cette variante utilise une liste de listes ## ## (que l'on pourrait aisem*nt remplacer par deux listes distinctes) # La liste ci-dessous contient deux elements qui sont eux-memes des listes . # 1' element 0 contient les nombres de jours de chaque mois, tandis que # 1' element 1 contient les noms des douze mois : mois = [[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31], ['Janvier', 'Fevrier', 'Mars', 'Avril', 'Mai', ' Juin ' , 'Juillet', 'Aout', 'Septembre', 'Octobre', 'Novembre', 'Decembre']] jour = [ ' Dimanche ' , ' Lundi ' , ' Mardi ' , ' Mercredi ' , ' Jeudi ' , ' Vendredi ' , ' Samedi ' ] ja, jm, js, m = 0, 0, 0, 0 while ja <365: ja, jm = ja +1, jm +1 # ja = jour dans 1' annee, jm = jour dans le mois js = (ja +3) % 7 # js = jour de la semaine. Le decalage ajoute # permet de choisir le jour de depart if jm > mois[0] [m] : # element m de 1' element 0 de la liste jm, m = 1, m+1 print jourfjs], jm, mois[l] [m] # element m de l'element 1 de la liste Exercice 10.36 : # Insertion de nouveaux elements dans une liste existante tl = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] t2 = [ ' Janvier ' , ' Fevrier ' , ' Mars ' , ' Avril ' , ' Mai ' , ' Juin ' , ' Juillet ' , ' Aout ' , ' Septembre ' , ' Octobre ' , ' Novembre ' , ' Decembre ' ] c, d = 1, 0 while d < 12 : t2[c:c] = [tl [d] ] # ! l'element insere doit etre une liste c, d = c+2, d+1 Gerard Swinnen : Apprendre a programmer avec Python 337. Exercice 10.40 : # Crible d ' Eratosthene pour rechercher les nombres premiers de 1 a 999 # Creer une liste de 1000 elements 1 (leurs indices vont de 0 a 999) : 1st = [1]*1000 # Parcourir la liste a partir de 1 ' element d'indice 2: for i in range (2, 1000) : # Mettre a zero les elements suivants dans la liste, # dont les indices sont des multiples de i : for j in range(i*2, 1000, i) : lst[j] = 0 # Afficher les indices des elements restes a 1 (on ignore 1' element 0) : for i in range (1, 1000) : if 1st [i] : print i, Exercice 10.43 (Test du generateur de nombres aleatoires, page 141) : from random import random # tire au hasard un reel entre 0 et 1 n = raw_input ( "Nombre de valeurs a tirer au hasard (defaut = 1000) : ") if n == "" : nVal =1000 else : nVal = int(n) n = raw_input ( "Nombre de fractions dans l'intervalle 0-1 (entre 2 et " + str(nVal/10) + ", defaut =5) : ") if n == "" : nFra =5 else : nFra = int (n) if nFra < 2 : nFra =2 elif nFra > nVal/10: nFra = nVal/10 print "Tirage au sort des", nVal, "valeurs ..." listVal = [0]*nVal # creer une liste de zeros for i in range (nVal) : # puis modifier chaque element listVal[i] = random () print "Comptage des valeurs dans chacune des", nFra, "fractions ..." listCompt = [0]*nFra # creer une liste de compteurs # parcourir la liste des valeurs : for valeur in listVal : # trouver 1 ' index de la fraction qui contient la valeur : index = int (valeur*nFra) # incrementer le compteur correspondant : listCompt [index] = listCompt [index] +1 # afficher 1 ' etat des compteurs : for compt in listCompt : print compt , 338. Gerard Swinnen : Apprendre a programmer avec Python Exercice 10.44 : tirage de cartes from random import randrange couleurs = ['Pique', ' Tref le ' , 'Carreau', 'Coeur'] valeurs = [2, 3, 4, 5, 6, 7, 8, 9, 10, 'valet', 'dame', ' roi ' , 'as'] # Construction de la liste des 52 cartes : carte = [ ] for coul in couleurs : for val in valeurs : carte. append ("%s de %s" % (str(val), coul)) # Tirage au hasard : while 1 : k = raw_input ( "Frappez pour tirer une carte, pour terminer ") if k =="" : break r = randrange (52) print carte [r] Exercice 10.45 : Creation et consultation d'un dictionnaire def consultation () : while 1 : nom = raw_input ( "Entrez le nom (ou pour terminer) : ") if nom == "" : break if dico . has_key (nom) : # le nom est-il repertorie ? item = dico [nom] # consultaion proprement dite age, taille = item[0] , itemfl] print "Nom : %s - age : %s ans - taille : %s m."\ % (nom, age, taille) else : print "*** nom inconnu ! ***" def remplissage () : while 1 : nom = raw_input ( "Entrez le nom (ou pour terminer) : ") if nom == " " : break age = int (raw_input ("Entrez l'age (nombre entier !) : ")) taille = float (raw_input ( "Entrez la taille (en metres) : ")) dico [nom] = (age, taille) dico ={} while 1 : choix = raw_input ( "Choisissez : (R) emplir - (C)onsulter - (T)erminer : ") if choix . upper ( ) == 'T': break elif choix . upper ( ) == 'R' : remplissage () elif choix . upper ( ) == 'C: consultation ( ) Gerard Swinnen : Apprendre a programmer avec Python 339. Exercice 10.46 : echange des cles et des valeurs dans un dictionnaire def inverse (dico) : "Construction d'un nouveau dico, pas a pas" dic_inv = { } for cle in dico: item = dico [cle] dic_inv [item] = cle return dic_inv # programme test : dico = { ' Computer ' : ' Ordinateur ' , ' Mouse ' : ' Souris ' , ' Keyboard ' : ' Clavier ' , 'Hard disk' : 'Disque dur' , ' Screen ' : ' Ecran ' } print dico print inverse (dico) Exercice 10.47 : histogramme nFich = raw_input ( ' Nom du f ichier : ' ) fi = open (nFich, 'r') texte = fi.read() # conversion du f ichier en une chaine de caracteres fi. close () print texte dico ={} for c in texte: c = cupper () # conversion de toutes les lettres en majuscules dico[c] = dico. get (c, 0) +1 liste = dico. items () liste . sort () print liste Exercice 10.48 : nFich = raw_input ( ' Nom du f ichier a traiter : ' ) fi = open (nFich, 'r') texte = fi.read() fi. close () # afin de pouvoir aisem*nt separer les mots du texte, on commence # par convertir tous les caracteres non-alphabetiques en espaces : alpha = "abcdefghi jklmnopqrstuvwxyzeeaucaeiouaeiou" lettres = ' ' # nouvelle chaine a construire for c in texte: c = c. lower () # conversion de chaque caractere en minuscule if c in alpha: lettres = lettres + c else : lettres = lettres + ' ' 340. Gerard Swinnen : Apprendre a programmer avec Python # conversion de la chaine resultante en une liste de mots mots = lettres . split () # construction de 1 ' histogramme : dico ={} for m in mots : dico[m] = dico. get (m, 0) +1 liste = dico. items () # tri de la liste resultante : liste . sort () # affichage en clair : for item in liste : print item[0], item[l] Exercice 10.49 : # encodage d'un texte dans un dictionnaire nFich = raw_input ( ' Nom du f ichier a traiter : ' ) fi = open (nFich, 'r') texte = f i . read ( ) f i . close () # On considere que les mots sont des suites de caracteres faisant partie # de la chaine ci-dessous. Tous les autres sont des separateurs : alpha = "abcdefghi jklmnopqrstuvwxyzeeaucaeiouaeiou" # construction du dictionnaire : dico ={} # parcours de tous les caracteres du texte : i =0 # indice du caractere en cours de lecture mot ="" # variable de travail : mot en cours de lecture for c in texte: c = c. lower () # conversion de chaque caractere en minuscule if c in alpha: # car. alphab. => on est a 1 ' interieur d'un mot mot = mot + c else : # car . non-alphabetique => fin de mot if mot != "": # afin d'ignorer les car. non-alphab. successifs # pour chaque mot, on construit une liste d' indices : if dico. has_key (mot) : # mot deja repertorie : dico [mot] . append (i) # ajout d'un indice a la liste else: # mot rencontre pour la le fois : dico [mot] =[i] # creation de la liste d' indices mot ="" # preparer la lecture du mot suivant i = i+1 # indice du caractere suivant # Affichage du dictionnaire, en clair : for clef, valeur in dico. items () : print clef, ":", valeur Gerard Swinnen : Apprendre a programmer avec Python 341. Exercice 10.50 : Sauvegarde d'un dictionnaire (complement de l'ex. 10.45). def enregistrement () : fich = raw_input ( "Entrez le nom du fichier de sauvegarde : ") ofi = open (fich, "w") # parcours du dictionnaire entier, converti au prealable en une liste : for cle, valeur in dico . items () : # utilisation du f ormatage des chaines pour creer 1 ' enregistrement : ofi.write("%s@%s#%s\n" % (cle, valeur[0], valeur[l])) ofi . close () def lectureFichier () : fich = raw_input ( "Entrez le nom du fichier de sauvegarde : ") try: ofi = open (fich, "r") except : print "*** fichier inexistant ***" return while 1 : ligne = of i . readline () if ligne == ' ' : break enreg = ligne . split ( "@") cle = enreg [0] valeur = enreg [1] [:— 1] data = valeur. split ("#") age, taille = int(data[0]) dico [cle] = (age, taille) ofi . close () # detection de la fin de fichier # restitution d'une liste [cle, valeur] # elimination # restitution float (data[l] ) # reconstitution du caractere de fin de ligne d'une liste [age, taille] du dictionnaire Ces deux fonctions peuvent etre appelees respectivement a la fin et au debut du programme principal, comme dans l'exemple ci-dessous : dico ={} lectureFichier ( ) while 1 : choix = raw_input ( "Choisissez : (R) emplir - (C)onsulter - (T)erminer : ") if choix . upper ( ) == 'T': break elif choix . upper ( ) == 'R': remplissage () elif choix . upper ( ) == 'C: consultation ( ) enregistrement () Exercice 10.51 : Controle du flux d'execution a l'aide d'un dictionnaire Cet exercice complete le precedent. On ajoute encore deux petites fonctions, et on reecrit le corps principal du programme pour diriger le flux d'execution en se servant d'un dictionnaire : def sortie () : print "*** Job termine ***" return 1 # afin de provoquer la sortie de la boucle 342. Gerard Swinnen : Apprendre a programmer avec Python def autre () : print "Veuillez f rapper R, A, C, S ou T, svp." dico ={} fonc ={ "R" : lectureFichier, "A" : remplissage, "C" : consultation, "S" : enregistrement, "T" :sortie} while 1 : choix = raw_input ( "Choisissez :\n" +\ "(R)ecuperer un dictionnaire preexistant sauvegarde dans un fichier\n" +\ " (A) jouter des donnees au dictionnaire courant\n" +\ "(C)onsulter le dictionnaire courant\n" +\ " (S) auvegarder le dictionnaire courant dans un fichier\n" +\ " (T) erminer : ") # 1 ' instruction ci-dessous appelle une fonction differente pour # chaque choix, par 1 ' intermediaire du dictionnaire : if fonc . get (choix, autre) () : break # Rem : toutes les fonctions appelees ici renvoient par defaut, # sauf la fonction sortie () qui renvoie 1 => sortie de la boucle Exercice 12.1 : class Domino: def init (self, pa, pb) : self .pa, self.pb = pa, pb def af fiche_points (self) : print "face A :", self .pa, print "face B :", self.pb def valeur (self ) : return self. pa + self.pb # Programme de test : dl = Domino (2, 6) d2 = Domino (4,3) dl . af f iche_points ( ) d2 . af f iche_points ( ) print "total des points :", dl. valeur () + d2. valeur () liste_dominos = [ ] for i in range (7): liste_dominos . append (Domino (6, i) ) vt =0 for i in range (7): liste_dominos [i] . af f iche_points () vt = vt + liste_dominos [i] .valeur () print "valeur totale des points", vt Gerard Swinnen : Apprendre a programmer avec Python 343. Exercice 12.3 : class Voiture : def init (self, marque = 'Ford', couleur = 'rouge'): self.couleur = couleur self .marque = marque self.pilote = 'personne' self. vitesse = 0 def accelerer (self , taux, duree) : if self.pilote == 'personne ' : print "Cette voiture n'a pas de conducteur !" else : self. vitesse = self. vitesse + taux * duree def choix_conducteur (self , nom) : self.pilote = nom def af f iche_tout (self ) : print "%s %s pilotee par %s, vitesse = %s m/s" % \ (self .marque, self.couleur, self.pilote, self .vitesse) al = Voiture ( ' Peugeot ' , ' bleue ' ) a2 = Voiture (couleur = 'verte') a3 = Voiture ( 'Mercedes ' ) al . choix_conducteur ( ' Romeo ' ) a2 . choix_conducteur ( ' Juliette ' ) a2 .accelerer (1 . 8, 12) a3 .accelerer (1 . 9, 11) a2 . af f iche_tout ( ) a3 . af f iche_tout ( ) Exercice 12.4 : class Satellite: def init (self, nom, masse =100, vitesse =0) : self. nom, self. masse, self. vitesse = nom, masse, vitesse def impulsion (self , force, duree): self. vitesse = self. vitesse + force * duree / self .masse def energie (self ) : return self .masse * self . vitesse**2 / 2 def af f iche_vitesse (self ) : print "Vitesse du satellite %s = %s m/s" \ % (self. nom, self . vitesse) # Programme de test : si = Satellite (' Zoe ' , masse =250, vitesse =10) si . impulsion (500, 15) si . af f iche_vitesse ( ) print s 1 . energie ( ) si . impulsion (500 , 15) si . af f iche_vitesse ( ) print s 1 . energie ( ) 344. Gerard Swinnen : Apprendre a programmer avec Python Exercices 12.5-12.6 (classes de cylindres et de cones) : # Classes derivees - polymorphisms class Cercle: def init (self, rayon) : self. rayon = rayon def surface (self ) : return 3.1416 * self . rayon**2 class Cylindre (Cercle) : def init (self, rayon, hauteur) : Cercle. init (self, rayon) self. hauteur = hauteur def volume (self ) : return self . surface ( ) *self . hauteur # la methode surface () est heritee de la classe parente class Cone (Cylindre) : def init (self, rayon, hauteur) : Cylindre. init (self, rayon, hauteur) def volume (self ) : return Cylindre . volume (self) /3 # cette nouvelle methode volume () remplace celle que # l'on a heritee de la classe parente (exemple de polymorphisme) cyl = Cylindre (5, 7) print cyl . surface ( ) print cyl . volume ( ) co = Cone (5, 7) print co. surface () print co . volume ( ) Gerard Swinnen : Apprendre a programmer avec Python 345. Exercice 12.7 : # Tirage de cartes from random import randrange class JeuDeCartes : """Jeu de cartes""" # attributs de classe (communs a toutes les instances) : couleur = ('Pique', ' Tref le ' , ' Carreau ' , 'Coeur') valeur = (0, 0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 'valet', 'dame', ' roi ' , 'as') def init (self) : "Construction de la liste des 52 cartes" self. carte =[] for coul in range (4) : for val in range (13) : self .carte. append ( (val +2, coul)) # la valeur commence a 2 def nom_carte (self , c) : "Renvoi du nom de la carte c, en clair" return "%s de %s" % (self .valeur [c [0] ] , self . couleur [c [1] ] ) def battre(self) : "Melange des cartes" t = len (self . carte) # nombre de cartes restantes # pour melanger, on procede a un nombre d'echanges equivalent : for i in range (t) : # tirage au hasard de 2 emplacements dans la liste : hi, h2 = randrange (t) , randrange (t) # echange des cartes situees a ces emplacements : self . carte [hi] , self . carte [h2] = self .carte [h2] , self . carte [hi] def tirer(self): "Tirage de la premiere carte de t = len (self . carte) if t >0: carte = self .carte [0] del (self .carte [0] ) return carte else : return None la pile" # verifier qu'il reste des cartes # choisir la premiere carte du jeu # la retirer du jeu # en renvoyer copie au prog, appelant # facultatif ### Programme test : if name == ' main ' : jeu = JeuDeCartes ( ) jeu.battre () for n in range (53): c = jeu.tirer() if c == None: print ' Termine ! ' else : print jeu . nom_carte (c) # instanciation d'un objet # melange des cartes # tirage des 52 cartes : # il ne reste aucune carte # dans la liste # valeur et couleur de la carte 346. Gerard Swinnen : Apprendre a programmer avec Python Exercice 12.8 : (On supposera que l'exercice precedent a ete sauvegarde sous le nom cartes.py) # Bataille de de cartes from cartes import JeuDeCartes jeuA = JeuDeCartes () # instanciation du premier jeu jeuB = JeuDeCartes () # instanciation du second jeu jeuA.battre () # melange de chacun jeuB.battre () pA, pB = 0, 0 # compteurs de points des joueurs A et B # tirer 52 fois une carte de chaque jeu : for n in range (52) : cA, cB = jeuA. tirer () , jeuB. tirer () vA, vB = cA[0] , cB[0] # valeurs de ces cartes if vA > vB: pA += 1 elif vB > vA: pB +=1 # (rien ne se passe si vA = vB) # affichage des points successifs et des cartes tirees : print "%s * %s ==> %s * %s" % ( jeuA. nom_carte (cA) , jeuB . nom_carte (cB) , pA, pB) print "le joueur A obtient %s points, le joueur B en obtient %s." % (pA, pB) Exercice 13.6 : from Tkinter import * def cercle (can, x, y, r, coul ='white'): "dessin d'un cercle de rayon en dans le canevas " can . create_oval (x-r, y-r, x+r, y+r, fill =coul) class Application (Tk) : def init (self) : Tk. init (self) # constructeur de la classe parente self. can =Canvas (self , width =475, height =130, bg ="white") self . can. pack (side =TOP, padx =5, pady =5) Button(self, text ="Train", command =self. dessine) .pack (side =LEFT) Button(self, text ="Hello", command =self. coucou) .pack (side =LEFT) Button(self, text ="Ecl34", command =self . eclai34) .pack (side =LEFT) def dessine (self ) : "instanciation de 4 wagons dans le canevas" self.wl = Wagon (self. can, 10, 30) self.w2 = Wagon (self . can, 130, 30, self.w3 = Wagon (self. can, 250, 30, self.w4 = Wagon (self . can, 370, 30, ' dark green ' ) 'maroon ' ) 'purple ' ) def coucou (self ) : "apparition de personnages dans certaines fenetres' self.wl .perso (3) self . w3 .per so (1) self . w3 .perso (2) self .w4 .perso (1) # ler wagon, 3e fenetre # 3e wagon, le fenetre # 3e wagon, 2e fenetre # 4e wagon, le fenetre def eclai34 (self) Gerard Swinnen : Apprendre a programmer avec Python 347. "allumage de l'eclairage dans les wagons 3 & 4" self . w3 . allumer ( ) self . w4 . allumer ( ) class Wagon: def init (self, canev, x, y, coul ='navy'): "dessin d'un petit wagon en dans le canevas " # memorisation des parametres dans des variables d' instance : self. canev, self.x, self.y = canev, x, y # rectangle de base : 95x60 pixels : canev. create_rect angle (x, y, x+95, y+60, fill =coul) # 3 fenetres de 25x40 pixels, ecartees de 5 pixels : self. fen =[] # pour memoriser les ref. des fenetres for xf in range (x +5, x +90, 30) : self . fen. append (canev. create_rectangle (xf , y+5, xf+25, y+40, fill = 'black')) # 2 roues de rayon egal a 12 pixels : cercle (canev, x+18, y+73, 12, 'gray') cercle (canev, x+77, y+73, 12, 'gray') def perso(self, fen): "apparition d'un petit personnage a la fenetre " # calcul des coordonnees du centre de chaque fenetre : xf = self.x + fen*30 -12 yf = self.y + 25 cercle (self . canev, xf, yf, 10, "pink") # visage cercle (self .canev, xf-5, yf-3, 2) # oeil gauche cercle (self .canev, xf+5, yf-3, 2) # oeil droit cercle (self .canev, xf, yf+5, 3) # bouche def allumer (self ) : "declencher l'eclairage interne du wagon" for f in self. fen: self . canev . itemconf igure (f, fill =' yellow') Application ( ) . app . mainloop ( ) Exercice 13.21 : # Dictionnaire de couleurs Norn de la couleur : ocre ocre Existe deja ? |#FFCC00 Test Ajouter la couleur au dictionnaire Enregistrer le dictionnaire Restaurer le dictionnaire 348. Gerard Swinnen : Apprendre a programmer avec Python from Tkinter import * # Module donnant acces aux boites de dialogue standard pour # la recherche de fichiers sur disque : from tkFileDialog import asksaveasf ile, askopenfile class Application (Frame) : ' ' 'Fenetre d' application' ' ' def init (self) : Frame . init (self) self .master .title ("Creation d'un dictionnaire de couleurs") self.dico ={} # creation du dictionnaire # Les widgets sont regroupes dans deux cadres (Frames) : frSup =Frame(self) # cadre superieur contenant 6 widgets Label (frSup, text ="Nom de la couleur :", width =20) .grid(row =1, column =1) self.enNom =Entry (f rSup, width =25) # champ d' entree pour self . enNom. grid (row =1, column =2) # le nom de la couleur Button (frSup, text ="Existe deja ?", width =12, command =self . chercheCoul) . grid (row =1, column =3) Label (frSup, text ="Code hexa. corresp. :", width =20) .grid(row =2, column =1) self. enCode =Entry (f rSup, width =25) # champ d'entree pour self . enCode . grid (row =2, column =2) # le code hexa. Button (frSup, text ="Test", width =12, command =self . testeCoul) .grid (row =2, column =3) f r Sup .pack (padx =5, pady =5) frlnf =Frame (self) # cadre inferieur contenant le reste self. test = Label (frlnf, bg ="white", width =45, # zone de test height =7, relief = SUNKEN) self .test .pack (pady =5) Button (frlnf , text ="Ajouter la couleur au dictionnaire", command =self . a jouteCoul) .pack ( ) Button (frlnf , text ="Enregistrer le dictionnaire", width =25, command =self . enregistre) .pack (side = LEFT, pady =5) Button (frlnf , text ="Restaurer le dictionnaire", width =25, command =self . restaure) .pack (side =RIGHT, pady =5) frlnf .pack (padx =5, pady =5) self .pack () def a jouteCoul (self ) : "ajouter la couleur presente au dictionnaire" if self .testeCoul () ==0: # une couleur a-t-elle ete definie ? return nom = self . enNom. get ( ) if len (nom) >1 : # refuser les noms trop petit* self . dico [nom] =self .cHexa else : self . test . config (text ="%s : nom incorrect" % nom, bg ='white') def chercheCoul (self ) : "rechercher une couleur deja inscrite au dictionnaire" nom = self . enNom. get ( ) if self . dico . has_key (nom) : self . test . config (bg =self .dico [nom] , text ="") else : self . test . config (text ="%s : couleur inconnue" % nom, bg ='white') def testeCoul (self ) : Gerard Swinnen : Apprendre a programmer avec Python 349. "verifier la validite d'un code hexa. - afficher la couleur corresp." try: self.cHexa =self .enCode.get () self .test . config (bg =self.cHexa, text ="") return 1 except : self .test . config (text ="Codage de couleur incorrect", bg ='white') return 0 def enregistre (self ) : "enregistrer le dictionnaire dans un fichier texte" # Cette methode utilise une boite de dialogue standard pour la # selection d'un fichier sur disque. Tkinter fournit toute une serie # de fonctions associees a ces boites, dans le module tkFileDialog. # La fonction ci-dessous renvoie un ob jet-f ichier ouvert en ecriture : ofi =asksaveasfile (filetypes=[ ("Texte", " .txt") , ("Tous", "*")]) for clef, valeur in self . dico . items () : ofi. write ("%s %s\n" % (clef, valeur)) ofi . close () def restaure (self ) : "restaurer le dictionnaire a partir d'un fichier de memorisation" # La fonction ci-dessous renvoie un ob jet-f ichier ouvert en lecture : ofi =askopenfile(filetypes=[ ("Texte", " .txt") , ("Tous", "*") ] ) lignes = of i . readlines () for li in lignes : cv = li. split () # extraction de la cle et la valeur corresp. self .dico [cv[0] ] = cv[l] ofi . close () if name == ' main ' : Application ( ) .mainloopO Exercice 13.22 (variante 3) : from Tkinter import * from random import randrange from math import sin, cos, pi class FaceDom: def init (self, can, val, pos, taille =70) : self. can =can x, y, c = pos[0], pos[l], taille/2 self, carre = can . create_rectangle (x -c, y-c, x+c, y+c, fill =' ivory', width =2) d = taille/3 # disposition des points sur la face, pour chacun des 6 cas : self.pDispo = [((0,0),), ((-d,d), (d,-d)), ((-d,-d), (0,0), (d,d)), ( (-d, -d) , (-d, d) , (d, -d) , (d, d) ) , ( (-d, -d) , (-d, d) , (d, -d) , (d, d) , (0, 0) ) , ( (-d, -d) , (-d, d) , (d, -d) , (d, d) , (d, 0) , (-d, 0) ) ] self.x, self.y, self. dim = x, y, taille/15 self.pList =[] # liste contenant les points de cette face self . tracer_points (val) def tracer_points (self, val) : # creer les dessins de points correspondant a la valeur val : disp = self .pDispo [val -1] 350. Gerard Swinnen : Apprendre a programmer avec Python for p in disp: self . cercle (self . x +p[0], self.y +p[l], self. dim, 'red') self.val = val def cercle (self, x, y, r, coul) : self .pList . append (self . can . create_oval (x-r, y-r, x+r, y+r, fill=coul) ) def ef facer (self , flag =0): for p in self .pList : self . can . delete (p) if flag: self .can. delete (self .carre) class Pro jet (Frame) : def init (self, larg, haut) : Frame . init (self) self. larg, self. haut = larg, haut self. can = Canvas (self, bg='dark green', width =larg, height =haut) self .can. pack (padx =5, pady =5) # liste des boutons a installer, avec leur gestionnaire : bList = [("A", self.boutA), ("B", self.boutB), ("C", self.boutC), ("Quitter", self . boutQuit ) ] bList . reverse () # inverser 1 ' ordre de la liste for b in bList : Button(self, text =b[0], command =b [1] ) .pack (side =RIGHT, padx=3) self .pack () self. des =[] # liste qui contiendra les faces de des self . actu =0 # ref . du de actuellement selectionne def boutA(self) : if len (self .des) : return # car les dessins existent deja ! a, da = 0, 2*pi/13 for i in range (13): cx, cy = self.larg/2, self.haut/2 x = cx + cx*0 . 75*sin (a) # pour disposer en cercle, y = cy + cy*0 . 75*cos (a) # on utilise la trigono ! self .des. append (FaceDom (self .can, randrange (1, 7) , (x,y), 65)) a += da def boutB(self) : # incrementer la valeur du de selectionne. Passer au suivant : v = self .des [self .actu] .val v = v % 6 v += 1 self . des [self . actu] .effacer() self . des [ self . actu] . tracer_point s ( v) self. actu += 1 self. actu = self. actu % 13 def boutC(self) : for i in range (len (self . des) ) : self. des [i] .effacer(l) self. des =[] self. actu =0 def boutQuit (self ) : self . master . destroy ( ) Projet(600, 600) .mainloop () Gerard Swinnen : Apprendre a programmer avec Python 351. Exercice 16.1 (Creation de la base de donnees "musique") : import gadfly connex = gadf ly . gadfly ( ) connex . startup ( "musique" , "E : /Python/essais/gadf ly" ) cur = connex. cursor () requete = "create table compositeurs (comp varchar, a_naiss integer, \ a_mort integer) " cur . execute (requete) requete = "create table oeuvres (comp varchar, titre varchar, \ duree integer, interpr varchar) " cur . execute (requete) print "Entree des enregistrements , table des compositeurs :" while 1 : nm = raw_input ( "Nom du compositeur ( pour terminer) : ") if nm == ' ' : break an = raw_input ( "Annee de naissance : ") am = raw_input ( "Annee de mort : " ) requete ="insert into compositeurs (comp, a_naiss, a_mort) values \ ('%s', %s, %s)" % (nm, an, am) cur . execute (requete) # Affichage des donnees entrees, pour verification : cur .execute ("select * from compositeurs") print cur . pp ( ) print "Entree des enregistrements, table des oeuvres musicales :" while 1 : nom = raw_input ( "Nom du compositeur ( pour terminer) : ") if nom == ' ' : break tit = raw_input ( "Titre de l'oeuvre : ") dur = raw_input (" duree (minutes) : ") int = raw_input ( " interprete principal : " ) requete =" insert into oeuvres (comp, titre, duree, interpr) values \ ('%s', '%s', %s, '%s')" % (nom, tit, dur, int) cur . execute (requete) # Affichage des donnees entrees, pour verification : cur .execute ("select * from oeuvres") print cur . pp ( ) connex . commit ( ) Exercice 18.2 : ##################################### # Bombardement d'une cible mobile # # (C) G. Swinnen - Avril 2004 - GPL # ##################################### from Tkinter import * from math import sin, cos, pi from random import randrange from threading import Thread class Canon: """Petit canon graphique""" def init (self, boss, num, x, y, sens) : self .boss = boss # reference du canevas 352. Gerard Swinnen : Apprendre a programmer avec Python self.num = num # n° du canon dans la liste self.xl, self.yl = x, y # axe de rotation du canon self. sens = sens # sens de tir (-1: gauche, +l:droite) self.lbu =30 # longueur de la buse # dessiner la buse du canon (horizontale) : self.x2, self.y2 = x + self.lbu * sens, y self. buse = boss . create_line (self .xl, self.yl, self.x2, self.y2, width =10) # dessiner le corps du canon (cercle de couleur) : self.rc =15 # rayon du cercle self. corps = boss . create_oval (x -self.rc, y -self.rc, x +self.rc, y +self.rc, fill = 'black') # pre-dessiner un obus (au depart c'est un simple point) : self.obus = boss . create_oval (x, y, x, y, fill='red') self.anim = 0 # retrouver la largeur et la hauteur du canevas : self.xMax = int (boss . cget ( 'width ') ) self.yMax = int (boss . cget ( 'height ') ) def orienter (self , angle): "regler la hausse du canon" # rem : le parametre est recu en tant que chaine. # il faut done le traduire en reel, puis le convertir en radians : self. angle = float (angle) *2*pi/360 self.x2 = self.xl + self.lbu * cos (self . angle) * self. sens self.y2 = self.yl - self.lbu * sin (self . angle) self .boss . coords (self .buse, self.xl, self.yl, self.x2, self.y2) def feu (self ) : "declencher le tir d'un obus" # reference de 1 ' ob jet cible : self.cible = self .boss .master . cible if self.anim ==0: self.anim =1 # position de depart de l'obus (c'est la bouche du canon) : self.xo, self.yo = self.x2, self.y2 v = 20 # vitesse initiale # composantes verticale et horizontale de cette vitesse : self.vy = -v *sin (self . angle) self.vx = v *cos (self . angle) *self.sens self . animer_obus ( ) def animer_obus (self ) : "animer l'obus (trajectoire balistique) " # positionner l'obus, en re-def inissant ses coordonnees : self .boss . coords (self . obus, self.xo -3, self.yo -3, self.xo +3, self.yo +3) if self.anim >0 : # calculer la position suivante : self.xo += self.vx self.yo += self.vy self.vy += .5 self . test_obstacle () # a-t-on atteint un obstacle ? self .boss . after (1, self . animer_obus) else : # fin de 1 ' animation : self .boss . coords (self . obus, self.xl, self.yl, self.xl, self.yl) def test_obstacle (self ) : "evaluer si l'obus a atteint une cible ou les limites du jeu" if self.yo >self.yMax or self.xo <0 or self.xo >self.xMax: Gerard Swinnen : Apprendre a programmer avec Python 353. self.anim =0 return if self.yo > self.cible.y -3 and self.yo < self.cible.y +18 \ and self.xo > self.cible.x -3 and self.xo < self.cible.x +43: # dessiner 1' explosion de l'obus (cercle orange) : self.explo = self .boss . create_oval (self . xo -10, self.yo -10, self.xo +10, self.yo +10, fill =' orange', width =0) self .boss . after (150, self . fin_explosion) self.anim =0 def f in_explosion (self ) : "ef facer le cercle d' explosion - gerer le score" self .boss . delete (self . explo) # signaler le succes a la fenetre maitresse : self . boss . master . goal ( ) class Pupitre (Frame) : """Pupitre de pointage associe a un canon""" def init (self, boss, canon) : Frame. init (self, bd =3, relief =GROOVE) self. score =0 s =Scale(self, from_ =88, to =65, troughcolor = ' dark grey ' , command =canon . orienter) s. set (45) # angle initial de tir s. pack (side =LEFT) Label(self, text =' Hausse ') .pack (side =TOP, anchor =W, pady =5) Button (self, text ='Feu !', command =canon . f eu) . \ pack (side =BOTTOM, padx =5, pady =5) Label(self, text ="points") .pack () self .points =Label (self , text=' 0 ' , bg =' white') self . points . pack ( ) # positionner a gauche ou a droite suivant le sens du canon : gd = (LEFT, RIGHT) [canon. sens == -1] self .pack (padx =3, pady =5, side =gd) def attribuerPoint (self , p) : "incrementer ou decrementer le score" self. score += p self .points . config (text = ' %s ' % self. score) class Cible: """ob jet graphique servant de cible""" def init (self, can, x, y) : self. can = can # reference du canevas self.x, self.y = x, y self. cible = can . create_oval (x, y, x+40, y+15, fill =' purple') def deplacer (self , dx, dy) : "effectuer avec la cible un deplacement dx,dy" self .can. move (self .cible, dx, dy) self.x += dx self.y += dy return self.x, self.y class Thread_cible (Thread) : """ob jet thread gerant 1' animation de la cible""" def init (self, app, cible) : Thread. init (self) self. cible = cible # objet a deplacer 354. Gerard Swinnen : Apprendre a programmer avec Python self . app = app self.sx, self.sy = 6, 3 self.dt =300 # ref. de la fenetre d' application # increments d'espace et de # temps pour 1' animation (ms) def run (self) : "animation, tant que la fenetre d' application existe" x, y = self . cible .deplacer (self . sx, self.sy) if x > self. app. xm -50 or x < self. app. xm /5: self.sx = -self.sx if y < self. app. ym /2 or y > self. app. ym -20: self.sy = -self.sy if self. app != None: self . app. after (int (self . dt) , self . run) def stop (self ) : "fermer le thread si la fenetre d' application est refermee" self . app =None def accelere (self ) : "accelerer le mouvement" self.dt /= 1.5 class Application (Frame) : def init (self) : Frame . init (self) self .master .title (' «< Tir sur cible mobile »>') self .pack () self .xm, self .ym = 600, 500 self.jeu = Canvas (self, width =self.xm, height =self.ym, bg =' ivory', bd =3, relief =SUNKEN) self . jeu. pack (padx =4, pady =4, side =TOP) # Instanciation d'un canon et d'un pupitre de pointage : x, y = 30, self.ym -20 self. gun =Canon (self . jeu, 1, x, y, 1) self .pup =Pupitre (self , self. gun) # instanciation de la cible mobile : self. cible = Cible (self . jeu, self.xm/2, self.ym -25) # animation de la cible mobile, sur son propre thread : self.tc = Thread_cible (self , self. cible) self .tc. start () # arreter tous les threads lorsque 1 ' on ferme la fenetre : self .bind ( ' ' , self . fermer_threads) def goal (self) : "la cible a ete touchee" self . pup . att r ibuerPoint ( 1 ) self . tc . accelere ( ) def fermer_threads (self, evt) : "arreter le thread d' animation de la cible" self .tc. stop () if name == ' main ' : Application () .mainloopO Gerard Swinnen : Apprendre a programmer avec Python 355. 19.10 Annexes extraites de « How to think like a computer scientist » Suivant les termes de la GNU Free Documentation licence (voir p. 361), les annexes qui suivent doivent obligatoirement accompagner telles quelles toute distribution du texte original, que celui-ci ait ete modifie (traduit, par exemple) ou non. 19.10. 1 Contributor list by Jeffrey Elkner Perhaps the most exciting thing about a free content textbook is the possibility it creates for those using the book to collaborate in its development. I have been delighted by the many responses, suggestions, corrections, and words of encouragement I have received from people who have found this book to be useful, and who have taken the time to let me know about it. Unfortunately, as a busy high school teacher who is working on this project in my spare time (what little there is of it ;-), I have been neglectful in giving credit to those who have helped with the book. I always planned to add an "Acknowlegdements" sections upon completion of the first stable version of the book, but as time went on it became increasingly difficult to even track those who had contributed. Upon seeing the most recent version of Tony Kuphaldt's wonderful free text, "Lessons in Electric Circuits", I got the idea from him to create an ongoing "Contributor List" page which could be easily modified to include contributors as they come in. My only regret is that many earlier contributors might be left out. I will begin as soon as possible to go back through old emails to search out the many wonderful folks who have helped me in this endeavour. In the mean time, if you find yourself missing from this list, please accept my humble apologies and drop me an email atjeff@elkner.net to let me know about my oversight. And so, without further delay, here is a listing of the contributors: Lloyd Hugh Allen Lloyd sent in a correction to section 8.4. He can be reached at: lha2@columbia.edu Yvon Boulianne Yvon sent in a correction of a logical error in Chapter 5. She can be reached at: mystic@monuniverse.net Fred Bremmer Fred submitted a correction in section 2. 1. He can be reached at: Fred.Bremmer@ubc.cu Jonah Cohen Jonah wrote the Perl scripts to convert the LaTeX source for this book into beautiful HTML. His Web page isjonah.ticalc.org and his email is JonahCohen@aol.com Michael Conlon Michael sent in a grammer correction in Chapter 2 and an improvement in style in Chapter 1, and he initiated discussion on the technical aspects of interpreters. Michael can be reached at: michael.conlon@sru.edu Courtney Gleason Courtney and Katherine Smith created the first version of horsebet.py, which is used as the case study for the last chapters of the book. Courtney can be reached at: orionl558@aol.com Lee Harr Lee submitted corrections for sections 10.1 and 1 1.5. He can be reached at: missive@linuxfreemail.com James Kaylin James is a student using the text. He has submitted numerous corrections. James can be reached by email at: Jamarf@aol.com David Kershaw David fixed the broken catTwice function in section 3.10. He can be reached at: david_kershaw@merck.com Eddie Lam Eddie has sent in numerous corrections to Chapters 1, 2, and 3. He also fixed the Makefile so that it 356. Gerard Swinnen : Apprendre a programmer avec Python creates an index the first time it is run and helped us set up a versioning scheme. Eddie can be reached at: nautilus@yoyo.cc.monash.edu.au Man-Yong Lee Man-Yong sent in a correction to the example code in section 2.4. He can be reaced at: yong@linuxkorea.co.kr David Mayo While he didn't mean to hit us over the head with it, David Mayo pointed out that the word "unconsciously" in chapter 1 needed to be changed to "subconsciously". David can be reached at: bdbear44@netscape.net Chris McAIoon Chris sent in several corrections to sections 3.9 and 3.10. He can be reached at: cmcaloon@ou.edu Matthew J. Moelter Matthew has been a long-time contributor who sent in numerous corrections and suggestions to the book. He can be reached at: mmoelter@calpoly.edu Simon Dicon Montford Simon reported a missing function definition and several typos in Chapter 3. He also found errors in the increment function in Chapter 13. He can be reached at: dicon@bigfoot.com John Ouzts John sent in a correction to the "return value" definition in Chapter 3. He can be reached at: jouzts@bigfoot.com Kevin Parks Kevin sent in valuable comments and suggestions as to how to improve the distribution of the book. He can be reached at: cpsoct@lycos.com David Pool David sent in a typo in the glossary of chapter 1, as well as kind words of encouragement. He can be reached at: pooldavid@hotmail.com Michael Schmitt Michael sent in a correction to the chapter on files and exceptions. He can be reached at: ipv6_128@yahoo.com Paul Sleigh Paul found an error in Chapter 7 and a bug in Jonah Cohen's Perl script that generates HTML from LaTeX. He can be reached at: bat@atdot.dotat.org Christopher Smith Chris is a computer science teacher at the Blake School in Minnesota who teaches Python to his beginning students. He can be reached at: csmith@blakeschool.org or smiles@saysomething.com Katherine Smith Katherine and Courtney Gleason created the first version of horsebet.py, which is used as the case study for the last chapters of the book. Katherine can be reached at: kss_0326@yahoo.com Craig T. Snydal Craig is testing the text in a course at Drew University. He has contributed several valuable suggestions and corrections, and can be reached at: csnydal@drew.edu Ian Thomas Ian and his students are using the text in a programming course. They are the first ones to test the chapters in the latter half of the book, and they have make numerous corrections and suggestions. Ian can be reached at: ithomas@sd70.bc.ca Keith Verheyden Keith made correction in Section 3.11 and can be reached at: kverheyd@glam.ac.uk Chris Wrobel Chris made corrections to the code in the chapter on file I/O and exceptions. He can be reached at: Gerard Swinnen : Apprendre a programmer avec Python 357. ferz9 80@yahoo. com Moshe Zadka Moshe has made invaluable contributions to this project. In addition to writing the first draft of the chapter on Dictionaries, he provided continual guidance in the early stages of the book. He can be reached at: moshez@math.huji.ac.il 19.10.2 Preface by J. Elkner This book owes its existance to the collaboration made possible by the internet and the free software movement. Its three authors, a college professor, a high school teacher, and a professional programmer, have yet to meet face to face, but we have been able to work closely together, and have been aided by many wonderful folks who have donated their time and energy to helping make it better. What excites me most about it is that it is a testament to both the benefits and future possibilities of this kind of collaboration, the framework for which has been put in place by Richard Stallman and the Free Software Foundation. a) How and why I came to use Python In 1999, the College Board's Advanced Placement Computer Science exam was given in C++ for the first time. As in many high schools throughout the country, the decision to change languages had a direct impact on the computer science curriculum where I teach at Yorktown High School, in Arlington, Virginia. Up to this point, Pascal was the language of instruction in both our first year and AP courses. In keeping with past practice of giving students two years of exposure to the same language, we made the decision to switch to C++ in the first year course for the 1997-98 school year, so that we would be in step with the College Board's change for the AP course the following year. Two years later, I was convinced that C++ was a poor choice to use for introducing students to computer science. While it is certainly a very powerful programming language, it is also an extremely difficult language to learn and teach. I found myself constantly fighting with C++'s difficult syntax and multiple ways of doing things, and I was losing many students unnecessarily as a result. Convinced there had to be a better language choice for our first year class, I went looking for an alternative to C++. A discussion on the High School Linux Users' Group mailing list provided a solution. A thread emerged during the latter part of January, 1999 concerning the best programming language for use with first time high school computer science students. In a posting on January 30th, Brendon Ranking wrote: I believe that Python is the best choice for any entry-level programming class. It teaches proper programming principles while being incredibly easy to learn. It is also designed to be object oriented from its inception so it doesn't have the add-on pain that both Perl and C++ suffer from It is also *very* widely supported and very much web-centric, as well. I had first heard of Python a few years earlier at a Linux Install Fest, when an enthusiastic Michael McLay told me about Python's many merits. He and Brendon had now convinced me that I needed to look into Python. Matt Ahrens, one of Yorktown's gifted students, jumped at the chance to try out Python, and in the final two months of the 1998-99 school year he not only learned the language but wrote an application called pyTicket which enabled our staff to report technology problems via the web. I knew that Matt could not have finished an application of that scale in so short a time in C++, and this accomplishment combined with Matt's positive assessment of Python suggested Python was the solution I was looking for. b) Finding a text book Having decided to use Python in both my introductory computer science classes the following year, the most pressing problem was the lack of an available text book. Free content came to the rescue. Earlier in the year Richard Stallman had introduced me to Allen Downey. Both of us had written to Richard expressing an interest in developing free educational content. 358. Gerard Swinnen : Apprendre a programmer avec Python Allen had already written a first year computer science text book titled, How to think like a computer scientist. When I read this book I knew immediately that I wanted to use it in my class. It was the clearest and most helpful computer science text I had seen. It emphasized the processes of thought involved in programming, rather than the features of a particular language. Reading it immediately made me a better teacher. Not only was How to think like a computer scientist an excellent book, but it was also released under a GNU public license, which meant it could be used freely and modified to meet the needs of its user. Once I decided to use Python, it occurred to me that I could translate Allen's original Java version into the new language. While I would not have been able to write a text book on my own, having Allen's book to work from made it possible for me to do so, at the same time demonstrating that the cooperative development model used so well in software could also work for educational content. Working on this book for the last two years has been rewarding for both me and my students, and the students played a big part in the process. Since I could make instant changes whenever someone found a spelling error or difficult passage, I encouraged them to look for errors in the book by giving them a bonus point every time they found or suggested something that resulted in a change in the text. This had the double benefit of encouraging them to read the text more carefully, and of getting the text thoroughly reviewed by its most important critics, students using it to learn computer science. For the second half of the book on object oriented programming, I knew that someone with more real programming experience than I had would be needed to do it right. The book actually sat in an unfinished state for the better part of a year until two things happened that led to its completion. I received an email from Chris Meyers expressing interest in the book. Chris is a professional programmer who started teaching a programming course last year using Python at Lane Community College in Eugene Oregon. The prospect of teaching the course had led Chris to the book, and he started helping out with it immediately. By the end of the school year he had created a companion project on our web site at http://www.ibiblio.org/obp called Python for Fun and was working with some of my most advanced students as a master teacher, guiding them beyond the places I could take them. c) Introducing programming with Python The process of translating and using How to think like a computer scientist for the past two years has confirmed Python's suitability to teaching beginning students. Python greatly simplifies programming examples and makes important programming ideas easier to teach. The first example from the text dramatically illustrates this point. It is the traditional "hello, world" program, which in the C++ version of the book looks like this: #include void main O { cout « "Hello, world." « endl; } in the Python version it becomes: print "Hello, World!" Even though this is a trivial example, the advantages to Python stand out. There are no prerequisites to Yorktown's Computer Science I course, so many of the students seeing this example are looking at their first program. Some of them are undoubtedly a little nervous, having heard that computer programming is difficult to learn. The C++ version has always forced me to choose between two unsatisfying options: either to explain the #include, void main(), {, and } statements, and risk confusing or intimidating some of the students right at the start, or to tell them "just don't worry about all of that stuff now, we will talk about it later" and risk the same thing. The educational objectives at this point in the course are to introduce students to the idea of a programming statement and to get them to make their first program, thereby introducing them to the programming environment. The Python program has exactly what is needed to do these things, and nothing more. Gerard Swinnen : Apprendre a programmer avec Python 359. Comparing Section 1.5 of each version of the book, where this first program is located, further illustrates what this means to the beginning student. There are thirteen paragraphs of explanation of "Hello, world" in the C++ version, in the Python version there are only two. More importantly, the missing eleven paragraphs do not deal with the "big ideas" in computer programming, but with the minutia of C++ syntax. I found this same thing happening throughout the book. Whole paragraphs simply disappear from the Python version of the text because Python's much clearer syntax renders them unnecessary. Using a very high level language like Python allows a teacher to postpone talking about low level details of the machine until students have the background that they need to better make sense of the details. It thus creates the ability to put "first things first" pedagogically. One of the best examples of this is the way in which Python handles variables. In C++ a variable is a name for a place which holds a thing. Variables have to be declared with types at least in part because the size of the place to which they refer needs to be predetermined. Thus the idea of a variable is bound up with the hardware of the machine. The powerful and fundamental concept of a variable is already difficult enough for beginning students (in both Computer Science and Algebra). Bytes and addresses do not help the matter. In Python a variable is a name which refers to a thing. This is a far more intuitive concept for beginning students, and one which is much closer to the meaning of variable that they learned in their math class. I had much less difficulty teaching variables this year than I did in the past, and I spent less time helping students with problems using them. Another example of how Python aides in the teaching and learning of programming is in its syntax for functions. My students have always had a great deal of difficulty understanding functions. The main problem centers around the difference between a function definition and a function call, and the related distinction between a parameter and an argument. Python comes to the rescue with syntax that is nothing short of beautiful. Function definitions begin with the key word def, so I simply tell my students, "when you define a function, begin with def, followed by the name of the function that you are defining, when you call a function, simply call (type) out its name." Parameters go with definitions, arguments go with calls. There are no return types or parameter types or reference and value parameters to get in the way, so I am now able to teach functions in less then half the time that it previously took me, with better comprehension. Using Python has improved the effectiveness of our computer science program for all students. I see a higher general level of success and a lower level of frustration than I experienced during the two years using C++. I move faster with better results. More students leave the course with the ability to create meaningful programs, and with the positive attitude toward the experience of programming that this engenders. d) Building a community I have received email every continent on the globe and from as far away as Korea from people using this book to learn or to teach programming. A user community has begun to emerge and increasing numbers of people have been contributing to the project by sending in materials for the companion web site at http://www.ibiblio.org/obp. With the publication of the book in print form, I expect the growth in the user community to continue and accelerate. It is the emergence of this user community and the possibility it suggests for similar collaboration among educators that has been the most exciting thing for me about working on the project. By working together we can both increase the quality of materials available for our use and save valuable time. I invite you to join our community and look forward to hearing from you. Jeffrey Elkner Yorktown High School Arlington, Virginia 360. Gerard Swinnen : Apprendre a programmer avec Python 19.10.3 GNU Free Documentation License Version 1.1, March 2000 Copyright © 2000 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The purpose of this License is to make a manual, textbook, or other written document "free" in the sense of freedom: to assure everyone the effective freedom to copy and redistribute it, with or without modifying it, either commercially or noncommercially. Secondarily, this License preserves for the author and publisher a way to get credit for their work, while not being considered responsible for modifications made by others. This License is a kind of "copyleft," which means that derivative works of the document must themselves be free in the same sense. It complements the GNU General Public License, which is a copyleft license designed for free software. We have designed this License in order to use it for manuals for free software, because free software needs free documentation: a free program should come with manuals providing the same freedoms that the software does. But this License is not limited to software manuals; it can be used for any textual work, regardless of subject matter or whether it is published as a printed book. We recommend this License principally for works whose purpose is instruction or reference. 19.10.3. a.l Applicability and Definitions This License applies to any manual or other work that contains a notice placed by the copyright holder saying it can be distributed under the terms of this License. The "Document," below, refers to any such manual or work. Any member of the public is a licensee, and is addressed as "you." A "Modified Version" of the Document means any work containing the Document or a portion of it, either copied verbatim, or with modifications and/or translated into another language. A "Secondary Section" is a named appendix or a front-matter section of the Document that deals exclusively with the relationship of the publishers or authors of the Document to the Document's overall subject (or to related matters) and contains nothing that could fall directly within that overall subject. (For example, if the Document is in part a textbook of mathematics, a Secondary Section may not explain any mathematics.) The relationship could be a matter of historical connection with the subject or with related matters, or of legal, commercial, philosophical, ethical, or political position regarding them. The "Invariant Sections" are certain Secondary Sections whose titles are designated, as being those of Invariant Sections, in the notice that says that the Document is released under this License. The "Cover Texts" are certain short passages of text that are listed, as Front-Cover Texts or Back-Cover Texts, in the notice that says that the Document is released under this License. A "Transparent" copy of the Document means a machine-readable copy, represented in a format whose specification is available to the general public, whose contents can be viewed and edited directly and straightforwardly with generic text editors or (for images composed of pixels) generic paint programs or (for drawings) some widely available drawing editor, and that is suitable for input to text formatters or for automatic translation to a variety of formats suitable for input to text formatters. A copy made in an otherwise Transparent file format whose markup has been designed to thwart or discourage subsequent modification by readers is not Transparent. A copy that is not "Transparent" is called "Opaque." Examples of suitable formats for Transparent copies include plain ASCII without markup, Texinfo input format, \LaTeX~input format, SGML or XML using a publicly available DTD, and standard-conforming simple HTML designed for human modification. Opaque formats include PostScript, PDF, proprietary formats that can be read and edited only by proprietary word processors, SGML or XML for which the DTD and/or processing tools are not generally available, and the machine-generated HTML produced by some word processors for output purposes only. The "Title Page" means, for a printed book, the title page itself, plus such following pages as are needed Gerard Swinnen : Apprendre a programmer avec Python 361. to hold, legibly, the material this License requires to appear in the title page. For works in formats which do not have any title page as such, "Title Page" means the text near the most prominent appearance of the work's title, preceding the beginning of the body of the text. 19.10.3.a.2 Verbatim Copying You may copy and distribute the Document in any medium, either commercially or noncommercially, provided that this License, the copyright notices, and the license notice saying this License applies to the Document are reproduced in all copies, and that you add no other conditions whatsoever to those of this License. You may not use technical measures to obstruct or control the reading or further copying of the copies you make or distribute. However, you may accept compensation in exchange for copies. If you distribute a large enough number of copies you must also follow the conditions in Section 3. You may also lend copies, under the same conditions stated above, and you may publicly display copies. 19.10.3.a.3 Copying in Quantity If you publish printed copies of the Document numbering more than 100, and the Document's license notice requires Cover Texts, you must enclose the copies in covers that carry, clearly and legibly, all these Cover Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on the back cover. Both covers must also clearly and legibly identify you as the publisher of these copies. The front cover must present the full title with all words of the title equally prominent and visible. You may add other material on the covers in addition. Copying with changes limited to the covers, as long as they preserve the title of the Document and satisfy these conditions, can be treated as verbatim copying in other respects. If the required texts for either cover are too voluminous to fit legibly, you should put the first ones listed (as many as fit reasonably) on the actual cover, and continue the rest onto adjacent pages. If you publish or distribute Opaque copies of the Document numbering more than 100, you must either include a machine-readable Transparent copy along with each Opaque copy, or state in or with each Opaque copy a publicly accessible computer-network location containing a complete Transparent copy of the Document, free of added material, which the general network-using public has access to download anonymously at no charge using public-standard network protocols. If you use the latter option, you must take reasonably prudent steps, when you begin distribution of Opaque copies in quantity, to ensure that this Transparent copy will remain thus accessible at the stated location until at least one year after the last time you distribute an Opaque copy (directly or through your agents or retailers) of that edition to the public. It is requested, but not required, that you contact the authors of the Document well before redistributing any large number of copies, to give them a chance to provide you with an updated version of the Document. 19.10.3.a.4 Modifications You may copy and distribute a Modified Version of the Document under the conditions of Sections 2 and 3 above, provided that you release the Modified Version under precisely this License, with the Modified Version filling the role of the Document, thus licensing distribution and modification of the Modified Version to whoever possesses a copy of it. In addition, you must do these things in the Modified Version: • Use in the Title Page (and on the covers, if any) a title distinct from that of the Document, and from those of previous versions (which should, if there were any, be listed in the History section of the Document). You may use the same title as a previous version if the original publisher of that version gives permission. • List on the Title Page, as authors, one or more persons or entities responsible for authorship of the modifications in the Modified Version, together with at least five of the principal authors of the Document (all of its principal authors, if it has less than five). • State on the Title page the name of the publisher of the Modified Version, as the publisher. • Preserve all the copyright notices of the Document. • Add an appropriate copyright notice for your modifications adjacent to the other copyright notices. • Include, immediately after the copyright notices, a license notice giving the public permission to use the Modified Version under the terms of this License, in the form shown in the Addendum below. 362. Gerard Swinnen : Apprendre a programmer avec Python • Preserve in that license notice the full lists of Invariant Sections and required Cover Texts given in the Document's license notice. • Include an unaltered copy of this License. • Preserve the section entitled "History," and its title, and add to it an item stating at least the title, year, new authors, and publisher of the Modified Version as given on the Title Page. If there is no section entitled "History" in the Document, create one stating the title, year, authors, and publisher of the Document as given on its Title Page, then add an item describing the Modified Version as stated in the previous sentence. • Preserve the network location, if any, given in the Document for public access to a Transparent copy of the Document, and likewise the network locations given in the Document for previous versions it was based on. These may be placed in the "History" section. You may omit a network location for a work that was published at least four years before the Document itself, or if the original publisher of the version it refers to gives permission. • In any section entitled "Acknowledgements" or "Dedications," preserve the section's title, and preserve in the section all the substance and tone of each of the contributor acknowledgements and/or dedications given therein. • Preserve all the Invariant Sections of the Document, unaltered in their text and in their titles. Section numbers or the equivalent are not considered part of the section titles. • Delete any section entitled "Endorsem*nts." Such a section may not be included in the Modified Version. • Do not retitle any existing section as "Endorsem*nts" or to conflict in title with any Invariant Section. If the Modified Version includes new front-matter sections or appendices that qualify as Secondary Sections and contain no material copied from the Document, you may at your option designate some or all of these sections as invariant. To do this, add their titles to the list of Invariant Sections in the Modified Version's license notice. These titles must be distinct from any other section titles. You may add a section entitled "Endorsem*nts," provided it contains nothing but endorsem*nts of your Modified Version by various parties — for example, statements of peer review or that the text has been approved by an organization as the authoritative definition of a standard. You may add a passage of up to five words as a Front-Cover Text, and a passage of up to 25 words as a Back-Cover Text, to the end of the list of Cover Texts in the Modified Version. Only one passage of Front- Cover Text and one of Back-Cover Text may be added by (or through arrangements made by) any one entity. If the Document already includes a cover text for the same cover, previously added by you or by arrangement made by the same entity you are acting on behalf of, you may not add another; but you may replace the old one, on explicit permission from the previous publisher that added the old one. The author(s) and publisher(s) of the Document do not by this License give permission to use their names for publicity for or to assert or imply endorsem*nt of any Modified Version. 19.10.3. a.5 Combining Documents You may combine the Document with other documents released under this License, under the terms defined in Section 4 above for modified versions, provided that you include in the combination all of the Invariant Sections of all of the original documents, unmodified, and list them all as Invariant Sections of your combined work in its license notice. The combined work need only contain one copy of this License, and multiple identical Invariant Sections may be replaced with a single copy. If there are multiple Invariant Sections with the same name but different contents, make the title of each such section unique by adding at the end of it, in parentheses, the name of the original author or publisher of that section if known, or else a unique number. Make the same adjustment to the section titles in the list of Invariant Sections in the license notice of the combined work. In the combination, you must combine any sections entitled "History" in the various original documents, forming one section entitled "History"; likewise combine any sections entitled "Acknowledgements," and any sections entitled "Dedications." You must delete all sections entitled "Endorsem*nts." 19.10.3. a.6 Collections of Documents You may make a collection consisting of the Document and other documents released under this License, Gerard Swinnen : Apprendre a programmer avec Python 363. and replace the individual copies of this License in the various documents with a single copy that is included in the collection, provided that you follow the rules of this License for verbatim copying of each of the documents in all other respects. You may extract a single document from such a collection, and distribute it individually under this License, provided you insert a copy of this License into the extracted document, and follow this License in all other respects regarding verbatim copying of that document. 19.10.3.a.7 Aggregation with Independent Works A compilation of the Document or its derivatives with other separate and independent documents or works, in or on a volume of a storage or distribution medium, does not as a whole count as a Modified Version of the Document, provided no compilation copyright is claimed for the compilation. Such a compilation is called an "aggregate," and this License does not apply to the other self-contained works thus compiled with the Document, on account of their being thus compiled, if they are not themselves derivative works of the Document. If the Cover Text requirement of Section 3 is applicable to these copies of the Document, then if the Document is less than one quarter of the entire aggregate, the Document's Cover Texts may be placed on covers that surround only the Document within the aggregate. Otherwise they must appear on covers around the whole aggregate. 19.10.3.a.8 Translation Translation is considered a kind of modification, so you may distribute translations of the Document under the terms of Section 4. Replacing Invariant Sections with translations requires special permission from their copyright holders, but you may include translations of some or all Invariant Sections in addition to the original versions of these Invariant Sections. You may include a translation of this License provided that you also include the original English version of this License. In case of a disagreement between the translation and the original English version of this License, the original English version will prevail. 19.10.3.a.9 Termination You may not copy, modify, sublicense, or distribute the Document except as expressly provided for under this License. Any other attempt to copy, modify, sublicense, or distribute the Document is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 19.10.3.a.l0 Future Revisions of This License The Free Software Foundation may publish new, revised versions of the GNU Free Documentation License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. See http:///www.gnu.org/copyleft/. Each version of the License is given a distinguishing version number. If the Document specifies that a particular numbered version of this License "or any later version" applies to it, you have the option of following the terms and conditions either of that specified version or of any later version that has been published (not as a draft) by the Free Software Foundation. If the Document does not specify a version number of this License, you may choose any version ever published (not as a draft) by the Free Software Foundation. 364. Gerard Swinnen : Apprendre a programmer avec Python Table des matieres Introduction 4 Choix d'un premier langage de programmation 5 Presentation du langage Python, par Stefane Fermigier 6 Plusieurs versions differentes ? 7 Distribution de Python - Bibliographie 8 Pour le professeur qui souhaite utiliser cet ouvrage comme support de cours 9 Exemples du livre 10 Remerciements 10 Chapitre 1 : Penser comme un programmeur 11 1.1 La demarche du programmeur 11 1.2 Langage machine, langage de programmation 11 1 .3 Compilation et interpretation 13 1.4 Mise au point d'un programme - Recherche des erreurs (« debug ») 15 1 .4. 1 Erreurs de syntaxe 15 1.4.2 Erreurs semantiques 15 1.4.3 Erreurs a l'execution 16 1.5 Recherche des erreurs et experimentation 16 1.6 Langages naturels et langages formels 17 Chapitre 2 : Premiers pas 19 2. 1 Calculer avec Python 19 2.2 Donnees et variables 21 2.3 Noms de variables et mots reserves 22 2.4 Affectation (ou assignation) 23 2.5 Afficher la valeur dune variable 24 2.6 Typage des variables 24 2.7 Affectations multiples 25 2.8 Operateurs et expressions 26 2.9 Priorite des operations 27 2.10 Composition 28 Chapitre 3 : Controle du flux d'instructions 29 3.1 Sequence d'instructions 29 3.2 Selection ou execution conditionnelle 29 3.3 Operateurs de comparaison 31 3.4 Instructions composees - Blocs d'instructions 31 3.5 Instructions imbriquees 32 3.6 Quelques regies de syntaxe Python 32 3.6.1 Les limites des instructions et des blocs sont definies par la mise en page 32 3.6.2 Instruction composee = En-tete , double point , bloc d'instructions indente 33 3.6.3 Les espaces et les commentaires sont normalement ignores 33 Chapitre 4 : Instructions repetitives 34 4.1 Re-affectation 34 Gerard Swinnen : Apprendre a programmer avec Python 365. 4.2 Repetitions en boucle - l'instruction while 35 4.3 Elaboration de tables 37 4.4 Construction d'une suite mathematique 37 4.5 Premiers scripts, ou : Comment conserver nos programmes ? 38 4.6 Remarque concernant les caracteres accentues et speciaux : 40 Chapitre 5 : Principaux types de donnees 42 5.1 Les donnees numeriques 42 5.1.1 Les types « integer » et « long » 42 5.1.2 Le type « float » 44 5.2 Les donnees alphanumeriques 46 5.2.1 Le type « string » (chaine de caracteres) 46 5.2.2 Acces aux caracteres individuels d'une chaine 47 5.2.3 Operations elementaires sur les chaines 48 5.3 Les listes (premiere approche) 50 Chapitre 6 : Fonctions predefinies 53 6.1 Interaction avec l'utilisateur : la fonction input() 53 6.2 Importer un module de fonctions 54 6.3 Un peu de detente avec le module turtle 56 6.4 Veracite/faussete d'une expression 57 6.5 Revision 58 6.5.1 Controle du flux - Utilisation d'une liste simple 58 6.5.2 Boucle while - Instructions imbriquees 59 Chapitre 7 : Fonctions originales 62 7.1 Definir une fonction 62 7.1.1 Fonction simple sans parametres 63 7.1.2 Fonction avec parametre 65 7.1.3 Utilisation d'une variable comme argument 66 7.1.4 Fonction avec plusieurs parametres 67 7.2 Variables locales, variables globales 68 7.3 « Vraies » fonctions et procedures 70 7.4 Utilisation des fonctions dans un script 72 7.5 Modules de fonctions 73 7.6 Typage des parametres 78 7.7 Valeurs par defaut pour les parametres 78 7.8 Arguments avec etiquettes 79 Chapitre 8 : Utilisation de fenetres et de graphismes 81 8.1 Interfaces graphiques (GUI) 81 8.2 Premiers pas avec Tkinter 81 8.3 Programmes pilotes par des evenements 85 8.3.1 Exemple graphique : trace de lignes dans un canevas 87 8.3.2 Exemple graphique : deux dessins alternes 90 8.3.3 Exemple graphique : calculatrice minimaliste 92 8.3.4 Exemple graphique : detection et positionnement dun clic de souris 94 366. Gerard Swinnen : Apprendre a programmer avec Python 8.4 Les classes de widgets Tkinter 95 8.5 Utilisation de la methode grid() pour controler la disposition des widgets 96 8.6 Composition d'instructions pour ecrire un code plus compact 100 8.7 Modification des proprietes d'un objet - Animation 102 8.8 Animation automatique - Recurs ivite 105 Chapitre 9 : Les fichiers 108 9.1 Utilite des fichiers 108 9.2 Travailler avec des fichiers 109 9.3 Noms de fichiers - Repertoire courant 110 9.4 Les deux formes d'importation 110 9.5 Ecriture sequentielle dans un fichier 112 9.6 Lecture sequentielle d'un fichier 113 9.7 L'instruction break pour sortir d'une boucle 114 9.8 Fichiers texte 115 9.9 Enregistrement et restitution de variables diverses 117 9.10 Gestion des exceptions. Les instructions try - except - else 118 Chapitre 10 : Approfondir les structures de donnees 121 10.1 Le point sur les chaines de caracteres 121 10.1.1 Concatenation, Repetition 121 10.1.2 Indicage, extraction, longueur 121 10.1.3 Parcours d'une sequence. L'instruction for ... in 123 10.1.4 Appartenance d'un element a une sequence. L'instruction in utilisee seule 124 10.1.5 Les chaines sont des sequences non modifiables 125 10.1.6 Les chaines sont comparables 126 10.1.7 Classem*nt des caracteres 126 10.1.8 Les chaines sont des objets 128 10.1.9 Formatage des chaines de caracteres 130 10.2 Le point sur les listes 132 10.2.1 Definition d'une liste - Acces a ses elements 132 10.2.2 Les listes sont modifiables 133 10.2.3 Les listes sont des objets 133 10.2.4 Techniques de « slicing » avance pour modifier une liste 135 10.2.5 Creation d'une liste de nombres a l'aide de la fonction rangef) 136 10.2.6 Parcours d'une liste a l'aide de for, rangef) et len() 136 10.2.7 Une consequence du typage dynamique 137 10.2.8 Operations sur les listes 137 10.2.9 Test d'appartenance 137 10.2.10 Copie d'une liste 138 10.2.1 1 Nombres aleatoires - Histogrammes 140 10.3 Les tuples 143 10.4 Les dictionnaires 144 10.4.1 Creation d'un dictionnaire 1 44 10.4.2 Operations sur les dictionnaires 145 Gerard Swinnen : Apprendre a programmer avec Python 367 '. 10.4.3 Les dictionnaires sont des objets 145 10.4.4 Parcours dun dictionnaire 146 10.4.5 Les cles ne sont pas necessairement des chaines de caracteres 147 10.4.6 Les dictionnaires ne sont pas des sequences 148 10.4.7 Construction d'un histogramme a l'aide d'un dictionnaire 149 10.4.8 Controle du flux d'execution a l'aide d'un dictionnaire 150 Chapitre 11 : Classes, objets, attributs 152 11.1 Utilite des classes 152 11.2 Definition d'une classe elementaire 153 1 1.3 Attributs (ou variables) d'instance 154 1 1.4 Passage d' objets comme arguments lors de l'appel d'une fonction 155 11.5 Similitude et unicite 155 11.6 Objets composes d'objets 156 1 1.7 Objets comme valeurs de retour d'une fonction 157 1 1.8 Les objets sont modifiables 158 Chapitre 12 : Classes, methodes, heritage 159 12.1 Definition d'une methode 159 12.2 La methode « constructeur » 161 12.3 Espaces de noms des classes et instances 165 12.4 Heritage 166 12.5 Heritage et polymorphisme 167 12.6 Modules contenant des bibliotheques de classes 171 Chapitre 13 : Classes & Interfaces graphiques 174 13.1 « Code des couleurs » : un petit projet bien encapsule 174 13.2 « Petit train » : heritage, echange d'informations entre classes 178 13.3 « OscilloGraphe » : un widget personnalise 181 13.4 « Curseurs » : un widget composite 186 13.4.1 Presentation du widget « Scale » 186 13.4.2 Construction d'un panneau de controle a trois curseurs 187 13.5 Integration de widgets composites dans une application synthese 191 Chapitre 14 : Et pour quelques widgets de plus 198 14.1 Les « boutons radio » 198 14.2 Utilisation des cadres (frames) pour la composition d'une fenetre 200 14.3 Comment deplacer des dessins a l'aide de la souris 202 14.4 Python Mega Widgets 205 14.4.1 « Combo Box » 205 14.4.2 Remarque concernant l'entree de caracteres accentues 206 14.4.3 « Scrolled Text » 207 14.4.4 « Scrolled Canvas » 210 14.4.5 Barres d'outils avec bulles d'aide - expressions lambda 213 14.5 Fenetres avec menus 216 14.5.1 Premiere ebauche du programme : 217 14.5.2 Ajout de la rubrique « Musiciens » 219 368. Gerard Swinnen : Apprendre a programmer avec Python 14.5.3 Ajout de la rubrique « Peintres » : 221 14.5.4 Ajout de la rubrique « Options » : 222 Chapitre 15 : Analyse de programmes concrets 227 15.1 Jeu des bombardes 227 15.1.1 Prototypage d'une classe « Canon » 229 15.1.2 Ajout de methodes au prototype 232 15.1.3 Developpement de l'application 234 15.1.4 Developpements complementaires 239 15.2 Jeu de Ping 243 15.2.1 Principe 243 15.2.2 Programmation 244 Chapitre 16 : Gestion d'une base de donnees 249 16.1 Les bases de donnees 249 16.1.1 SGBDR - Le modele client/serveur 249 16.1.2 Le langage SQL - Gadfly 250 16.2 Mise en oeuvre d'une base de donnees simple avec Gadfly 251 16.2.1 Creation de la base de donnees 251 16.2.2 Connexion a une base de donnees existante 252 16.2.3 Recherches dans une base de donnees 253 16.2.4 La requete select 255 16.3 Ebauche dun logiciel client pour MySQL 256 16.3.1 Decrire la base de donnees dans un dictionnaire d'application 256 16.3.2 Definir une classe d'objets-interfaces 258 16.3.3 Construire un generateur de formulaires 261 16.3.4 Le corps de l'application 262 Chapitre 17 : Applications web 264 17.1 Pages web interactives 264 17.2 L'interface CGI 265 17.2.1 Une interaction CGI rudimentaire 265 17.2.2 Un formulaire HTML pour l'acquisition des donnees 267 17.2.3 Un script CGI pour le traitement des donnees 268 17.3 Un serveur web en pur Python ! 269 17.3.1 Installation de Karrigell 270 17.3.2 Demarrage du serveur : 270 17.3.3 Ebauche de site web 271 17.3.4 Prise en charge des sessions 273 17.3.5 Autres developpements 277 Chapitre 18 : Communications a travers un reseau 278 18.1 Les sockets 278 18.2 Construction dun serveur elementaire 279 18.3 Construction dun client rudimentaire 281 18.4 Gestion de plusieurs taches en parallele a l'aide des threads 282 18.5 Client gerant remission et la reception simultanees 283 Gerard Swinnen : Apprendre a programmer avec Python 369. 18.6 Serveur gerant les connexions de plusieurs clients en parallele 285 18.7 Jeu des bombardes, version reseau 287 18.7.1 Programme serveur : vue d'ensemble 288 18.7.2 Protocole de communication 288 18.7.3 Programme serveur : premiere partie 290 18.7.4 Synchronisation de threads concurrents a l'aide de « verrous » (thread locks) 293 18.7.5 Programme serveur : suite et fin 294 18.7.6 Programme client 297 18.8 Utilisation de threads pour optimiser les animations 300 18.8.1 Temporisation des animations a l'aide de after() 300 18.8.2 Temporisation des animations a l'aide de time.sleep() 301 18.8.3 Exemple concret 302 Chapitre 19 : Annexes 304 19.1 Installation de Python 304 19.2 Sous Windows 304 19.3 Sous Linux 304 19.4 SousMacOS 304 19.5 Installation de SciTE (Scintilla Text Editor) 304 19.5.1 Installation sous Linux : 305 19.5.2 Installation sous Windows : 305 19.5.3 Pour les deux versions : 305 19.6 Installation des Python mega-widgets 305 19.7 Installation de Gadfly (systeme de bases de donnees) 306 19.8 Sous Windows : 306 19.8.1 Sous Linux : 306 19.9 Solutions aux exercices 307 19.10 Annexes extraites de « How to think like a computer scientist » 356 19.10.1 Contributor list 356 19.10.2 Preface 358 19.10.3 GNU Free Documentation License 361 370. Gerard Swinnen : Apprendre a programmer avec Python Veuillez vous identifier, SVP : Bienvenue, %s %s Page suivante References

Adaptation libre de "How to think like a computer scientist" de Allen B. Downey, Jeffrey Elkner & Chris Meyers Grace Hopper, inventeur du compilateur : « Pour moi, la programmation est plus qu'un art applique important. C'est aussi une ambitieuse quite menee dans les trefonds de la connaissance. » A Maximilien, Elise, Lucille, Augustin et Alexane. Colophon Choisie deliberement hors propos, l'illustration de couverture est un dessin realise par l'auteur a la mine de graphite sur papier Canson en 1987, d'apres une photographie ancienne. II represente le yacht de course de 106 tonnes Valdora participant a une regate dans la rade de Cowes en 1923. Construit vingt ans plus tot, et d'abord gree en yawl, Valdora remporta plusieurs trophees avant d'etre regree en ketch en 1912 avec la voilure de 516 m 2 que Ton voit sur le dessin. Ce superbe voilier, tres estime par ses equipages pour son bon comportement a la mer, a navigue pendant pres dun demi-siecle. 2. Gerard Swinnen : Apprendre a programmer avec Python Apprendre a programmer avec Python par Gerard Swinnen professeur et conseiller pedagogique Institut S' Jean Berchmans - S te Marie 59, rue des Wallons - B4000 Liege Ces notes peuvent etre telechargees librement a partir du site : http://www.ulg.ac.be/cifen/inforef/swi Une part de ce texte est inspiree de : How to think like a computer scientist de Allen B. Downey, Jeffrey Elkner & Chris Meyers disponible sur : http://rocky.wellesley.edu/downrey/ost ou : http://www.ibiblio.org/obp Copyright (C) 2000-2005 Gerard Swinnen Les notes qui suivent sont distributes suivant les termes de la Licence de Documentation Libre GNU (GNU Free Documentation License, version 1.1) de la Free Software Foundation. Cela signifie que vous pouvez copier, modifier et redistribuer ces notes tout a fait librement, pour autant que vous respectiez un certain nombre de regies qui sont precisees dans cette licence, dont le texte complet peut etre consulte dans l'annexe intitulee « GNU Free Documentation licence », page 361. Pour l'essentiel, sachez que vous ne pouvez pas vous approprier ce texte pour le redistribuer ensuite (modifie ou non) en definissant vous-meme d'autres droits de copie. Le document que vous redistribuez, modifie ou non, doit obligatoirement inclure integralement le texte de la licence citee ci-dessus, le present avis, l'introduction qui suit, ainsi que la section Preface du texte original americain (voir annexes). L'acces a ces notes doit rester libre pour tout le monde. Vous etes autorise a demander une contribution financiere a ceux a qui vous redistribuez ces notes, mais la somme demandee ne peut concerner que les frais de reproduction. Vous ne pouvez pas redistribuer ces notes en exigeant pour vous-meme des droits d'auteur, ni limiter les droits de reproduction des copies que vous distribuez. La diffusion commerciale de ce texte en librairie, sous la forme classique d'un manuel imprime, est reservee exclusivement a la maison d' edition O'Reilly (Paris). Ces notes sont publiees dans l'espoir qu'elles seront utiles, mais sans aucune garantie. Gerard Swinnen : Apprendre a programmer avec Python 3. Introduction A l'origine, les presentes notes ont ete redigees a l'intention des eleves qui suivent le cours Programmation et langages de l'option Sciences & informatique au 3 e degre de transition de l'enseignement secondaire beige. II s'agit d'un texte experimental qui s'inspire largement de plusieurs autres documents publies sous licence libre sur {'internet. Nous proposons dans ces notes une demarche d'apprentissage non lineaire qui est tres certainement critiquable. Nous sommes conscients qu'elle apparaitra un peu chaotique aux yeux de certains puristes, mais nous l'avons voulue ainsi parce que nous sommes convaincus qu'il existe de nombreuses manieres d'apprendre (pas seulement la programmation, d'ailleurs), et qu'il faut accepter d'emblee ce fait etabli que des individus differents n'assimilent pas les memes concepts dans le meme ordre. Nous avons done cherche avant tout a susciter l'interet et a ouvrir un maximum de portes, en nous efforcant tout de meme de respecter les principes directeurs suivants : • L'apprentissage que nous visons doit etre adapte au niveau de comprehension et aux connaissances generates d'un eleve moyen. Nous nous refusons d'elaborer un cours qui soit reserve a une « elite » de petit* genies. Dans la meme optique, notre ambition reste generaliste : nous voulons mettre en evidence les invariants de la programmation et de l'informatique, sans poursuivre une specialisation quelconque. • Les outils utilises au cours de l'apprentissage doivent etre modernes et performants, mais il faut aussi que l'eleve puisse se les procurer en toute legalite pour son usage personnel. Toute notre demarche d'apprentissage repose en effet sur l'idee que l'eleve devra tres tot mettre en chantier des realisations personnelles qu'il pourra developper et exploiter a sa guise. • L'eleve qui apprend doit pouvoir rapidement realiser de petites applications graphiques. Les etudiants auxquels nous nous adressons sont en effet fort jeunes (en theorie, ils sont a peine arrives a l'age ou Ton commence a pouvoir faire des abstractions). Dans ce cours, nous avons pris le parti d'aborder tres tot la programmation d'une interface graphique, avant meme d'avoir presente l'ensemble des structures de donnees disponibles, parce que nous observons que les jeunes qui arrivent aujourd'hui dans nos classes « baignent » deja dans une culture informatique a base de fenetres et autres objets graphiques interactifs. S'ils choisissent d'apprendre la programmation, ils sont forcement impatients de creer par eux-memes des applications (peut-etre tres simples) ou l'aspect graphique est deja bien present. Nous avons done choisi cette approche un peu inhabituelle afin de permettre a nos eleves de se lancer tres tot dans de petit* projets personnels attrayants, par lesquels ils puissent se sentir valorises. Nous leur imposerons cependant de realiser leurs projets sans faire appel a l'un ou l'autre de ces environnements de programmation sophistiques qui ecrivent automatiquement de nombreuses lignes de code, parce que nous ne voulons pas non plus masquer la complexity sous-jacente. Certains nous reprocheront que notre demarche n'est pas suffisamment centree sur l'algorithmique pure et dure. Nous pensons qu'une telle approche n'est guere adaptee aux jeunes, pour les raisons deja evoquees ci-dessus. Nous pensons egalement qu'elle est moins primordiale que par le passe. II semble en effet que l'apprentissage de la programmation moderne par objets necessite plutot une mise en contact aussi precoce que possible de l'etudiant avec des objets et des bibliotheques de classes preexistants. Ainsi il apprend tres tot a raisonner en termes d' interactions entre objets, plutot qu'en termes de procedures, et cela l'autorise assez vite a tirer profit de concepts avances, tels que l'heritage et le polymorphisme. Nous avons par ailleurs accorde une place assez importante a la manipulation de differents types de structures de donnees, car nous estimons que e'est la reflexion sur les donnees qui doit rester la colonne vertebrale de tout developpement logiciel. 4. Gerard Swinnen : Apprendre a programmer avec Python Choix d'un premier langage de programmation II existe un tres grand nombre de langages de programmation, chacun avec ses avantages et ses inconvenients. L'ideal serait certainement d'en utiliser plusieurs, et nous ne pouvons qu'encourager les professeurs a presenter de temps a autre quelques exemples tires de langages differents. II faut cependant bien admettre que nous devons avant tout viser l'acquisition de bases solides, et que le temps dont nous disposons est limite. Dans cette optique, il nous semble raisonnable de n'utiliser d'abord qu'un seul langage, au mo ins pendant la premiere annee d' etudes. Mais quel langage allons-nous choisir pour commencer ? Lorsque nous avons commence a reflechir a cette question, durant notre preparation d'un curriculum pour la nouvelle option Sciences & Informatique, nous avions personnellement accumule une assez longue experience de la programmation sous Visual Basic (Micro$off) et sous Clarion (Top$peed). Nous avions egalement experiments quelque peu sous Delphi {Borl@nd). II etait done naturel que nous pensions d'abord exploiter l'un ou l'autre de ces langages (avec une nette preference pour Clarion, qui reste malheureusem*nt peu connu). Si nous souhaitons les utiliser comme outils de base pour un apprentissage general de la programmation, ces langages presentent toutefois deux gros inconvenients : • lis sont lies a des environnements de programmation (e'est-a-dire des logiciels) proprietaries. Cela signifie done, non seulement que l'institution scolaire desireuse de les utiliser devrait acheter une licence de ces logiciels pour chaque poste de travail (ce qui risque de se reveler assez couteux), mais surtout que les eleves souhaitant utiliser leurs competences de programmation ailleurs qu'a l'ecole seraient implicitement forces d'en acquerir eux aussi des licences, ce que nous ne pouvons pas accepter. • Ce sont des langages specifiquement lies au seul systeme d'exploitation Windows. lis ne sont pas « portables » sur d'autres systemes {Unix, MacOS, etc.). Cela ne cadre pas avec notre projet pedagogique qui ambitionne d'inculquer une formation generale (et done diversifiee) dans laquelle les invariants de l'informatique seraient autant que possible mis en evidence. Nous avons alors decide d'examiner l'offre alternative, e'est-a-dire celle qui est proposee gratuitement dans la mouvance de l'informatique libre 1 . Ce que nous avons trouve nous a enthousiasmes : non seulement il existe dans le monde de VOpen Source des interpreters et des compilateurs gratuits pour toute une serie de langages, mais le veritable cadeau consiste dans le fait que ces langages sont modernes, performants, portables (e'est-a-dire utilisables sur differents systemes d'exploitation tels que Windows, Linux, MacOS ...), et fort bien documentes. Le langage dominant y est sans conteste C/C++. Ce langage s'impose comme une reference absolue, et tout informaticien serieux doit s'y frotter tot ou tard. II est malheureusem*nt tres rebarbatif et complique, trop proche de la machine. Sa syntaxe est peu lisible et fort contraignante. La mise au point d'un gros logiciel ecrit en C/C++ est longue et penible. (Les memes remarques valent aussi dans une large mesure pour le langage Java). 1 Un logiciel libre {Free Software) est avant tout un logiciel dont le code source est accessible a tous (Open source). Souvent gratuit (ou presque), copiable et modifiable librement au gre de son acquereur, il est generalement le produit de la collaboration benevole de centaines de developpeurs enthousiastes disperses dans le monde entier. Son code source etant "epluche" par de tres nombreux specialistes (etudiants et professeurs universitaires), un logiciel libre se caracterise la plupart du temps par un tres haut niveau de qualite technique. Le plus celebre des logiciels libres est le systeme d'exploitation GNU/Linux, dont la popularity ne cesse de s'accroitre de jour en jour. Gerard Swinnen : Apprendre a programmer avec Python 5. D'autre part, la pratique moderne de ce langage fait abondamment appel a des generateurs d'applications et autres outils d'assistance tres elabores tels C++Builder, Kdevelop, etc. Ces environnements de programmation peuvent certainement se reveler tres efficaces entre les mains de programmeurs experimentes, mais ils proposent d'emblee beaucoup trop d'outils complexes, et ils presupposent de la part de l'utilisateur des connaissances qu'un debutant ne maitrise evidemment pas encore. Ce seront done aux yeux de celui-ci de veritables « usines a gaz » qui risquent de lui masquer les mecanismes de base du langage lui-meme. Nous laisserons done le C/C++ pour plus tard. Pour nos debuts dans l'etude de la programmation, il nous semble preferable d'utiliser un langage de plus haut niveau, moins contraignant, a la syntaxe plus lisible. Veuillez aussi consulter a ce sujet la preface de « How to think like a computer scientist », par Jeffrey Elkner (voir page 358). Apres avoir successivement examine et experiments quelque peu les langages Perl et Tcl/Tk , nous avons finalement decide d'adopter Python, langage tres moderne a la popularite grandissante. Presentation du langage Python, par Stefane Fermigier 2 . Python est un langage portable, dynamique, extensible, gratuit, qui permet (sans l'imposer) une approche modulaire et orientee objet de la programmation. Python est developpe depuis 1989 par Guido van Rossum et de nombreux contributeurs benevoles. Caracteristiques du langage Detaillons un peu les principales caracteristiques de Python, plus precisem*nt, du langage et de ses deux implantations actuelles: • Python est portable, non seulement sur les differentes variantes 6! Unix, mais aussi sur les OS proprietaries: MacOS, BeOS, NeXTStep, MS-DOS et les differentes variantes de Windows. Un nouveau compilateur, baptise JPython, est ecrit en Java et genere du bytecode Java. • Python est gratuit, mais on peut l'utiliser sans restriction dans des projets commerciaux. • Python convient aussi bien a des scripts d'une dizaine de lignes qu'a des projets complexes de plusieurs dizaines de milliers de lignes. • La syntaxe de Python est tres simple et, combinee a des types de donnees evolues (listes, dictionnaires,...), conduit a des programmes a la fois tres compacts et tres lisibles. A fonctionnalites egales, un programme Python (abondamment commente et presente selon les canons standards) est souvent de 3 a 5 fois plus court qu'un programme C ou C++ (ou meme Java) equivalent, ce qui represente en general un temps de developpement de 5 a 10 fois plus court et une facilite de maintenance largement accrue. • Python gere ses ressources (memo ire, descripteurs de fichiers...) sans intervention du programmeur, par un mecanisme de comptage de references (proche, mais different, d'un garbage collector). • II n'y a pas de pointeurs explicites en Python. • Python est (optionnellement) multi-threade. • Python est oriente-objet. II supporte l'heritage multiple et la surcharge des operateurs. Dans son modele objets, et en reprenant la terminologie de C++, toutes les methodes sont virtuelles. • Python integre, comme Java ou les versions recentes de C++, un systeme d'exceptions, qui permettent de simplifier considerablement la gestion des erreurs. 2 Stefane Fermigier est le president de l'AFUL (Association Francophone des Utilisateurs de Linux et des logiciels libres). Ce texte est extrait d'un article paru dans le magazine Programmez! en decembre 1998. II est egalement disponible sur http://www.linux-center.org/articles/9812/python.htmn 6. Gerard Swinnen : Apprendre a programmer avec Python • Python est dynamique (l'interpreteur peut evaluer des chaines de caracteres representant des expressions ou des instructions Python), orthogonal (un petit nombre de concepts suffit a engendrer des constructions tres riches), reflectif (il supporte la metaprogrammation, par exemple la capacite pour un objet de se rajouter ou de s'enlever des attributs ou des methodes, ou meme de changer de classe en cours d' execution) et introspectif (un grand nombre d'outils de developpement, comme le debugger ou le profiler, sont implantes en Python lui-meme). • Comme Scheme ou SmallTalk, Python est dynamiquement type. Tout objet manipulable par le programmeur possede un type bien defini a l'execution, qui n'a pas besoin d'etre declare a l'avance. • Python possede actuellement deux implementations. L'une, interpretee, dans laquelle les programmes Python sont compiles en instructions portables, puis executes par une machine virtuelle (comme pour Java, avec une difference importante: Java etant statiquement type, il est beaucoup plus facile d'accelerer l'execution d'un programme Java que d'un programme Python). L'autre genere directement du bytecode Java. • Python est extensible : comme Tel ou Guile, on peut facilement l'interfacer avec des bibliotheques C existantes. On peut aussi s'en servir comme d'un langage d' extension pour des systemes logiciels complexes. • La bibliotheque standard de Python, et les paquetages contribues, donnent acces a une grande variete de services : chaines de caracteres et expressions regulieres, services UNIX standards (fichiers, pipes, signaux, sockets, threads...), protocoles Internet (Web, News, FTP, CGI, HTML...), persistance et bases de donnees, interfaces graphiques. • Python est un langage qui continue a evoluer, soutenu par une communaute d'utilisateurs enthousiastes et responsables, dont la plupart sont des supporters du logiciel libre. Parallelement a l'interpreteur principal, ecrit en C et maintenu par le createur du langage, un deuxieme interpreteur, ecrit en Java, est en cours de developpement. • Enfin, Python est un langage de choix pour traiter le XML. Plusieurs versions differentes ? Comme cela a ete evoque dans le texte ci-dessus, Python continue a evoluer sans cesse. Mais cette evolution ne vise qu'a ameliorer ou perfectionner le produit. De ce fait, vous ne devez pas craindre de devoir tot ou tard modifier tous vos programmes afin de les adapter a une nouvelle version qui serait devenue incompatible avec les precedentes. Les exemples de ce livre ont ete realises les uns apres les autres sur une periode de temps relativement longue : certains ont ete developpes sous Python 1.5.2, puis d'autres sous Python 1.6, Python 2.0, Python 2.1, Python 2.2 et enfin Python 2.3. Tous continuent cependant a fonctionner sans probleme sous cette derniere version, et ils continueront certainement a fonctionner sans modification majeure sur les versions futures. Installez done sur votre systeme la derniere version disponible, et amusez-vous bien ! Gerard Swinnen : Apprendre a programmer avec Python 7. Distribution de Python - Bibliographie Les differentes versions de Python (pour Windows, Unix, etc.), son tutoriel original, son manuel de reference, la documentation des bibliotheques de fonctions, etc. sont disponibles en telechargement gratuit depuis l'internet, a partir du site web officiel : http://www.python.org II existe egalement de tres bons ouvrages imprimes concernant Python. Si la plupart d'entre eux n' existent encore qu'en version anglaise, on peut cependant deja se procurer en traduction francaise les manuels ci-apres : • Python en concentre, par Alex Martelli, traduction d'Eric Jacoboni, Editions O'Reilly, Paris, 2004, 645 p., ISBN 2-84177-290-X. C'est le premier ouvrage de reference veritable edite en langue francaise. Une mine de renseignements essentielle. • Introduction a Python, par Mark Lutz & David Ascher, traduction de Sebastien Tanguy, Olivier Berger & Jerome Kalifa, Editions O'Reilly, Paris, 2000, 385 p., ISBN 2-84177-089-3. Cet ouvrage est une excellente initiation a Python pour ceux qui pratiquent deja d'autres langages. • L 'intro Python, par Ivan Van Laningham, traduction de Denis Frere, Karine Cottereaux et Noel Renard, Editions CampusPress, Paris, 2000, 484 p., ISBN 2-7440-0946-6 • Python precis & concis (il s'agit d'un petit aide-memoire bien pratique), par Mark Lutz, traduction de James Guerin, Editions O'Reilly, Paris, 2000, 80 p., ISBN 2-84177-1 1 1-3 En langue anglaise, le choix est evidemment beaucoup plus vaste. Nous apprecions personnellement beaucoup Python : How to program, par Deitel, Liperi & Wiedermann, Prentice Hall, Upper Saddle River - NJ 07458, 2002, 1300 p., ISBN 0-13-092361-3 , tres complet, tres clair, agreable a lire et qui utilise une methodologie eprouvee, Core Python programming, par Wesley J. Chun, Prentice Hall, 2001, 770 p., ISBN 0-13-026036-3 dont les explications sont limpides, et Learn to program using Python, par Alan Gauld, Addison- Wesley, Reading, MA, 2001, 270 p., ISBN 0-201-70938-4 , qui est un tres bon ouvrage pour debutants. Pour aller plus loin, notamment dans l'utilisation de la bibliotheque graphique Tkinter, on pourra utilement consulter Python and Tkinter Programming, par John E. Grayson, Manning publications co., Greenwich (USA), 2000, 658 p., ISBN 1-884777-81-3 , et surtout l'incontournable Programming Python (second edition) de Mark Lutz, Editions O'Reilly, Paris, 2001, 1255 p., ISBN 0-596-00085-5, qui est une extraordinaire mine de renseignements sur de multiples aspects de la programmation moderne (sur tous systemes). Si vous savez deja bien programmer, et que vous souhaiter progresser encore en utilisant les concepts les plus avances de l'algorithmique Pythonienne, procurez vous Python cookbook, par Alex Martelli et David Ascher, Editions O'Reilly, Paris, 2002, 575 p., ISBN 0-596-00167-3 , dont les recettes sont savoureuses. Si vous souhaitez plus particulierement exploiter aux mieux les ressources liees au systeme d'exploitation Windows, Python Programming on Win32, par Mark Hammond & Andy Robinson, Editions O'Reilly, Paris, 2000, 654 p., ISBN 1-56592-621-8 est un ouvrage precieux. Reference egalement fort utile, la Python Standard Library de Fredrik Lundh, Editions O'Reilly, Paris, 2001, 282 p., ISBN 0-596-00096-0 8. Gerard Swinnen : Apprendre a programmer avec Python Pour le professeur qui souhaite utiliser cet ouvrage comme support de cours Nous souhaitons avec ces notes ouvrir un maximum de portes. A notre niveau d'etudes, il nous parait important de montrer que la programmation d'un ordinateur est un vaste univers de concepts et de methodes, dans lequel chacun peut trouver son domaine de predilection. Nous ne pensons pas que tous nos etudiants doivent apprendre exactement les memes choses. Nous voudrions plutot qu'ils arrivent a developper chacun des competences quelque peu differentes, qui leur permettent de se valoriser a leurs propres yeux ainsi qu'a ceux de leurs condisciples, et egalement d'apporter leur contribution specifique lorsqu'on leur proposera de collaborer a des travaux d'envergure. De toute maniere, notre preoccupation primordiale doit etre d'arriver a susciter l'interet, ce qui est loin d'etre acquis d'avance pour un sujet aussi ardu que la programmation d'un ordinateur. Nous ne voulons pas feindre de croire que nos jeunes eleves vont se passionner d'emblee pour la construction de beaux algorithmes. Nous sommes plutot convaincus qu'un certain interet ne pourra durablement s'installer qu'a partir du moment ou ils commenceront a realiser qu'ils sont devenus capables de developper un projet personnel original, dans une certaine autonomie. Ce sont ces considerations qui nous ont amenes a developper une structure de cours que certains trouveront peut-etre un peu chaotique. Le principal fil conducteur en est l'excellent « How to think like a computer scientist », mais nous l'avons un peu eclate pour y inserer toute une serie d'elements concernant la gestion des entrees/sorties, et en particulier l'interface graphique Tkinter. Nous souhaiterions en effet que les eleves puissent deja realiser l'une ou l'autre petite application graphique des la fin de leur premiere annee d'etudes. Tres concretement, cela signifie que nous pensons pouvoir explorer les huit premiers chapitres de ces notes durant la premiere annee de cours. Cela suppose que Ton aborde d'abord toute une serie de concepts importants (types de donnees, variables, instructions de controle du flux, fonctions et boucles) d'une maniere assez rapide, sans trop se preoccuper de ce que chaque concept soit parfaitement compris avant de passer au suivant, en essayant plutot d'inculquer le gout de la recherche personnelle et de l'experimentation. II sera souvent plus efficace de reexpliquer les notions et les mecanismes essentiels en situation, dans des contextes varies. Dans notre esprit, c'est surtout en seconde annee que Ton cherchera a structurer les connaissances acquises, en les approfondissant. Les algorithmes seront davantage decortiques et commentes. Les projets, cahiers des charges et methodes d' analyse seront discutes en concertation. On exigera la tenue reguliere d'un cahier de notes et la redaction de rapports techniques pour certains travaux. L'objectif ultime sera pour chaque eleve de realiser un projet de programmation original d'une certaine importance. On s'efforcera done de boucler l'etude theorique des concepts essentiels suffisamment tot dans l'annee scolaire, afin que chacun puisse disposer du temps necessaire. II faut bien comprendre que les nombreuses informations fournies dans ces notes concernant une serie de domaines particuliers (gestion des interfaces graphiques, des communications, des bases de donnees, etc.) sont matieres facultatives. Ce sont seulement une serie de suggestions et de reperes que nous avons inclus pour aider les etudiants a choisir et a commencer leur projet personnel de fin d'etudes. Nous ne cherchons en aucune maniere a former des specialistes d'un certain langage ou d'un certain domaine technique : nous voulons simplement donner un petit apercu des immenses possibilites qui s'offrent a celui qui se donne la peine d'acquerir une competence de programmeur. Gerard Swinnen : Apprendre a programmer avec Python 9. Exemples du livre Le code source des exemples de ce livre peut etre telecharge a partir du site de l'auteur : http://www.ulg.ac.be/cifen/inforef/swi/python.htm Remerciements Ces notes sont pour une partie le resultat d'un travail personnel, mais pour une autre - bien plus importante - la compilation d'informations et d'idees mises a la disposition de tous par des professeurs et des chercheurs benevoles. Comme deja signale plus haut, l'une de mes sources les plus importantes a ete le cours de A.Downey, J.Elkner & C.Meyers : How to think like a computer scientist. Merci encore a ces professeurs enthousiastes. J'avoue aussi m'etre largement inspire du tutoriel original ecrit par Guido van Rossum lui-meme (l'auteur principal de Python), ainsi que d'exemples et de documents divers emanant de la (tres active) communaute des utilisateurs de Python. II ne m'est malheureusem*nt pas possible de preciser davantage les references de tous ces textes, mais je voudrais que leurs auteurs soient assures de toute ma reconnaissance. Merci egalement a tous ceux qui oeuvrent au developpement de Python, de ses accessoires et de sa documentation, a commencer par Guido van Rossum, bien sur, mais sans oublier non plus tous les autres (II sont (mal)heureusem*nt trop nombreux pour que je puisse les citer tous ici). Merci encore a mes collegues Freddy Klich, Christine Ghiot et David Carrera, professeurs a l'lnstitut St. Jean-Berchmans de Liege, qui ont accepte de se lancer dans l'aventure de ce nouveau cours avec leurs eleves, et ont egalement suggere de nombreuses ameliorations. Un merci tout particulier a Christophe Morvan, professeur a 1'IUT de Marne-la-Vallee, pour ses avis precieux et ses encouragements. Grand merci aussi a Florence Leroy, mon editrice chez O'Reilly, qui a corrige mes incoherences et mes belgicismes avec une competence sans faille. Merci enfin a mon epouse Suzel, pour sa patience et sa comprehension. 10. Gerard Swinnen : Apprendre a programmer avec Python Chapitre 1 : Penser comme un programmeur 3 Nous allons introduire dans ce chapitre quelques concepts qu'il vous faut connaitre avant de vous lancer dans l'apprentissage de la programmation. Nous avons volontairement limite nos explications afin de ne pas vous encombrer l'esprit. La programmation n'est pas difficile : il suffit d'un peu de methode et de perseverance. 1. 1 La demarche du programmeur Le but de ce cours est de vous apprendre a penser et d reflechir comme un analyste- programmeur. Ce mode de pensee combine des demarches intellectuelles complexes, similaires a celles qu'accomplissent les mathematiciens, les ingenieurs et les scientifiques. Comme le mathematicien, l'analyste-programmeur utilise des langages formels pour decrire des raisonnements (ou algorithmes). Comme l'ingenieur, il concoit des dispositifs, il assemble des composants pour realiser des mecanismes et il evalue leurs performances. Comme le scientifique, il observe le comportement de systemes complexes, il ebauche des hypotheses explicatives, il teste des predictions. L 'activite essentielle d'un analyste-programmeur est la resolution de problemes. II s'agit la d'une competence de haut niveau, qui implique des capacites et des connaissances diverses : etre capable de (re)formuler un probleme de plusieurs manieres differentes, etre capable d'imaginer des solutions innovantes et efficaces, etre capable d'exprimer ces solutions de maniere claire et complete. La programmation d'un ordinateur consiste en effet a « expliquer » en detail a une machine ce qu'elle doit faire, en sachant d'emblee qu'elle ne peut pas veritablement « comprendre » un langage humain, mais seulement effectuer un traitement automatique sur des sequences de caracteres. Un programme n'est rien d'autre qu'une suite destructions, encodees en respectant de maniere tres stricte un ensemble de conventions fixees a l'avance que Ton appelle un langage informatique. La machine est ainsi pourvue d'un mecanisme qui decode ces instructions en associant a chaque « mot » du langage une action precise. Vous allez done apprendre a programmer, activite deja interessante en elle-meme parce qu'elle contribue a developper votre intelligence. Mais vous serez aussi amene a utiliser la programmation pour realiser des projets concrets, ce qui vous procurera certainement de tres grandes satisfactions. 1.2 Langage machine, langage de programmation A strictement parler, un ordinateur n'est rien d'autre qu'une machine effectuant des operations simples sur des sequences de signaux electriques, lesquels sont conditionnes de maniere a ne pouvoir prendre que deux etats seulement (par exemple un potentiel electrique maximum ou minimum). Ces sequences de signaux obeissent a une logique du type « tout ou rien » et peuvent done etre consideres conventionnellement comme des suites de nombres ne prenant jamais que les deux valeurs 0 et 1 . Un systeme numerique ainsi limite a deux chiffres est appele systeme binaire. Sachez des a present que dans son fonctionnement interne, un ordinateur est totalement incapable de traiter autre chose que des nombres binaires. Toute information d'un autre type doit etre convertie, ou codee, en format binaire. Cela est vrai non seulement pour les donnees que Ton 3 Une part importante de ce chapitre est traduite d'un chapitre similaire de « How to think like a computer scientist » de Downey, Elkner & Meyers. Gerard Swinnen : Apprendre a programmer avec Python 11. souhaite traiter (les textes, les images, les sons, les nombres, etc.), mais aussi pour les programmes, c'est-a-dire les sequences d'instructions que Ton va fournir a la machine pour lui dire ce qu'elle doit faire avec ces donnees. Le seul « langage » que l'ordinateur puisse veritablement « comprendre » est done tres eloigne de ce que nous utilisons nous-memes. C'est une longue suite de 1 et de 0 (les "bits") souvent traites par groupes de 8 (les « octets »), 16, 32, ou meme 64. Ce « langage machine » est evidemment presqu'incomprehensible pour nous. Pour « parler » a un ordinateur, il nous faudra utiliser des systemes de traduction automatiques, capables de convertir en nombres binaires des suites de caracteres formant des mots-cles (anglais en general) qui seront plus significatifs pour nous. Ces systemes de traduction automatique seront etablis sur la base de toute une serie de conventions, dont il existera evidemment de nombreuses variantes. Le systeme de traduction proprement dit s'appellera interpreteur ou bien compilateur, suivant la methode utilisee pour effectuer la traduction (voir ci-apres). On appellera langage de programmation un ensemble de mots-cles (choisis arbitrairement) associe a un ensemble de regies tres precises indiquant comment on peut assembler ces mots pour former des « phrases » que l'interpreteur ou le compilateur puisse traduire en langage machine (binaire). Suivant son niveau d'abstraction, on pourra dire d'un langage qu'il est « de bas niveau » (ex : Assembler) ou « de haut niveau » (ex : Pascal, Perl, Smalltalk, Clarion, Java...). Un langage de bas niveau est constitute d'instructions tres elementaires, tres « proches de la machine ». Un langage de haut niveau comporte des instructions plus abstraites ou, plus « puissantes ». Cela signifie que chacune de ces instructions pourra etre traduite par l'interpreteur ou le compilateur en un grand nombre d'instructions machine elementaires. Le langage que vous allez apprendre en premier est Python. II s'agit d'un langage de haut niveau, dont la traduction en codes binaires est complexe et prend done toujours un certain temps. Cela pourrait paraitre un inconvenient. En fait, les avantages que presentent les langages de haut niveau sont enormes : il est beaucoup plus facile d'ecrire un programme dans un langage de haut niveau ; l'ecriture du programme prend done beaucoup moins de temps ; la probability d'y faire des fautes est nettement plus faible ; la maintenance (c'est-a-dire l'apport de modifications ulterieures) et la recherche des erreurs (les « bugs ») sont grandement facilities. De plus, un programme ecrit dans un langage de haut niveau sera souvent portable, c'est-a-dire que Ton pourra le faire fonctionner sans guere de modifications sur des machines ou des systemes d'exploitation differents. Un programme ecrit dans un langage de bas niveau ne peut jamais fonctionner que sur un seul type de machine : pour qu'une autre l'accepte, il faut le reecrire entierement. 12. Gerard Swinnen : Apprendre a programmer avec Python 1.3 Compilation et interpretation Le programme tel que nous l'ecrivons a l'aide d'un logiciel editeur (une sorte de traitement de texte specialise) sera appele desormais programme source (ou code source). Comme deja signale plus haut, il existe deux techniques principales pour effectuer la traduction d'un tel programme source en code binaire executable par la machine : Interpretation et la compilation. • Dans la technique appelee interpretation, le logiciel interpreteur doit etre utilise chaque fois que Ton veut faire fonctionner le programme. Dans cette technique en effet, chaque ligne du programme source analyse est traduite au fur et a mesure en quelques instructions du langage machine, qui sont ensuite directement executees. Aucun programme objet n'est genere. Code source Resultat L' interpreteur lit le code, source ... ... etle resultat apparait sur Vecran. La compilation consiste a traduire la totalite du texte source en une fois. Le logiciel compilateur lit toutes les lignes du programme source et produit une nouvelle suite de codes que Ton appelle programme objet (ou code objet). Celui-ci peut desormais etre execute independamment du compilateur et etre conserve tel quel dans un fichier (« fichier executable »). Code objet Le compilateur lit le code source ... ... et produit un code objet (binaire). On execute le code objet . Resultat ... et le resultat apparait a Vecran. Chacune de ces deux techniques a ses avantages et ses inconvenients : L'interpretation est ideale lorsque Ton est en phase d'apprentissage du langage, ou en cours d'experimentation sur un projet. Avec cette technique, on peut en effet tester immediatement toute modification apportee au programme source, sans passer par une phase de compilation qui demande toujours du temps. Par contre, lorsqu'un projet comporte des fonctionnalites complexes qui doivent s'executer rapidement, la compilation est preferable : il est clair en effet qu'un programme compile fonctionnera toujours nettement plus vite que son hom*ologue interprete, puisque dans cette technique l'ordinateur n'a plus a (re)traduire chaque instruction en code binaire avant qu'elle puisse etre executee. Gerard Swinnen : Apprendre a programmer avec Python 13. Certains langages modernes tentent de combiner les deux techniques afin de retirer le meilleur de chacune. C'est le cas de Python et aussi de Java. Lorsque vous lui fournissez un programme source, Python commence par le compiler pour produire un code intermediaire, similaire a un langage machine, que Ton appelle bytecode, lequel sera ensuite transmis a un interpreteur pour 1' execution finale. Du point de vue de l'ordinateur, le bytecode est tres facile a interpreter en langage machine. Cette interpretation sera done beaucoup plus rapide que celle d'un code source. Code i I rCompilateurJ I ^> ByteCode Resultat Le compilateur Python lit le code source ... ... et produit un pseudo- code intermediaire. U interpreteur Python ... et le resultat lit le pseudo-code ... apparait a I'ecran. Les avantages de cette methode sont appreciables : • Le fait de disposer en permanence d'un interpreteur permet de tester immediatement n'importe quel petit morceau de programme. On pourra done verifier le bon fonctionnement de chaque composant d'une application au fur et a mesure de sa construction. • L'interpretation du bytecode compile n'est pas aussi rapide que celle d'un veritable code binaire, mais elle est tres satisfaisante pour de tres nombreux programmes, y compris graphiques. • Le bytecode est portable. Pour qu'un programme Python ou Java puisse s'executer sur differentes machines, il suffit de disposer pour chacune d'elles d'un interpreteur adapte. Tout ceci peut vous paraitre un peu complique, mais la bonne nouvelle est que tout ceci est pris en charge automatiquement par l'environnement de developpement de Python. II vous suffira d'entrer vos commandes au clavier, de frapper , et Python se chargera de les compiler et de les interpreter pour vous. 14. Gerard Swinnen : Apprendre a programmer avec Python 1.4 Mise au point d'un programme - Recherche des erreurs (« debug ») La programmation est une demarche tres complexe, et comme c'est le cas dans toute activite humaine, on y commet de nombreuses erreurs. Pour des raisons anecdotiques, les erreurs de programmation s'appellent des « bugs » (ou « bogues », en France) 4 , et l'ensemble des techniques que Ton met en ceuvre pour les detecter et les corriger s'appelle « debug » (ou « deboguage »). En fait, il peut exister dans un programme trois types d'erreurs assez differentes, et il convient que vous appreniez a bien les distinguer : 1.4.1 Erreurs de syntaxe Python ne peut executer un programme que si sa syntaxe est parfaitement correcte. Dans le cas contraire, le processus s'arrete et vous obtenez un message d'erreur. Le terme syntaxe se refere aux regies que les auteurs du langage ont etablies pour la structure du programme. Tout langage comporte sa syntaxe. Dans la langue francaise, par exemple, une phrase doit toujours commencer par une majuscule et se terminer par un point, ainsi cette phrase comporte deux erreurs de syntaxe Dans les textes ordinaires, la presence de quelques petites fautes de syntaxe par-ci par-la n'a generalement pas d'importance. II peut meme arriver (en poesie, par exemple), que des fautes de syntaxe soient commises volontairement. Cela n'empeche pas que Ton puisse comprendre le texte. Dans un programme d'ordinateur, par contre, la moindre erreur de syntaxe produit invariablement un arret de fonctionnement (un « plantage ») ainsi que l'affichage d'un message d'erreur. Au cours des premieres semaines de votre carriere de programmeur, vous passerez certainement pas mal de temps a rechercher vos erreurs de syntaxe. Avec de l'experience, vous en commettrez beaucoup moins. Gardez a l'esprit que les mots et les symboles utilises n'ont aucune signification en eux-memes : ce ne sont que des suites de codes destines a etre convertis automatiquement en nombres binaires. Par consequent, il vous faudra etre tres attentifs a respecter scrupuleusem*nt la syntaxe du langage. II est heureux que vous fassiez vos debuts en programmation avec un langage interprets tel que Python. La recherche des erreurs y est facile et rapide. Avec les langages compiles (tel C++), il vous faudrait recompiler l'integralite du programme apres chaque modification, aussi minime soit- elle. 1.4.2 Erreurs semantiques Le second type d'erreur est l'erreur semantique ou erreur de logique. S'il existe une erreur de ce type dans un de vos programmes, celui-ci s'execute parfaitement, en ce sens que vous n'obtenez aucun message d'erreur, mais le resultat n'est pas celui que vous attendiez : vous obtenez autre chose. En realite, le programme fait exactement ce que vous lui avez dit de faire. Le probleme est que ce que vous lui avez dit de faire ne correspond pas a ce que vous vouliez qu'il fasse. La sequence d'instructions de votre programme ne correspond pas a l'objectif poursuivi. La semantique (la logique) est incorrecte. Rechercher des fautes de logique peut etre une tache ardue. II faut analyser ce qui sort de la machine et tacher de se representer une par une les operations qu'elle a effectuees, a la suite de 4 "bug" est a l'origine un terme anglais servant a designer de petit* insectes genants, tels les punaises. Les premiers ordinateurs fonctionnaient a l'aide de "lampes" radios qui necessitaient des tensions electriques assez elevees. II est arrive a plusieurs reprises que des petit* insectes s'introduisent dans cette circuiterie complexe et se fassent electrocuter, leurs cadavres calcines provoquant alors des court-circuits et done des pannes incomprehensibles. Le mot francais "bogue" a ete choisi par hom*onymie approximative. II designe la coque epineuse de la chataigne. Gerard Swinnen : Apprendre a programmer avec Python 15. chaque instruction. 1.4.3 Erreurs a I'execution Le troisieme type d'erreur est l'erreur en cours d'execution (Run-time error), qui apparait seulement lorsque votre programme fonctionne deja, mais que des circonstances particulieres se presentent (par exemple, votre programme essaie de lire un fichier qui n'existe plus). Ces erreurs sont egalement appelees des exceptions, parce qu'elles indiquent generalement que quelque chose d'exceptionnel s'est produit (et qui n'avait pas ete prevu). Vous rencontrerez davantage ce type d'erreur lorsque vous programmerez des projets de plus en plus volumineux. 1.5 Recherche des erreurs et experimentation L'une des competences les plus importantes a acquerir au cours de votre apprentissage est celle qui consiste a « deboguer » efficacement un programme. II s'agit d'une activite intellectuelle parfois enervante mais toujours tres riche, dans laquelle il faut faire montre de beaucoup de perspicacite. Ce travail ressemble par bien des aspects a une enquete policiere. Vous examinez un ensemble de faits, et vous devez emettre des hypotheses explicatives pour reconstituer les processus et les evenements qui ont logiquement entraine les resultats que vous constatez. Cette activite s'apparente aussi au travail experimental en sciences. Vous vous faites une premiere idee de ce qui ne va pas, vous modifiez votre programme et vous essayez a nouveau. Vous avez emis une hypothese, qui vous permet de predire ce que devra donner la modification. Si la prediction se verifie, alors vous avez progresse d'un pas sur la voie d'un programme qui fonctionne. Si la prediction se revele fausse, alors il vous faut emettre une nouvelle hypothese. Comme l'a bien dit Sherlock Holmes: « Lorsque vous avez elimine Vimpossible, ce qui reste, meme si c'est improbable, doit etre la verite » (A. Conan Doyle, Le signe des quatre). Pour certaines personnes, « programmer » et « deboguer » signifient exactement la meme chose. Ce qu'elles veulent dire par la est que l'activite de programmation consiste en fait a modifier, a corriger sans cesse un meme programme, jusqu'a ce qu'il se comporte finalement comme vous le vouliez. L'idee est que la construction d'un programme commence toujours par une ebauche qui fait deja quelque chose (et qui est done deja deboguee), a laquelle on ajoute couche par couche de petites modifications, en corrigeant au fur et a mesure les erreurs, afin d'avoir de toute facon a chaque etape du processus un programme qui fonctionne. Par exemple, vous savez que Linux est un systeme d'exploitation (et done un gros logiciel) qui comporte des milliers de lignes de code. Au depart, cependant, cela a commence par un petit programme simple que Linus Torvalds avait developpe pour tester les particularites du processeur Intel 80386. Suivant Larry GreenField (« The Linux user's guide », beta version 1) : «L'un des premiers projets de Linus etait un programme destine a convertir une chaine de caracteres AAAA en BBBB. C'est cela qui plus tard finit par devenir Linux I ». Ce qui precede ne signifie pas que nous voulions vous pousser a programmer par approximations successives, a partir d'une vague idee. Lorsque vous demarrerez un projet de programmation d'une certaine importance, il faudra au contraire vous efforcer d'etablir le mieux possible un cahier des charges detaille, lequel s'appuiera sur un plan solidement construit pour l'application envisagee. Diverses methodes existent pour effectuer cette tache ^analyse, mais leur etude sort du cadre de ces notes. Veuillez consulter votre professeur pour de plus amples informations et references. 16. Gerard Swinnen : Apprendre a programmer avec Python 1.6 Langages naturels et langages formels Les langages naturels sont ceux que les etres humains utilisent pour communiquer. Ces langages n'ont pas ete mis au point deliberement (encore que certaines instances tachent d'y mettre un peu d'ordre) : ils evoluent naturellement. Les langages formels sont des langages developpes en vue d' applications specifiques. Ainsi par exemple, le systeme de notation utilise par les mathematiciens est un langage formel particulierement efficace pour representer les relations entre nombres et grandeurs diverses. Les chimistes utilisent un langage formel pour representer la structure des molecules, etc. Les langages de programmation sont des langages formels qui ont ete developpes pour decrire des algorithmes et des structures de donnees. Comme on l'a deja signale plus haut, les langages formels sont dotes d'une syntaxe qui obeit a des regies tres strictes. Par exemple, 3+3=6 est une representation mathematique correcte, alors que $3=+ 6 ne Test pas. De meme, la formule chimique H 2 0 est correcte, mais non Zq 3 G2 Les regies de syntaxe s'appliquent non seulement aux symboles du langage (par exemple, le symbole chimique Zq est illegal parce qu'il ne correspond a aucun element), mais aussi a la maniere de les combiner. Ainsi l'equation mathematique 6+=+/5- ne contient que des symboles parfaitement autorises, mais leur arrangement incorrect ne signifie rien du tout. Lorsque vous lisez une phrase quelconque, vous devez arriver a vous representer la structure logique de la phrase (meme si vous faites cela inconsciemment la plupart du temps). Par exemple, lorsque vous lisez la phrase « la piece est tombee », vous comprenez que « la piece » en est le sujet et « est tombee » le verbe. L'analyse vous permet de comprendre la signification, la logique de la phrase (sa semantique). D'une maniere analogue, l'interpreteur Python devra analyser la structure de votre programme source pour en extraire la signification. Les langages naturels et formels ont done beaucoup de caracteristiques communes (des symboles, une syntaxe, une semantique), mais ils presentent aussi des differences tres importantes : Ambigui'te. Les langages naturels sont pleins d'ambigui'tes, que nous pouvons lever dans la plupart des cas en nous aidant du contexte. Par exemple, nous attribuons tout naturellement une signification differente au mot vaisseau, suivant que nous le trouvons dans une phrase qui traite de circulation sanguine ou de navigation a voiles. Dans un langage formel, il ne peut pas y avoir d'ambigui'te. Chaque instruction possede une seule signification, independante du contexte. Redondance. Pour compenser toutes ces ambigui'tes et aussi de nombreuses erreurs ou pertes dans la transmission de l'information, les langages naturels emploient beaucoup la redondance (dans nos phrases, nous repetons plusieurs fois la meme chose sous des formes differentes, pour etre surs de bien nous faire comprendre). Les langages formels sont beaucoup plus concis. Litteralite. Les langages naturels sont truffes d'images et de metaphores. Si je dis « la piece est tombee ! » dans un certain contexte, il se peut qu'il ne s'agisse en fait ni d'une veritable piece, ni de la chute de quoi que ce soit. Dans un langage formel, par contre, les expressions doivent etre prises pour ce qu'elles sont, « au pied de la lettre ». Gerard Swinnen : Apprendre a programmer avec Python 17. Habitues comme nous le sommes a utiliser des langages naturels, nous avons souvent bien du mal a nous adapter aux regies rigoureuses des langages formels. C'est l'une des difficultes que vous devrez surmonter pour arriver a penser comme un analyste-programmeur efficace. Pour bien nous faire comprendre, comparons encore differents types de textes : Un texte poetique : Les mots y sont utilises autant pour leur musicalite que pour leur signification, et l'effet recherche est surtout emotionnel. Les metaphores et les ambigui'tes y regnent en maitres. Un texte en prose : La signification litterale des mots y est plus importante, et les phrases sont structurees de maniere a lever les ambigui'tes, mais sans y parvenir toujours completement. Les redondances sont souvent necessaires. Un programme d'ordinateur : La signification du texte est unique et litterale. Elle peut etre comprise entierement par la seule analyse des symboles et de la structure. On peut done automatiser cette analyse. Pour conclure, voici quelques suggestions concernant la maniere de lire un programme d'ordinateur (ou tout autre texte ecrit en langage formel). Premierement, gardez a l'esprit que les langages formels sont beaucoup plus denses que les langages naturels, ce qui signifie qu'il faut davantage de temps pour les lire. De plus, la structure y est tres importante. Aussi, ce n'est generalement pas une bonne idee que d'essayer de lire un programme d'une traite, du debut a la fin. Au lieu de cela, entrainez-vous a analyser le programme dans votre tete, en identifiant les symboles et en interpretant la structure. Finalement, souvenez-vous que tous les details ont de l'importance. II faudra en particulier faire tres attention a la casse (e'est-a-dire l'emploi des majuscules et des minuscules) et a la ponctuation. Toute erreur a ce niveau (meme minime en apparence, tel l'oubli d'une virgule, par exemple) peut modifier considerablement la signification du code, et done le deroulement du programme. 18. Gerard Swinnen : Apprendre a programmer avec Python Chapitre 2 : Premiers pas II est temps de se mettre au travail. Plus exactement, nous allons demander a l'ordinateur de travailler a notre place, en lui donnant, par exemple, l'ordre d'effectuer une addition et d'afficher le resultat. Pour cela, nous allons devoir lui transmettre des « instructions », et egalement lui indiquer les « donnees » auxquelles nous voulons appliquer ces instructions. 2. 1 Calculer avec Python Python presente la particularite de pouvoir etre utilise de plusieurs manieres differentes. Vous allez d'abord l'utiliser en mode interactif, c'est-a-dire d'une maniere telle que vous pourrez dialoguer avec lui directement depuis le clavier. Cela vous permettra de decouvrir tres vite un grand nombre de fonctionnalites du langage. Dans un second temps, vous apprendrez comment creer vos premiers programmes (scripts) et les sauvegarder sur disque. L'interpreteur peut etre lance directement depuis la ligne de commande (dans un « shell » Linux, ou bien dans une fenetre DOS sous Windows) : il suffit d'y taper la commande "python" (en supposant que le logiciel lui-meme ait ete correctement installe). Si vous utilisez une interface graphique telle que Windows, Gnome, WindowMaker ou KDE, vous prefererez vraisemblablement travailler dans une « fenetre de terminal », ou encore dans un environnement de travail specialise tel que IDLE. Voici par exemple ce qui apparait dans une fenetre de terminal KDE (sous Linux) 5 : 5? gust@gromit:~ - Terminal - Konsole Session Edition Affichage Signets Configuration Aide gust@g romi t : ~> python Python 2.2.2 (#1. Mar 17 2003, 15:17:58) [GCC 3.3 20030226 (prerelease) (SuSE Linux)] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> | ij*J Terminal I Nouveau 5 Sous Windows, vous aurez surtout le choix entre l'environnement IDLE developpe par Guido Van Rossum, auquel nous donnons nous-meme la preference, et PythonWin, une interface de developpement developpee par Mark Hammond. D'autres environnements de travail plus sophistiques existent aussi, tels l'excellent Boa Constructor par exemple (qui fonctionne de facon tres similaire a Delphi), mais nous estimons qu'ils ne conviennent guere aux debutants. Pour tout renseignement complementaire, veuillez consulter le site Web de Python. Sous Linux, nous preferons personnellement travailler dans l'environnement graphique WindowMaker (plutot que KDE ou Gnome trop gourmands en ressources), en ouvrant une simple fenetre de terminal pour lancer l'interpreteur Python ou l'execution des scripts, et en faisant appel a l'excellent logiciel Nedit pour l'edition de ces derniers. Gerard Swinnen : Apprendre a programmer avec Python 19. Avec IDLE sous Windows, votre environnement de travail ressemblera a celui-ci : -Python Shell' x| File Edit Debug Windows Help Python 2.2 (#28, Dec 21 2001, 12:21:22) Type "copyright", "credits" or "license" IDLE 0.8 — press Fl for help »>l [HSC 32 bit (Intel)] on Win32 Les trois caracteres « superieur a » constituent le signal d'invite, ou prompt principal, lequel vous indique que Python est pret a executer une commande. Par exemple, vous pouvez tout de suite utiliser l'interpreteur comme une simple calculatrice de bureau. Veuillez done vous-meme tester les commandes ci-dessous (Prenez l'habitude d'utiliser votre cahier d'exercices pour noter les resultats qui apparaissent a l'ecran) : »> 5+3 »> 2-9 # les espaces sont optionnels >>> 7+3*4 # la hierarchie des operations mathematiques # est-elle respectee ? »> (7+3) *4 »> 20/3 # surprise ! ! ! Comme vous pouvez le constater, les operateurs arithmetiques pour l'addition, la soustraction, la multiplication et la division sont respectivement +, -, * et /. Les parentheses sont fonctionnelles. Par defaut, la division est cependant une division entiere, ce qui signifie que si on lui fournit des arguments qui sont des nombres entiers, le resultat de la division est lui-meme un entier (tronque), comme dans le dernier exemple ci-dessus. Si vous voulez qu'un argument soit compris par Python comme etant un nombre reel, il faut le lui faire savoir, en fournissant au moins un point decimal 6 . Essayez par exemple : »> 20.0 / 3 # (comparez le resultat avec celui obtenu a l'exercice precedent) »> 8./5 Si une operation est effectuee avec des arguments de types melanges (entiers et reels), Python convertit automatiquement les operandes en reels avant d'effectuer l'operation. Essayez : »> 4 * 2.5 / 3.3 6 Dans tous les langages de programmation, les conventions mathematiques de base sont celles en vigueur dans les pays anglophones : le separateur decimal sera done toujours un point, et non une virgule comme chez nous. Dans le monde de l'informatique, les nombres reels sont souvent designes comme des nombres "a virgule flottante", ou encore des nombres "de type float". 20. Gerard Swinnen : Apprendre a programmer avec Python 2.2 Donnees et variables Nous aurons l'occasion de detailler plus loin les differents types de donnees numeriques. Mais avant cela, nous pouvons des a present aborder un concept de grande importance : L'essentiel du travail effectue par un programme d'ordinateur consiste a manipuler des donnees. Ces donnees peuvent etre tres diverses (tout ce qui est numerisable, en fait 7 ), mais dans la memoire de l'ordinateur elles se ramenent toujours en definitive a une suite finie de nombres binaires. Pour pouvoir acceder aux donnees, le programme d'ordinateur (quel que soit le langage dans lequel il est ecrit) fait abondamment usage d'un grand nombre de variables de differents types. Une variable apparait dans un langage de programmation sous un nom de variable a peu pres quelconque (voir ci-apres), mais pour l'ordinateur il s'agit d'une reference designant une adresse memoire, c'est-a-dire un emplacement precis dans la memoire vive. A cet emplacement est stocke une valeur bien determines C'est la donnee proprement dite, qui est done stockee sous la forme d'une suite de nombres binaires, mais qui n'est pas necessairement un nombre aux yeux du langage de programmation utilise. Cela peut etre en fait a peu pres n'importe quel « objet » susceptible d'etre place dans la memoire d'un ordinateur, par exemple : un nombre entier, un nombre reel, un nombre complexe, un vecteur, une chaine de caracteres typographiques, un tableau, une fonction, etc. Pour distinguer les uns des autres ces divers contenus possibles, le langage de programmation fait usage de differents types de variables, (le type 'entier', le type 'reel', le type 'chaine de caracteres', le type 'liste', etc.). Nous allons expliquer tout cela dans les pages suivantes. 7 Que peut-on numeriser au juste ? Voila une question tres importante, qu'il vous faudra debattre dans votre cours d'informatique generale. Gerard Swinnen : Apprendre a programmer avec Python 21. 2.3 Noms de variables et mots reserves Les noms de variables sont des noms que vous choisissez vous-meme assez librement. Efforcez- vous cependant de bien les choisir : de preference assez courts, mais aussi explicites que possible, de maniere a exprimer clairement ce que la variable est censee contenir. Par exemple, des noms de variables tel que altitude, altit ou alt conviennent mieux que jc pour exprimer une altitude. Un bon programmeur doit veiller a ce que ses lignes d' instructions soient faciles a lire. Sous Python, les noms de variables doivent en outre obeir a quelques regies simples : • Un nom de variable est une sequence de lettres (a — > z , A — > Z) et de chiffres (0 — > 9), qui doit toujours commencer par une lettre. • Seules les lettres ordinaires sont autorisees. Les lettres accentuees, les cedilles, les espaces, les caracteres speciaux tels que $, #, @, etc. sont interdits, a l'exception du caractere _ (souligne). • La casse est significative (les caracteres majuscules et minuscules sont distingues). Attention : Joseph, joseph, JOSEPH sont done des variables differentes. Soyez attentifs ! Prenez l'habitude d'ecrire l'essentiel des noms de variables en caracteres minuscules (y compris la premiere lettre 8 ). II s'agit d'une simple convention, mais elle est largement respectee. N'utilisez les majuscules qu'a l'interieur meme du nom, pour en augmenter eventuellement la lisibilite, comme dans tableDesMatieres, par exemple. En plus de ces regies, il faut encore ajouter que vous ne pouvez pas utiliser comme noms de variables les 29 « mots reserves » ci-dessous (ils sont utilises par le langage lui-meme) : and assert break class continue def del elif else except exec finally for from global if import in is lambda not or pass print raise return try while yield 8 Les noms commencant par une majuscule ne sont pas interdits, mais l'usage veut qu'on le reserve plutot aux variables qui designent des classes (le concept de classe sera aborde plus loin dans ces notes). Gerard Swinnen : Apprendre a programmer avec Python 22. 2.4 Affectation (ou assignation) Nous savons desormais comment choisir judicieusem*nt un nom de variable. Voyons a present comment nous pouvons en definir une et lui affecter une valeur. Les termes « affecter une valeur » ou « assigner une valeur » a une variable sont equivalents. lis designent l'operation par laquelle on etablit un lien entre le nom de la variable et sa valeur (son contenu). En Python comme dans de nombreux autres langages, l'operation d'affectation est representee par le signe egale 9 : »> n = 7 # donner a n la valeur 7 »> msg = "Quoi de neuf ?" # affecter la valeur "Quoi de neuf ?" a msg »> pi = 3.14159 # assigner sa valeur a la variable pi Les exemples ci-dessus illustrent des instructions d'affectation Python tout a fait classiques. Apres qu'on les ait executees, il existe dans la memoire de l'ordinateur, a des endroits differents : • trois noms de variables, a savoir n, msg et pi • trois sequences d'octets, ou sont encodees le nombre entier 7, la chaine de caracteres Quoi de neuf ? et le nombre reel 3,14159. Les trois instructions d'affectation ci-dessus ont eu pour effet chacune de realiser plusieurs operations dans la memoire de l'ordinateur : • creer et memoriser un nom de variable ; • lui attribuer un type bien determine (ce point sera explicite a la page suivante) ; • creer et memoriser une valeur particuliere ; • etablir un lien (par un systeme interne de pointeurs) entre le nom de la variable et l'emplacement memoire de la valeur correspondante. On peut mieux se representer tout cela par un diagramme d'etat tel que celui-ci : n msg pi ill 7 Quoi de neuf? 3.14159 Les trois noms de variables sont des references, memorisees dans une zone particuliere de la memoire que Ton appelle espace de noms, alors que les valeurs correspondantes sont situees ailleurs, dans des emplacements parfois fort eloignes les uns des autres. Nous aurons l'occasion de preciser ce concept plus loin dans ces pages. 9 II faut bien comprendre qu'il ne s'agit en aucune facon d'une egalite, et que Ton aurait tres bien pu choisir un autre symbolisme, tel que n <— 7 par exemple, comme on le fait souvent dans certains pseudo-langages servant a decrire des algorithmes, pour bien montrer qu'il s'agit de relier un contenu (la valeur 7) a un contenant (la variable n). Gerard Swinnen : Apprendre a programmer avec Python 23. 2.5 Afficher la valeur d'une variable A la suite de l'exercice ci-dessus, nous disposons done des trois variables n, msg et pi. Pour afficher leur valeur a l'ecran, il existe deux possibilites. La premiere consiste a entrer au clavier le nom de la variable, puis . Python repond en affichant la valeur correspondante : >» n 7 >>> msg "Quoi de neuf ?" »> pi 3.14159 II s'agit cependant la d'une fonctionnalite secondaire de l'interpreteur, qui est destinee a vous faciliter la vie lorsque vous faites de simples exercices a la ligne de commande. A l'interieur d'un programme, vous utiliserez toujours l'instruction print : >>> print msg Quoi de neuf ? Remarquez la subtile difference dans les affichages obtenus avec chacune des deux methodes. L'instruction print n'affiche strictement que la valeur de la variable, telle qu'elle a ete encodee, alors que l'autre methode (celle qui consiste a entrer seulement le nom de la variable) affiche aussi des guillemets (afin de vous rappeler le type de la variable : nous y reviendrons). 2.6 Typage des variables Sous Python, il n'est pas necessaire d'ecrire des lignes de programme specifiques pour definir le type des variables avant de pouvoir les utiliser. II vous suffit en effet d'assigner une valeur a un nom de variable pour que celle-ci soit automatiquement creee avec le type qui correspond au mieux a la valeur fournie. Dans l'exercice precedent, par exemple, les variables n, msg et pi ont ete creees automatiquement chacune avec un type different (« nombre entier » pour n, « chaine de caracteres » pour msg, « nombre a virgule flottante » (ou « float », en anglais) pour pi). Ceci constitue une particularity interessante de Python, qui le rattache a une famille particuliere de langages ou Ton trouve aussi par exemple Lisp, Scheme, et quelques autres. On dira a ce sujet que le typage des variables sous Python est un typage dynamique, par opposition au typage statique qui est de regie par exemple en C++ ou en Java. Dans ces langages, il faut toujours - par des instructions distinctes - d'abord declarer (definir) le nom et le type des variables, et ensuite seulement leur assigner un contenu, lequel doit bien entendu etre compatible avec le type declare. Le typage statique est preferable dans le cas des langages compiles, parce qu'il permet d'optimiser l'operation de compilation (dont le resultat est un code binaire « fige »). Le typage dynamique quant a lui permet d'ecrire plus aisem*nt des constructions logiques de niveau eleve (metaprogrammation, reflexivite), en particulier dans le contexte de la programmation orientee objet (polymorphisme). II facilite egalement l'utilisation de structures de donnees tres riches telles que les listes et les dictionnaires. 24. Gerard Swinnen : Apprendre a programmer avec Python 2.7 Affectations multiples Sous Python, on peut assigner une valeur a plusieurs variables simultanement. Exemple : »> x = y >>> x 7 »> y 7 = 7 On peut aussi effectuer des affectations paralleles a l'aide d'un seul operateur : »> a, b = 4, 8.33 »> a 4 »> b 8.33 Dans cet exemple, les variables a et b prennent simultanement les nouvelles valeurs 4 et 8,33. Attention : les francophones que nous sommes avons pour habitude d'utiliser la virgule comme separateur decimal, alors que les langages de programmation utilisent toujours la convention en vigueur dans les pays de langue anglaise, c'est-d-dire le point decimal. La virgule, quant a elle, est tres generalement utilisee pour separer differents elements (arguments, etc.) comme on le voit dans notre exemple, pour les variables elles-memes ainsi que pour les valeurs qu'on leur attribue. (2) Exercices 2.1. Decrivez le plus clairement et le plus completement possible ce qui se passe a chacune des trois lignes de l'exemple ci-dessous : »> largeur = 20 >» hauteur = 5 * 9.3 »> largeur * hauteur 930 2.2. Assignez les valeurs respectives 3, 5, 7 a trois variables a, b, c. Effectuez l'operation a - b/c . Le resultat est-il mathematiquement correct ? Si ce n'est pas le cas, comment devez-vous proceder pour qu'il le soit ? Gerard Swinnen : Apprendre a programmer avec Python 25. 2.8 Operateurs et expressions On manipule les valeurs et les variables qui les referencent, en les combinant avec des operateurs pour former des expressions. Exemple : a, b = 7.3, 12 y = 3*a + b/5 Dans cet exemple, nous commencons par affecter aux variables a et b les valeurs 7,3 et 12. Comme deja explique precedemment, Python assigne automatiquement le type « reel » a la variable a, et le type « entier » a la variable b. La seconde ligne de l'exemple consiste a affecter a une nouvelle variable y le resultat d'une expression qui combine les operateurs * , + et / avec les operandes a, b, 3 et 5. Les operateurs sont les symboles speciaux utilises pour representer des operations mathematiques simples, telles l'addition ou la multiplication. Les operandes sont les valeurs combinees a l'aide des operateurs. Python evalue chaque expression qu'on lui soumet, aussi compliquee soit-elle, et le resultat de cette evaluation est toujours lui-meme une valeur. A cette valeur, il attribue automatiquement un type, lequel depend de ce qu'il y a dans l'expression. Dans l'exemple ci-dessus, y sera du type reel, parce que l'expression evaluee pour determiner sa valeur contient elle-meme au moins un reel. Les operateurs Python ne sont pas seulement les quatre operateurs mathematiques de base. II faut leur ajouter l'operateur ** pour l'exponentiation, ainsi qu'un certain nombre d' operateurs logiques, des operateurs agissant sur les chaines de caracteres, des operateurs effectuant des tests d'identite ou d'appartenance, etc. Nous reparlerons de tout cela plus loin. Signalons au passage la disponibilite de l'operateur modulo, represents par le symbole %. Cet operateur fournit le reste de la division entier e d'un nombre par un autre. Essay ez par exemple : »> 10 % 3 (et prenez note de ce qui se passe ! ) »> 10 % 5 Cet operateur vous sera tres utile plus loin, notamment pour tester si un nombre a est divisible par un nombre b. II suffira en effet de verifier que a % b donne un resultat egal a zero. Exercice : 2.3. Testez les lignes d'instructions suivantes. Decrivez dans votre cahier ce qui se passe : »> r , pi = 12, 3.14159 »> s = pi * r**2 »> print s »> print type(r), type (pi), type(s) »> Quelle est, a votre avis, l'utilite de la fonction type() ? (Note : les fonctions seront decrites en detail, plus loin dans ce cours). 26. Gerard Swinnen : Apprendre a programmer avec Python 2.9 Priorite des operations Lorsqu'il y a plus d'un operateur dans une expression, l'ordre dans lequel les operations doivent etre effectuees depend de regies de priorite. Sous Python, les regies de priorite sont les memes que celles qui vous ont ete enseignees au cours de mathematique. Vous pouvez les memoriser aisem*nt a l'aide d'un « true » mnemotechnique, l'acronyme PEMDAS : • P pour parentheses. Ce sont elles qui ont la plus haute priorite. Elles vous permettent done de « forcer » revaluation d'une expression dans l'ordre que vous voulez. Ainsi 2* (3-1) = 4 , et (1+1)** (5-2) = 8. • E pour exposants. Les exposants sont evalues ensuite, avant les autres operations. Ainsi 2**1+1 = 3(etnon4), et 3*1**10 = 3 (et non 59049 !). • M et D pour multiplication et division, qui ont la meme priorite. Elles sont evaluees avant V addition A et la soustraction S, lesquelles sont done effectuees en dernier lieu. Ainsi 2*3-1 = 5 (plutot que 4), et 2/3-1 = -1 (Rappelez-vous que par defaut Python effectue une division entiere). • Si deux operateurs ont la meme priorite, revaluation est effectuee de gauche a droite. Ainsi dans l'expression 59*100/60, la multiplication est effectuee en premier, et la machine doit done ensuite effectuer 5900/60, ce qui donne 98. Si la division etait effectuee en premier, le resultat serait 59 (rappelez-vous ici encore qu'il s'agit d'une division entiere). Gerard Swinnen : Apprendre a programmer avec Python 27. 2.10 Composition Jusqu'ici nous avons examine les differents elements d'un langage de programmation, a savoir : les variables, les expressions et les instructions, mais sans traiter de la maniere dont nous pouvons les combiner les unes avec les autres. Or Tune des grandes forces d'un langage de programmation de haut niveau est qu'il permet de construire des instructions complexes par assemblage de fragments divers. Ainsi par exemple, si vous savez comment additionner deux nombres et comment afficher une valeur, vous pouvez combiner ces deux instructions en une seule : »> print 17 + 3 »> 20 Cela n'a fair de rien, mais cette fonctionnalite qui parait si evidente va vous permettre de programmer des algorithmes complexes de facon claire et concise. Exemple : »> h, m, s = 15, 27, 34 >>> print "nombre de secondes ecoulees depuis minuit = ", h*3600 + m*60 + s Attention cependant : il y a une limite a ce que vous pouvez combiner ainsi : Ce que vous placez a la gauche du signe egale dans une expression doit toujours etre une variable, et non une expression. Cela provient du fait que le signe egale n'a pas ici la meme signification qu'en mathematique : comme nous l'avons deja signale, il s'agit d'un symbole d'affectation (nous placons un certain contenu dans une variable) et non un symbole d'egalite. Le symbole d'egalite (dans un test conditionnel, par exemple) sera evoque un peu plus loin. Ainsi par exemple, l'instruction m + 1 = b est tout a fait illegale. Par contre, ecrire a = a + 1 est inacceptable en mathematique, alors que cette forme d'ecriture est tres frequente en programmation. L'instruction a = a + 1 signifie en l'occurrence « augmenter la valeur de la variable a d'une unite » (ou encore : « incrementer a »). Nous aurons l'occasion de revenir bientot sur ce sujet. Mais auparavant, il nous faut encore aborder un autre concept de grande importance. 28. Gerard Swinnen : Apprendre a programmer avec Python Chapitre 3 : Controle du flux d'instructions Dans notre premier chapitre, nous avons vu que l'activite essentielle d'un analyste-programmeur est la resolution de problemes. Or, pour resoudre un probleme informatique, il faut toujours effectuer une serie factions dans un certain ordre. La description structuree de ces actions et de l'ordre dans lequel il convient de les effectuer s'appelle un algorithme. Les structures de controle sont les groupes d'instructions qui determinent l'ordre dans lequel les actions sont effectuees. En programmation moderne, il en existe seulement trois : la sequence et la selection, que nous allons decrire dans ce chapitre, et la repetition que nous aborderons au chapitre suivant. 3. 1 Sequence 10 d'instructions Sauf mention explicite, les instructions d'un programme s'executent les unes apres les autres, dans l'ordre ou elles ont ete ecrites a I'interieur du script. Le « chemin » suivi par Python a travers un programme est appele un flux d'instructions, et les constructions qui le modifient sont appelees des instructions de controle de flux. Python execute normalement les instructions de la premiere a la derniere, sauf lorsqu'il rencontre une instruction conditionnelle comme l'instruction if decrite ci-apres (nous en rencontrerons d'autres plus loin, notamment a propos des boucles de repetition). Une telle instruction va permettre au programme de suivre differents chemins suivant les circonstances. 3.2 Selection ou execution conditionnelle Si nous voulons pouvoir ecrire des applications veritablement utiles, il nous faut des techniques permettant d'aiguiller le deroulement du programme dans differentes directions, en fonction des circonstances rencontrees. Pour ce faire, nous devons disposer d'instructions capables de tester une certaine condition et de modifier le comportement du programme en consequence. La plus simple de ces instructions conditionnelles est l'instruction if. Pour experimenter son fonctionnement, veuillez entrer dans votre editeur Python les deux lignes suivantes : »> a = 150 »> if (a > 100) : La premiere commande affecte la valeur 150 a la variable a. Jusqu'ici rien de nouveau. Lorsque vous finissez d'entrer la seconde ligne, par contre, vous constatez que Python reagit d'une nouvelle maniere. En effet, et a moins que vous n'ayez oublie le caractere « : » a la fin de la ligne, vous constatez que le prompt principal (»>) est maintenant remplace par un prompt secondaire constitute de trois points 11 . Si votre editeur ne le fait pas automatiquement, vous devez a present effectuer une tabulation (ou entrer 4 espaces) avant d'entrer la ligne suivante, de maniere a ce que celle-ci soit indentee (c'est-a- dire en retrait) par rapport a la precedente. Votre ecran devrait se presenter maintenant comme suit : 10 Tel qu'il est utilise ici, le terme de sequence designe done une serie d'instructions qui se suivent. Nous prefererons dans la suite de cet ouvrage reserver ce terme a un concept Python precis, lequel englobe les chatnes de caracteres, les tuples et les listes (voir plus loin). 1 1 Dans certaines versions de l'editeur Python pour Windows, le prompt secondaire n'apparait pas. Gerard Swinnen : Apprendre a programmer avec Python 29. »> a = 150 »> if (a > 100) : print "a depasse la centaine" Frappez encore une fois . Le programme s'execute, et vous obtenez : a depasse la centaine Recommencez le meme exercice, mais avec a = 20 en guise de premiere ligne : cette fois Python n'affiche plus rien du tout. L'expression que vous avez placee entre parentheses est ce que nous appellerons desormais une condition. L'instruction if permet de tester la validite de cette condition. Si la condition est vraie, alors l'instruction que nous avons indentee apres le « : » est executee. Si la condition est fausse, rien ne se passe. Notez que les parentheses utilisees ici sont optionnelles sous Python. Nous les avons utilisees pour ameliorer la lisibilite. Dans d'autres langages, il se peut qu'elles soient obligatoires. Recommencez encore, en ajoutant deux lignes comme indique ci-dessous. Veillez bien a ce que la quatrieme ligne debute tout a fait a gauche (pas d'indentation), mais que la cinquieme soit a nouveau indentee (de preference avec un retrait identique a celui de la troisieme) : »> a = 20 »> if (a > 100) : print "a depasse la centaine" . . . else : print "a ne depasse pas cent" Frappez encore une fois. Le programme s'execute, et affiche cette fois : a ne depasse pas cent Comme vous l'aurez certainement deja compris, l'instruction else (« sinon », en anglais) permet de programmer une execution alternative, dans laquelle le programme doit choisir entre deux possibilites. On peut faire mieux encore en utilisant aussi l'instruction elif (contraction de « else if»): »> a = 0 >» if a > 0 : print "a est positif " . . . elif a < 0 : print "a est negatif " . . . else : print "a est nul" 30. Gerard Swinnen : Apprendre a programmer avec Python 3.3 Operateurs de comparaison La condition evaluee apres l'instruction if peut contenir les operateurs de comparaison suivants : x == y # x est egal a y x != y # x est different de y x > y # x est plus grand que y x < y # x est plus petit que y x >= y # x est plus grand que, ou egal a y x <= y # x est plus petit que, ou egal a y Exemple : »> a = 7 »> if (a % 2 == 0) : print "a est pair" print "parce que le reste de sa division par 2 est nul" else : print "a est impair" Notez bien que l'operateur de comparaison pour l'egalite de deux valeurs est constitute de deux signes « egale » et non d'un seul 12 . (Le signe « egale » utilise seul est un operateur d'affectation, et non un operateur de comparaison. Vous retrouverez le meme symbolisme en C+ + et en Java). 3.4 Instructions composees - Blocs d'instructions La construction que vous avez utilisee avec l'instruction if est votre premier exemple ^instruction composee. Vous en rencontrerez bientot d'autres. Sous Python, toutes les instructions composees ont toujours la meme structure : une ligne d'en-tete terminee par un double point, suivie d'une ou de plusieurs instructions indentees sous cette ligne d'en-tete. Exemple : Ligne d ' en-tete : premiere instruction du bloc derniere instruction du bloc S'il y a plusieurs instructions indentees sous la ligne d'en-tete, elles doivent I'etre exactement au meme niveau (comptez un decalage de 4 caracteres, par exemple). Ces instructions indentees constituent ce que nous appellerons desormais un bloc d'instructions. Un bloc d'instructions est une suite d'instructions formant un ensemble logique, qui n'est execute que dans certaines conditions definies dans la ligne d'en-tete. Dans l'exemple du paragraphe precedent, les deux lignes d'instructions indentees sous la ligne contenant l'instruction if constituent un meme bloc logique : ces deux lignes ne sont executees - toutes les deux - que si la condition testee avec l'instruction if se revele vraie, c'est-a-dire si le reste de la division de a par 2 est nul. 12 Rappel : l'operateur % est l'operateur modulo : il calcule le reste d'une division entiere. Ainsi par exemple, a % 2 fournit le reste de la division de a par 2. Gerard Swinnen : Apprendre a programmer avec Python 31. 3.5 Instructions imbriquees II est parfaitement possible d'imbriquer les unes dans les autres plusieurs instructions composees, de maniere a realiser des structures de decision complexes. Exemple : if embranchement == "vertebres" : # 1 if classe == "mammi feres" : # 2 if ordre == "carnivores": # 3 if famille == "felins" : # 4 print "c'est peut-etre un chat" # 5 print "c'est en tous cas un mammi fere" # 6 elif classe == 'oiseaux': # 7 print "c'est peut-etre un canari" # 8 prinf'la classification des animaux est complexe" # 9 Analysez cet exemple. Ce fragment de programme n'imprime la phrase « c'est peut-etre un chat » que dans le cas ou les quatre premieres conditions testees sont vraies. Pour que la phrase « c'est en tous cas un mammifere » soit affichee, il faut et il suffit que les deux premieres conditions soient vraies. L'instruction d'affichage de cette phrase (ligne 4) se trouve en effet au meme niveau d'indentation que l'instruction : if ordre == "carnivores": (ligne 3). Les deux font done partie d'un meme bloc, lequel est entierement execute si les conditions testees aux lignes 1 & 2 sont vraies. Pour que la phrase « c'est peut-etre un canari » soit affichee, il faut que la variable embranchement contienne « vertebres », et que la variable classe contienne « oiseaux ». Quant a la phrase de la ligne 9, elle est affichee dans tous les cas, parce qu'elle fait partie du meme bloc d'instructions que la ligne 1 . 3.6 Quelques regies de syntaxe Python Tout ce qui precede nous amene a faire le point sur quelques regies de syntaxe : 3.6.1 Les limites des instructions et des blocs sont definies par la mise en page Dans de nombreux langages de programmation, il faut terminer chaque ligne d'instructions par un caractere special (souvent le point-virgule). Sous Python, c'est le caractere de fin de ligne 13 qui joue ce role. (Nous verrons plus loin comment outrepasser cette regie pour etendre une instruction complexe sur plusieurs lignes). On peut egalement terminer une ligne d'instructions par un commentaire. Un commentaire Python commence toujours par le caractere special # . Tout ce qui est inclus entre ce caractere et le saut a la ligne suivant est completement ignore par le compilateur. Dans la plupart des autres langages, un bloc d'instructions doit etre delimite par des symboles specifiques (parfois meme par des instructions, telles que begin et end). En C+ + et en Java, par exemple, un bloc d'instructions doit etre delimite par des accolades. Cela permet d'ecrire les blocs d'instructions les uns a la suite des autres, sans se preoccuper d'indentation ni de sauts a la ligne, mais cela peut conduire a l'ecriture de programmes confus, difficiles a relire pour les pauvres humains que nous sommes. On conseille done a tous les programmeurs qui utilisent ces langages de se servir aussi des sauts a la ligne et de l'indentation pour bien delimiter visuellement les blocs. 13 Ce caractere n'apparait ni a l'ecran, ni sur les listings imprimes. II est cependant bien present, a un point tel qu'il fait meme probleme dans certains cas, parce qu'il n'est pas encode de la meme maniere par tous les systemes d'exploitation. Nous en reparlerons plus loin, a l'occasion de notre etude des fichiers texte (page 115). 32. Gerard Swinnen : Apprendre a programmer avec Python Avec Python, vous devez utiliser les sauts a la ligne et l'indentation, mais en contrepartie vous n'avez pas a vous preoccuper d'autres symboles delimiteurs de blocs. En definitive, Python vous force done a ecrire du code lisible, et a prendre de bonnes habitudes que vous conserverez lorsque vous utiliserez d'autres langages. 3.6.2 Instruction composee = En-tete , double point , bloc constructions indente Bloc 1 Ligne d' en-tete Nous aurons de nombreuses occasions d'approfondir le concept de « bloc destructions » et de faire des exercices a ce sujet, des le chapitre suivant. Le schema ci-contre en resume le principe. • Les blocs d'instructions sont toujours associes a une ligne d'en-tete contenant une instruction bien specifique (if, elif, else, while, def, ...) se terminant par un double point. ' Les blocs sont delimites par l'indentation : toutes les lignes d'un meme bloc doivent etre indentees exactement de la meme maniere (e'est-a-dire decalees vers la droite d'un meme nombre d'espaces 14 ). Le nombre d'espaces a utiliser pour l'indentation est quelconque, mais la plupart des programmeurs utilisent des multiples de 4. • Notez que le code du bloc le plus externe (bloc 1) ne peut pas lui-meme etre ecarte de la marge de gauche (II n'est imbrique dans rien). Bloc 2 Ligne d'en-tete : Bloc 3 Bloc 2 (suite) Bloc 1 (suite) 3.6.3 Les espaces et les commentaires sont normalement ignores A part ceux qui servent a l'indentation, en debut de ligne, les espaces places a l'interieur des instructions et des expressions sont presque toujours ignores (sauf s'ils font partie d'une chaine de caracteres). II en va de meme pour les commentaires : ceux-ci commencent toujours par un caractere diese (#) et s'etendent jusqu'a la fin de la ligne courante. 14 Vous pouvez aussi indenter a l'aide de tabulations, mais alors vous devrez faire tres attention a ne pas utiliser tantot des espaces, tantot des tabulations pour indenter les lignes d'un meme bloc. En effet, et meme si le resultat parait identique a l'ecran, espaces et tabulations sont des codes binaires distincts : Python considerera done que ces lignes indentees differemment font partie de blocs differents. II peut en resulter des erreurs difficiles a deboguer. En consequence, la plupart des programmeurs preferent se passer des tabulations. Si vous utilisez un editeur "intelligent", vous pouvez escamoter le probleme en activant l'option "Remplacer les tabulations par des espaces". Gerard Swinnen : Apprendre a programmer avec Python 33. Chapitre 4 : Instructions repetitives. L'une des taches que les machines font le mieux est la repetition sans erreur de taches identiques. II existe bien des methodes pour programmer ces taches repetitives. Nous allons commencer par l'une des plus fondamentales : la boucle de repetition construite autour de l'instruction while. 4.1 Re-affectation Nous ne l'avions pas encore signale explicitement : il est permis de re-affecter une nouvelle valeur a une meme variable, autant de fois qu'on le souhaite. L'effet d'une re-affectation est de remplacer l'ancienne valeur d'une variable par une nouvelle. »> altitude = 320 >>> print altitude 320 »> altitude = 375 >>> print altitude 375 Ceci nous amene a attirer une nouvelle fois votre attention sur le fait que le symbole egale utilise sous Python pour realiser une affectation ne doit en aucun cas etre confondu avec un symbole d'egalite tel qu'il est compris en mathematique. II est tentant d'interpreter l'instruction altitude = 320 comme une affirmation d'egalite, mais ce n'en n'est pas une ! • Premierement, l'egalite est commutative, alors que l'affectation ne Test pas. Ainsi, en mathematique, les ecritures a = 7 et 7 = a sont equivalentes, alors qu'une instruction de programmation telle que 375 = altitude serait illegale. • Deuxiemement, l'egalite est permanente, alors que l'affectation peut etre remplacee comme nous venons de le voir. Lorsqu'en mathematique, nous affirmons une egalite telle que a = b au debut d'un raisonnement, alors a continue a etre egal a b durant tout le developpement qui suit. En programmation, une premiere instruction d'affectation peut rendre egales les valeurs de deux variables, et une instruction ulterieure en changer ensuite l'une ou l'autre. Exemple : »> a = 5 >» b = a # a et b contiennent des valeurs egales >>> b = 2 # a et b sont maintenant differentes Rappelons ici que Python permet d'affecter leurs valeurs a plusieurs variables simultanement : »> a, b, c, d = 3, 4, 5, 7 Cette fonctionnalite de Python est bien plus interessante encore qu'elle n'en a fair a premiere vue. Supposons par exemple que nous voulions maintenant echanger les valeurs des variables a et c. (Actuellement, a contient la valeur 3, et c la valeur 5. Nous voudrions que ce soit l'inverse). Comment faire ? (4) Exercice 4.1. Ecrivez les lignes d' instructions necessaires pour obtenir ce resultat. 34. Gerard Swinnen : Apprendre a programmer avec Python A la suite de l'exercice propose ci-dessus, vous aurez certainement trouve une methode, et votre professeur vous demandera probablement de la commenter en classe. Comme il s'agit d'une operation courante, les langages de programmation proposent souvent des raccourcis pour l'effectuer (par exemple des instructions specialisees, telle l'instruction SWAP du langage Basic). Sous Python, / 'affectation multiple permet de programmer l'echange d'une maniere particulierement elegante : »> a, b = b, a (On pourrait bien entendu echanger d'autres variables en meme temps, dans la meme instruction). 4.2 Repetitions en boucle - l'instruction while L'une des choses que les machines font le mieux est la repetition sans erreur de taches identiques. II existe bien des methodes pour programmer ces taches repetitives. Nous allons commencer par l'une des plus fondamentales : la boucle construite a partir de l'instruction while. Veuillez done entrer les commandes ci-dessous : »> a = 0 »> while (a < 7) : # (n'oubliez pas le double point !) ... a = a + 1 # (n'oubliez pas 1 ' indentation !) print a Frappez encore une fois . Que se passe-t-il ? Avant de lire les commentaires de la page suivante, prenez le temps d'ouvrir votre cahier et d'y noter cette serie de commandes. Decrivez aussi le resultat obtenu, et essayez de l'expliquer de la maniere la plus detaillee possible. Gerard Swinnen : Apprendre a programmer avec Python 35. Commentaires Le mot while signifie « tant que » en anglais. Cette instruction utilisee a la seconde ligne indique a Python qu'il lui faut repeter continuellement le bloc d 'instructions qui suit, tant que le contenu de la variable a reste inferieur a 7. Comme l'instruction if abordee au chapitre precedent, l'instruction while amorce une instruction composee. Le double point a la fin de la ligne introduit le bloc d'instructions a repeter, lequel doit obligatoirement se trouver en retrait. Comme vous l'avez appris au chapitre precedent, toutes les instructions d'un meme bloc doivent etre indentees exactement au meme niveau (c'est-a-dire decalees a droite d'un meme nombre d'espaces). Nous avons ainsi construit notre premiere boucle de progr animation, laquelle repete un certain nombre de fois le bloc d'instructions indentees. Voici comment cela fonctionne : • Avec l'instruction while, Python commence par evaluer la validite de la condition fournie entre parentheses (Celles-ci sont optionnelles. Nous ne les avons utilisees que pour clarifier notre explication). • Si la condition se revele fausse, alors tout le bloc qui suit est ignore et l'execution du programme se termine 15 . • Si la condition est vraie, alors Python execute tout le bloc d'instructions constituant le corps de la boucle, c'est-a-dire : • l'instruction a = a + 1 qui incremente d'une unite le contenu de la variable a (ce qui signifie que Ton affecte a la variable a une nouvelle valeur, qui est egale a la valeur precedente augmentee d'une unite). • l'instruction print qui affiche la valeur courante de la variable a • lorsque ces deux instructions ont ete executees, nous avons assiste a une premiere iteration, et le programme boucle, c'est-a-dire que l'execution reprend a la ligne contenant l'instruction while. La condition qui s'y trouve est a nouveau evaluee, et ainsi de suite. Dans notre exemple, si la condition a<7 est encore vraie, le corps de la boucle est execute une nouvelle fois et le bouclage se poursuit. Remarques : • La variable evaluee dans la condition doit exister au prealable (II faut qu'on lui ait deja affecte au moins une valeur) • Si la condition est fausse au depart, le corps de la boucle n'est jamais execute • Si la condition reste toujours vraie, alors le corps de la boucle est repete indefiniment (tout au moins tant que Python lui-meme continue a fonctionner). II faut done veiller a ce que le corps de la boucle contienne au moins une instruction qui change la valeur d'une variable intervenant dans la condition evaluee par while, de maniere a ce que cette condition puisse devenir fausse et la boucle se terminer. Exemple de boucle sans fin (a eviter) : »> n = 3 »> while n < 5: print "hello ! " 15 ... du moins dans cet exemple. Nous verrons un peu plus loin qu'en fait l'execution continue avec la premiere instruction qui suit le bloc indente, et qui fait partie du meme bloc que l'instruction while elle-meme. 36. Gerard Swinnen : Apprendre a programmer avec Python 4.3 Elaboration de tables Recommencez a present le premier exercice, mais avec la petite modification ci-dessous : »> a = 0 »> while a < 12 : a = a +1 print a , a**2 , a**3 Vous devriez obtenir la liste des carres et des cubes des nombres de 1 a 12. Notez au passage que l'instruction print permet d'afficher plusieurs expressions l'une a la suite de l'autre sur la meme ligne : il suffit de les separer par des virgules. Python insere automatiquement un espace entre les elements affiches. 4.4 Construction d'une suite mathematique Le petit programme ci-dessous permet d'afficher les dix premiers termes d'une suite appelee « Suite de Fibonacci ». II s'agit d'une suite de nombres, dont chaque terme est egal a la somme des deux termes qui le precedent. Analysez ce programme (qui utilise judicieusem*nt {'affectation multiple). Decrivez le mieux possible le role de chacune des instructions. »> a, b, c = 1, 1, 1 »> while c < 11 : print b, ... a, b, c = b, a+b, c+1 Lorsque vous lancez l'execution de ce programme, vous obtenez : 1 2 3 5 8 13 21 34 55 89 Les termes de la suite de Fibonacci sont affiches sur la meme ligne. Vous obtenez ce resultat grace a la virgule placee a la fin de la ligne qui contient l'instruction print. Si vous supprimez cette virgule, les nombres seront affiches l'un en-dessous de l'autre. Dans vos programmes futurs, vous serez tres souvent amenes a mettre au point des boucles de repetition comme celle que nous analysons ici. II s'agit d'une question essentielle, que vous devez apprendre a maitriser parfaitement. Soyez sur que vous y arriverez progressivement, a force d'exercices. Lorsque vous examinez un probleme de cette nature, vous devez considerer les lignes d'instruction, bien entendu, mais surtout decortiquer les etats successifs des differentes variables impliquees dans la boucle. Cela n'est pas toujours facile, loin de la. Pour vous aider a y voir plus clair, prenez la peine de dessiner sur papier une table d'etats similaire a celle que nous reproduisons ci-dessous pour notre programme « suite de Fibonacci » : Variables a b c Valeurs initiales 1 1 1 Valeurs prises 1 2 2 successivement, au 2 3 3 cours des iterations 3 5 4 5 8 5 Expression de b a+b c+1 remplacement Gerard Swinnen : Apprendre a programmer avec Python 37. Dans une telle table, on effectue en quelque sorte « a la main » le travail de l'ordinateur, en indiquant ligne par ligne les valeurs que prendront chacune des variables au fur et a mesure des iterations successives. On commence par inscrire en haut du tableau les noms des variables concernees. Sur la ligne suivante, les valeurs initiales de ces variables (valeurs qu'elles possedent avant le demarrage de la boucle). Enfin, tout en bas du tableau, les expressions utilisees dans la boucle pour modifier l'etat de chaque variable a chaque iteration. On remplit alors quelques lignes correspondant aux premieres iterations. Pour etablir les valeurs d'une ligne, il suffit d'appliquer a celles de la ligne precedente, l'expression de remplacement qui se trouve en bas de chaque colonne. On verifie ainsi que Ton obtient bien la suite recherchee. Si ce n'est pas le cas, il faut essayer d'autres expressions de remplacement. Exercices : 4.2. Ecrivez un programme qui affiche les 20 premiers termes de la table de multiplication par 7. 4.3. Ecrivez un programme qui affiche une table de conversion de sommes d' argent exprimees en euros, en dollars canadiens. La progression des sommes de la table sera « geometrique », comme dans l'exemple ci-dessous : 1 euro(s) =1.65 dollar (s) 2 euro(s) =3.30 dollar (s) 4 euro(s) = 6.60 dollar (s) 8 euro(s) = 13.20 dollar (s) etc. (S'arreter a 16384 euros) 4.4. Ecrivez un programme qui affiche une suite de 12 nombres dont chaque terme soit egal au triple du terme precedent. 4.5 Premiers scripts, ou : Comment conserver nos programmes ? Jusqu'a present, vous avez toujours utilise Python en mode interactif (c'cst-a-divc que vous avez a chaque fois entre les commandes directement dans l'interpreteur, sans les sauvegarder au prealable dans un fichier). Cela vous a permis d'apprendre tres rapidement les bases du langage, par experimentation directe. Cette facon de faire presente toutefois un gros inconvenient : toutes les sequences destructions que vous avez ecrites disparaissent irremediablement des que vous fermez l'interpreteur. Avant de poursuivre plus avant votre etude, il est done temps que vous appreniez a sauvegarder vos programmes dans des fichiers, sur disque dur ou disquette, de maniere a pouvoir les retravailler par etapes successives, les transferer sur d'autres machines, etc. Pour ce faire, vous allez desormais rediger vos sequences destructions dans un editeur de textes quelconque (par exemple Joe, Nedit, Kate ... sous Linux, Edit sous MS-DOS, Wordpad sous Windows, ou mieux encore l'editeur incorpore dans une interface de developpement telle que IDLE ou PythonWin). Ainsi vous ecrirez un script, que vous pourrez ensuite sauvegarder, modifier, copier, etc. comme n'importe quel autre texte traite par ordinateur 16 . La figure ci-dessous illustre l'utilisation de l'editeur Nedit sous Gnome (Linux) : 16 II serait parfaitement possible d'utiliser un systeme de traitement de textes, a la condition d'effectuer la sauvegarde sous un format "texte pur" (sans balises de mise en page). II est cependant preferable d'utiliser un veritable editeur ANSI "intelligent" tel que nedit ou IDLE, muni d'une fonction de coloration syntaxique pour Python, qui vous aide a eviter les fautes de syntaxe. Avec IDLE, suivez le menu : File — > New window (ou frappez CTRL-N) pour ouvrir une nouvelle fenetre dans laquelle vous ecrirez votre script. Pour l'executer, il vous suffira (apres sauvegarde), de suivre le menu : Edit -> Run script (ou de frapper CTRL-F5). 38. Gerard Swinnen : Apprendre a programmer avec Python File Edit Search Preferences Shell Macro Windows Help /dos_data/python/essais/fibo1.py DOS 496 bytes # Premier essai de script Python # petit programme simple affichant une suite de Fibonacci , c.a.d. une suite # de nombres dont chaque terme est egal a la somme des deux precedents . print "Suite de Fibonacci :" a / b / c= 1,1,1 # a & b servent au calcul des termes successifs # c est un simple compteur print. 1 # affichage du premier terme while c<15 : # nous afficherons 15 termes au total a,b,c = b,a+b,c+l print b Par la suite, lorsque vous voudrez tester l'execution de votre programme, il vous suffira de lancer l'interpreteur Python en lui fournissant (comme argument) le nom du fichier qui contient le script. Par exemple, si vous avez place un script dans un fichier nomme « MonScript », il suffira d'entrer la commande suivante dans une fenetre de terminal pour que ce script s'execute : python MonScript Pour faire mieux encore, veillez a donner au fichier un nom qui se termine par l'extension .py Si vous respectez cette convention, vous pourrez (sous Windows, KDE, Gnome, ...) lancer l'execution du script, simplement en cliquant sur son nom ou sur l'icone correspondante dans le gestionnaire de fichiers (c'est-a-dire l'explorateur, sous Windows, ou Konqueror, sous KDE). Ces gestionnaires graphiques « savent » en effet qu'il doivent lancer l'interpreteur Python chaque fois que leur utilisateur essaye d'ouvrir un fichier dont le nom se termine par .py. (Cela suppose bien entendu qu'ils aient ete correctement configures). La meme convention permet en outre aux editeurs « intelligents » de reconnaitre automatiquement les scripts Python et d'adapter leur coloration syntaxique en consequence. Un script Python contiendra des sequences destructions identiques a celles que vous avez experimentees jusqu'a present. Puisque ces sequences sont destinees a etre conservees et relues plus tard par vous-meme ou par d'autres, il vous est tres fortement recommande d'expliciter vos scripts le mieux possible, en y incorporant de nombreux commentaires. La principale difficulty de la programmation consiste en effet a mettre au point des algorithmes corrects. Afin que ces algorithmes puissent etre verifies, corriges, modifies, etc. dans de bonnes conditions, il est essentiel que leur auteur les decrive le plus completement et le plus clairement possible. Et le meilleur emplacement pour cette description est le corps meme du script (ainsi elle ne peut pas s'egarer). Un bon programmeur veille toujours a inserer un grand nombre de commentaires dans ses scripts. En procedant ainsi, non seulement il facilite la comprehension de ses algorithmes pour d'autres lecteurs eventuels, mais encore il se force lui-meme a avoir les idees plus claires. On peut inserer des commentaires quelconques a peu pres n'importe ou dans un script. II suffit de les faire preceder d'un caractere #. Lorsqu'il rencontre ce caractere, l'interpreteur Python ignore tout ce qui suit, jusqu'a la fin de la ligne courante. Comprenez bien qu'il est important d'inclure des commentaires au fur et d mesure de l'avancement de votre travail de programmation. N'attendez pas que votre script soit termine pour les aj outer « apres coup ». Vous vous rendrez progressivement compte qu'un programmeur passe enormement de temps a relire son propre code (pour le modifier, y rechercher des erreurs, etc). Cette relecture sera grandement facilitee si le code comporte de nombreuses explications et Gerard Swinnen : Apprendre a programmer avec Python 39. remarques. Ouvrez done un editeur de texte, et redigez le script ci-dessous # Premier essai de script Python # petit programme simple affichant une suite de Fibonacci, c.a.d. une suite # de nombres dont chaque terme est egal a la somme des deux precedents . a, b, c = 1, 1, 1 #a&b servent au calcul des termes successifs # c est un simple compteur print 1 # affichage du premier terme while c<15: # nous afficherons 15 termes au total a, b, c = b, a+b, c+1 print b Afin de vous montrer tout de suite le bon exemple, nous commencons ce script par trois lignes de commentaires, qui contiennent une courte description de la fonctionnalite du programme. Prenez l'habitude de faire de meme dans vos propres scripts. Les lignes de code elle-memes sont documentees. Si vous procedez comme nous l'avons fait, e'est-a-dire en inserant des commentaires a la droite des instructions correspondantes, veillez a les ecarter suffisamment de celles-ci, afin de ne pas gener leur lisibilite. Lorsque vous aurez bien verifie votre texte, sauvegardez-le et executez-le. Notes : Bien que ce ne soit pas indispensable, nous vous recommandons une fois encore de choisir pour vos scripts des noms de fichiers se terminant par l'extension .py Cela aide beaucoup a les identifier comme tels dans un repertoire. Les gestionnaires graphiques de fichiers {explorateur Windows, Konqueror) se servent d'ailleurs de cette extension pour leur associer une icone specifique. Evitez cependant de choisir des noms qui risqueraient d'etre deja attribues a des modules python existants : des noms tels que math.py ou Tkinter.py, par exemple, sont a proscrire ! Si vous travaillez en mode texte sous Linux, ou dans une fenetre MSDOS, vous pouvez executer votre script a l'aide de la commande python suivie du nom du script. Si vous travaillez en mode graphique sous Linux, vous pouvez ouvrir une fenetre de terminal et faire la meme chose. Dans Vexplorateur Windows ou dans Konqueror, vous pouvez lancer l'execution de votre script en effectuant un simple clic de souris sur l'icone correspondante. Si vous travaillez avec IDLE, vous pouvez lancer l'execution du script en cours d'edition, directement a l'aide de la combinaison de touches . Consultez votre professeur concernant les autres possibilites de lancement eventuelles sur differents systemes d' exploitation. 4.6 Remarque concernant les caracteres accentues et speciaux : A partir de la version 2.3, il est vivement recommande aux francophones d'inclure l'un des pseudo-commentaires suivants au debut de tous leurs scripts Python (obligatoirement a la l e ou a la 2 e ligne) : # -*- coding :Latin-l -*- Ou bien : # _*_ coding :Utf-8 -*- Ces pseudo-commentaires indiquent a Python que vous utiliserez dans votre script : # soit le jeu de caracteres accentues correspondant aux principales langues de l'Europe occidentale (Francais, Italien, Portugais, etc.), code sur un seul octet suivant la norme ISO-8859 ; 40. Gerard Swinnen : Apprendre d programmer avec Python • soit le systeme de codage mondial sur deux octets appele Unicode (dont la variante Utf-8 ne code que les caracteres « speciaux » sur deux octets, les caracteres du jeu ASCII standard restant codes sur un seul octet). Ce dernier systeme commence a se repandre de plus en plus, car il presente l'avantage de permettre la coexistence de caracteres de toutes origines dans le meme document (caracteres grecs, arabes, cyrilliques, japonais, etc.). Python peut utiliser les deux systemes, mais vous devez lui signaler lequel vous utilisez. Si votre systeme d'exploitation est configure de telle maniere que les frappes clavier generent des codes Utf- 8, configurez votre editeur de textes pour qu'il utilise lui aussi ce codage, et placez le second des pseudo-commentaires indiques ci-dessus au debut de chacun de vos scripts. Si votre systeme d'exploitation fonctionne suivant la norme ancienne (ISO-8859), vous devrez utiliser plutot le premier pseudo-commentaire. Si vous n'en indiquez aucun, vous recevrez de temps a autre des messages d'avertissem*nt de la part de l'interpreteur, et vous eprouverez peut-etre meme quelques difficultes a editer correctement vos scripts dans l'environnement IDLE (en particulier sous Windows). Que vous utilisiez une norme ou l'autre, ou aucune, vos scripts s'executeront correctement. C'est seulement pour pouvoir les rediger sur votre propre systeme qu'il faut choisir l'option adequate. Exercices : 4.5. Ecrivez un programme qui calcule le volume d'un parallelepipede rectangle dont sont fournis au depart la largeur, la hauteur et la profondeur. 4.6. Ecrivez un programme qui convertisse un nombre entier de secondes fourni au depart, en un nombre d'annees, de mois, de jours, de minutes et de secondes. (Utilisez l'operateur modulo : % ). 4.7. Ecrivez un programme qui affiche les 20 premiers termes de la table de multiplication par 7, en signalant au passage (a l'aide d'une asterisque) ceux qui sont des multiples de 3. Exemple : 7 14 21 * 28 35 42 * 49 4.8. Ecrivez un programme qui calcule les 50 premiers termes de la table de multiplication par 13, mais n'affiche que ceux qui sont des multiples de 7. 4.9. Ecrivez un programme qui affiche la suite de symboles suivante : ** *** **** Gerard Swinnen : Apprendre a programmer avec Python 41. Chapitre 5 : Principaux types de donnees Dans le chapitre 2, nous avons deja manipule des donnees de differents types : des nombres entiers ou reels, et des chaines de caracteres. II est temps a present d'examiner d'un peu plus pres ces types de donnees, et egalement de vous en faire decouvrir d'autres. 5. 1 Les donnees numeriques Dans les exercices realises jusqu'a present, nous avons deja utilise des donnees de deux types : les nombres entiers ordinaires et les nombres reels (aussi appeles nombres a virgule flottante). Tactions de mettre en evidence les caracteristiques (et les limites) de ces concepts : 5.1.1 Les types « integer » et « long » Supposons que nous voulions modifier legerement notre precedent exercice sur la suite de Fibonacci, de maniere a obtenir l'affichage d'un plus grand nombre de termes. A priori, il suffit de modifier la condition de bouclage, dans la deuxieme ligne. Avec « while c<49 : » , nous devrions obtenir quarante-huit termes. Modifions done legerement l'exercice, de maniere a afficher aussi le type de la variable principale : >» a, b, c = 1, 1, 1 »> while c<49: print c, " : ", b, type (b) a, b, c = b, a+b, c+1 (affichage des 43 premiers termes) 44 1134903170 45 1836311903 46 2971215073 47 4807526976 48 7778742049 Que pouvons-nous constater ? Si nous n'avions pas utilise la fonction type(), qui nous permet de verifier a chaque iteration le type de la variable b, nous n'aurions rien remarque du tout : la suite des nombres de Fibonacci s'affiche sans probleme (et nous pourrions encore l'allonger de nombreux termes supplementaires). II semble done que Python soit capable de traiter des nombres entiers de taille illimitee. L'exercice que nous venons de realiser indique cependant qu'il se passe « quelque chose » lorsque ces nombres deviennent tres grands. Au debut du programme, les variables a, b et c sont definies implicitement comme etant du type integer. C'est ce qui se passe toujours avec Python lorsqu'on affecte une valeur entiere a une variable, a condition que cette valeur ne soit pas trop grande. Dans la memoire de l'ordinateur, ce type de donnee est en effet encode sous la forme d'un bloc de 4 octets (ou 32 bits). Or la gamme de valeurs decimales qu'il est possible d' encoder sur 4 octets seulement s'etend de -2147483648 a + 2147483647 (Voir cours d'informatique generale). Les calculs effectues avec ce type de variable sont toujours tres rapides, parce que le processeur de l'ordinateur est capable de traiter directement par lui-meme de tels nombres entiers a 32 bits. En revanche, lorsqu'il est question de traiter des nombres entiers plus grands, ou encore des nombres reels (nombres « a virgule flottante »), les logiciels que sont les interpreteurs et compilateurs doivent effectuer un gros travail de codage/decodage, afin de ne presenter en definitive au processeur que des operations binaires sur des nombres entiers de 32 bits au maximum. 42. Gerard Swinnen : Apprendre a programmer avec Python Vous savez deja que le type des variables Python est defini de maniere dynamique. Puisqu'il s'agit du type le plus performant (aussi bien en termes de vitesse de calcul qu'en termes d'occupation de place dans la memoire), Python utilise le type integer par defaut, chaque fois que cela est possible, c'est-a-dire tant que les valeurs traitees sont des entiers compris entre les limites deja mentionnees plus haut (environ 2 milliards, en positif ou en negatif). Lorsque les valeurs traitees sont des nombres entiers se situant au-dela de ces limites, leur encodage dans la memoire de l'ordinateur devient plus complexe. Les variables auxquelles on affecte de tels nombres sont alors automatiquement definies comme appartenant au type « entier long » (lequel est designe par long dans la terminologie Python). Ce type long permet l'encodage de valeurs entieres avec une precision quasi infinie : une valeur definie sous cette forme peut en effet posseder un nombre de chiffres significatifs quelconque, ce nombre n 'e'tant limite que par la taille de la memoire disponible sur l'ordinateur utilise ! Exemple : »> a, b, c = 3, 2, 1 »> while c < 15: print c, ": ", b a, b, c = b, a*b, c+1 1 : 2 2 : 6 3 : 12 4 : 72 5 : 864 6 : 62208 7 : 53747712 8 : 3343537668096 9 : 179707499645975396352 10 : 600858794305667322270155425185792 11 : 107978831564966913814384922944738457859243070439030784 12 : 64880030544660752790736837369104977695001034284228042891827649456186234 582611607420928 13 : 70056698901118320029237641399576216921624545057972697917383692313271754 88362123506443467340026896520469610300883250624900843742470237847552 14 : 45452807645626579985636294048249351205168239870722946151401655655658398 64222761633581512382578246019698020614153674711609417355051422794795300591700 96950422693079038247634055829175296831946224503933501754776033004012758368256 »> Dans l'exemple ci-dessus, la valeur des nombres affiches augmente tres rapidement, car chacun d'eux est egal au produit des deux termes precedents. Au depart, les variables a, b et c sont du type integer, puisqu'on leur affecte des petites valeurs numeriques entieres : 3, 2 et 1. A partir de la 8 e iteration, cependant, les variables b et a sont automatiquement converties l'une apres l'autre dans le type long : le resultat de la multiplication des termes 6 et 7 est en effet deja bien superieur a la limite des 2 milliards evoquee plus haut. La progression continue avec des nombres de plus en plus gigantesques, mais la vitesse de calcul diminue. Les nombres memorises sous le type long occupent une place variable dans la memoire de l'ordinateur, en fonction de leur taille. Gerard Swinnen : Apprendre a programmer avec Python 43. 5.1.2 Le type « float » Vous avez deja rencontre precedemment cet autre type de donnee numerique : le type « nombre reel », ou « nombre a virgule flottante », designe en anglais par l'expression « floating point number », et que pour cette raison on appellera type float sous Python. Ce type autorise les calculs sur de tres grands ou tres petit* nombres (donnees scientifiques, par exemple), avec un degre de precision constant. Pour qu'une donnee numerique soit consideree par Python comme etant du type float, il suffit qu'elle contienne dans sa formulation un element tel qu'un point decimal ou un exposant de 10. Par exemple, les donnees : 3.14 10. .001 lelOO 3.14e-10 sont automatiquement interpretees par Python comme etant du type float. Essayons done ce type de donnees dans un nouveau petit programme (inspire du precedent) : >» a, b, c = 1 . , 2 . , 1 # => a et b seront du type 'float' »> while c <18: ... a, b, c = b, b*a, c+1 print b 2.0 4.0 8.0 32.0 256.0 8192.0 2097152.0 17179869184.0 3.6028797019e+16 6.18970019643e+26 2.23007451985e+43 1.38034926936e+70 3.07828173409e+113 4.24910394253e+183 1.30799390526e+297 Inf Inf Comme vous l'aurez certainement bien compris, nous affichons cette fois encore une serie dont les termes augmentent extremement vite, chacun d'eux etant egal au produit des deux precedents. Au huitieme terme, nous depassons deja largement la capacite d'un integer. Au neuvieme terme, Python passe automatiquement a la notation scientifique (« e+n » signifie en fait : « fois dix a l'exposant n »). Apres le quinzieme terme, nous assistons a nouveau a un depassem*nt de capacite (sans message d'erreur) : les nombres vraiment trop grands sont tout simplement notes « inf » (pour « infini »). Le type float utilise dans notre exemple permet de manipuler des nombres (positifs ou negatifs) compris entre 10" 308 et 10 308 avec une precision de 12 chiffres significatifs. Ces nombres sont encodes d'une maniere particuliere sur 8 octets (64 bits) dans la memoire de la machine : une partie du code correspond aux 12 chiffres significatifs, et une autre a l'ordre de grandeur (exposant de 10). 44. Gerard Swinnen : Apprendre a programmer avec Python (5) Exercices : 5.1. Ecrivez un programme qui convertisse en radians un angle fourni au depart en degres, minutes, secondes. 5.2. Ecrivez un programme qui convertisse en degres, minutes, secondes un angle fourni au depart en radians. 5.3. Ecrivez un programme qui convertisse en degres Celsius une temperature exprimee au depart en degres Fahrenheit, ou l'inverse. La formule de conversion est : T F — T c X 1,8 + 32 . 5.4. Ecrivez un programme qui calcule les interets accumules chaque annee pendant 20 ans, par capitalisation d'une somme de 100 euros placee en banque au taux fixe de 4,3 % 5.5. Une legende de l'lnde ancienne raconte que le jeu d'echecs a ete invente par un vieux sage, que son roi voulut remercier en lui affirmant qu'il lui accorderait n'importe quel cadeau en recompense. Le vieux sage demanda qu'on lui fournisse simplement un peu de riz pour ses vieux jours, et plus precisem*nt un nombre de grains de riz suffisant pour que Ton puisse en deposer 1 seul sur la premiere case du jeu qu'il venait d'inventer, deux sur la suivante, quatre sur la troisieme, et ainsi de suite jusqu'a la 64 e case. Ecrivez un programme Python qui affiche le nombre de grains a deposer sur chacune des 64 cases du jeu. Calculez ce nombre de deux manieres : - le nombre exact de grains (nombre entier) - le nombre de grains en notation scientifique (nombre reel) Gerard Swinnen : Apprendre a programmer avec Python 45. 5.2 Les donnees alphanumeriques Jusqu'a present nous n'avons manipule que des nombres. Mais un programme d'ordinateur peut egalement traiter des caracteres alphabetiques, des mots, des phrases, ou des suites de symboles quelconques. Dans la plupart des langages de programmation, il existe pour cet usage une structure de donnees que Ton appelle chaine de caracteres (ou string en anglais). 5.2.1 Le type « string » (chaine de caracteres) Sous Python, une donnee de type string est une suite quelconque de caracteres delimitee soit par des apostrophes (simple quotes), soit par des guillemets (double quotes). Exemples : >>> phrasel = ' les oeuf s durs . ' »> phrase2 = '"Oui", repondit-il, ' >>> phrase3 = "j'aime bien" >>> print phrase2, phrase3, phrasel "Oui", repondit-il, j'aime bien les oeufs durs. Les 3 variables phrasel, phrase2, phrase3 sont done des variables de type string. Remarquez l'utilisation des guillemets pour delimiter une chaine dans laquelle il y a des apostrophes, ou l'utilisation d'apostrophes pour delimiter une chaine qui contient des guillemets. Remarquez aussi encore une fois que l'instruction print insere un espace entre les elements affiches. Le caractere special « \ » (antislash) permet quelques subtilites complementaires : • En premier lieu, il permet d'ecrire sur plusieurs lignes une commande qui serait trop longue pour tenir sur une seule (cela vaut pour n'importe quel type de commande). • A l'interieur d'une chaine de caracteres, Yantislash permet d'inserer un certain nombre de codes speciaux (sauts a la ligne, apostrophes, guillemets, etc.). Exemples : »> txt3 = "'N\'est-ce pas ?" repondit-elle. ' »> print txt3 "N'est-ce pas ?" repondit-elle. »> Salut = "Ceci est une chaine plutot longue\n contenant plusieurs lignes \ ... de texte (Ceci fonctionne\n de la meme facon en C/C++. \n\ ... Notez que les blancs en debut\n de ligne sont signif icatif s . \n" »> print Salut Ceci est une chaine plutot longue contenant plusieurs lignes de texte (Ceci fonctionne de la meme f aeon en C/C++ . Notez que les blancs en debut de ligne sont signif icatif s . Remarques : • La sequence \n dans une chaine provoque un saut a la ligne. • La sequence V permet d'inserer une apostrophe dans une chaine delimitee par des apostrophes • Rappelons encore ici que la casse est significative dans les noms de variables (II faut respecter scrupuleusem*nt le choix initial de majuscules ou minuscules). 46. Gerard Swinnen : Apprendre a programmer avec Python « Triple quotes » : Pour inserer plus aisem*nt des caracteres speciaux ou « exotiques » dans une chaine, sans faire usage de Vantislash, ou pour faire accepter Yantislash lui-meme dans la chaine, on peut encore delimiter la chaine a l'aide de triples guillemets ou de triples apostrophes : »> al = """ ... Usage: trucmuche [OPTIONS] ... { -h -H hote J I! II II »> print al Usage: trucmuche [OPTIONS] { -h -H hote } 5.2.2 Acces aux caracteres individuels d'une chaine Les chaines de caracteres constituent un cas particulier d'un type de donnees plus general que Ton appelle des donnees composites. Une donnee composite est une entite qui rassemble dans une seule structure un ensemble d'entites plus simples : dans le cas d'une chaine de caracteres, par exemple, ces entries plus simples sont evidemment les caracteres eux-memes. En fonction des circonstances, nous souhaiterons traiter la chaine de caracteres, tantot comme un seul objet, tantot comme une collection de caracteres distincts. Un langage de programmation tel que Python doit done etre pourvu de mecanismes qui permettent d'acceder separement a chacun des caracteres d'une chaine. Comme vous allez le voir, cela n'est pas bien complique : Python considere qu'une chaine de caracteres est un objet de la categorie des sequences, lesquelles sont des collections ordonnees d'elements. Cela signifie simplement que les caracteres d'une chaine sont toujours disposes dans un certain ordre. Par consequent, chaque caractere de la chaine peut etre designe par sa place dans la sequence, a l'aide d'un index. Pour acceder a un caractere bien determine, on utilise le nom de la variable qui contient la chaine, et on lui accole entre deux crochets l'index numerique qui correspond a la position du caractere dans la chaine. Attention, cependant : comme vous aurez l'occasion de le verifier par ailleurs, les donnees informatiques sont presque toujours numerotees d partir de zero (et non a partir de un). C'est le cas pour les caracteres d'une chaine. Exemple : »> ch = "Stephanie" »> print ch[0], ch[3] S p Gerard Swinnen : Apprendre a programmer avec Python 47. 5.2.3 Operations elementaires sur les chatnes Python integre de nombreuses fonctions qui permettent d'effectuer divers traitements sur les chaines de caracteres (conversions majuscules/minuscules, decoupage en chaines plus petites, recherche de mots, etc.). Nous approfondirons ce sujet un peu plus loin (voir page 121). Pour l'instant, nous pouvons nous contenter de savoir qu'il est possible d'acceder individuellement a chacun des caracteres d'une chaine, comme cela a ete explique ci-dessus. Sachons en outre que Ton peut aussi : • assembler plusieurs petites chaines pour en construire de plus grandes. Cette operation s'appelle concatenation et on la realise sous Python a l'aide de l'operateur + (Cet operateur realise done l'operation d'addition lorsqu'on l'applique a des nombres, et l'operation de concatenation lorsqu'on l'applique a des chaines de caracteres. Exemple : a = 'Petit poisson' b = ' deviendra grand' c = a + b print c petit poisson deviendra grand • determiner la longueur (e'est-a-dire le nombre de caracteres) d'une chaine, en faisant appel a la fonction integree len() : »> print len(c) 29 • Convertir en nombre veritable une chaine de caracteres qui represente un nombre. Exemple : »> ch = '8647' »> print ch + 45 ==> *** erreur *** on ne peut pas additionner une chaine et un nombre »> n = int(ch) »> print n + 65 8712 # OK : on peut additionner 2 nombres Dans cet exemple, la fonction integree int() convertit la chaine en nombre entier. II serait egalement possible de convertir une chaine en nombre reel a l'aide de la fonction float(). 48. Gerard Swinnen : Apprendre a programmer avec Python Exercices : 5.6. Ecrivez un script qui determine si une chaine contient ou non le caractere « e ». 5.7. Ecrivez un script qui compte le nombre d'occurrences du caractere « e » dans une chaine. 5.8. Ecrivez un script qui recopie une chaine (dans une nouvelle variable), en inserant des asterisques entre les caracteres. Ainsi par exemple, « gaston » devra devenir « g*a*s*t*o*n » 5.9. Ecrivez un script qui recopie une chaine (dans une nouvelle variable) en l'inversant. Ainsi par exemple, « zorglub » deviendra « bulgroz ». 5.10. En partant de l'exercice precedent, ecrivez un script qui determine si une chaine de caracteres donnee est un palindrome (c'est-a-dire une chaine qui peut se lire indifferemment dans les deux sens), comme par exemple « radar » ou « s.o.s ». Gerard Swinnen : Apprendre a programmer avec Python 49. 5.3 Les listes (premiere approche) Les chaines que nous avons abordees a la rubrique precedente constituaient un premier exemple de donnees composites, lesquelles sont utilisees pour regrouper de maniere structuree des ensembles de valeurs. Vous apprendrez progressivement a utiliser plusieurs autres types de donnees composites, parmi lesquelles les listes, les tuples et les dictionnaires. 17 Nous n'allons cependant aborder ici que le premier de ces trois types, et ce de facon assez sommaire. II s'agit la en effet d'un sujet fort vaste, sur lequel nous devrons revenir a plusieurs reprises. Sous Python, on peut definir une liste comme une collection d'elements separes par des virgules, V ensemble etant enferme dans des crochets. Exemple : »> jour = ['lundi', 'mardi', 'mercredi', 1800, 20.357, 'jeudi', 'vendredi'] >>> print jour ['lundi', 'mardi', 'mercredi', 1800, 20.357, 'jeudi', 'vendredi'] Dans cet exemple, la valeur de la variable jour est une liste. Comme on peut le constater dans l'exemple choisi, les elements qui constituent une liste peuvent etre de types varies. Dans cet exemple, en effet, les trois premiers elements sont des chaines de caracteres, le quatrieme element est un entier, le cinquieme un reel, etc. (Nous verrons plus loin qu'un element d'une liste peut lui-meme etre une liste !). A cet egard, le concept de liste est done assez different du concept de « tableau » {array) ou de « variable indicee » que Ton rencontre dans d'autres langages de programmation. Remarquons aussi que comme les chaines de caracteres, les listes sont des sequences, e'est-a-dire des collections ordonnees d'objets. Les divers elements qui constituent une liste sont en effet toujours disposes dans le meme ordre, et Ton peut done acceder a chacun d'entre eux individuellement si Ton connait son index dans la liste. Comme e'etait deja le cas pour les caracteres dans une chaine, il faut cependant retenir que la numerotation de ces index commence a partir de zero, et non a partir de un. Exemples : »> jour = ['lundi', 'mardi', 'mercredi', 1800, 20.357, 'jeudi', 'vendredi'] >>> print jour [2] mercredi >>> print jour [4] 20.357 A la difference de ce qui se passe pour les chaines, qui constituent un type de donnees non- modifiables (nous aurons plus loin diverses occasions de revenir la-dessus), il est possible de changer les elements individuels d'une liste : >>> print jour ['lundi', 'mardi', 'mercredi', 1800, 20.357, 'jeudi', 'vendredi'] »> jour [3] = jour [3] +47 >>> print jour ['lundi', 'mardi', 'mercredi', 1847, 20.357, 'jeudi', 'vendredi'] 17 Vous pourrez meme creer vos propres types de donnees composites, lorsque vous aurez assimile le concept de classe (voir page 152). 50. Gerard Swinnen : Apprendre a programmer avec Python On peut done remplacer certains elements d'une liste par d'autres, comme ci-dessous : »> jour [3] = 'Juillet' >» print jour ['lundi', 'mardi', 'mercredi', 'Juillet', 20.357, 'jeudi', 'vendredi'] La fonction integree len() , que nous avons deja rencontree a propos des chaines, s'applique aussi aux listes. Elle renvoie le nombre d'elements presents dans la liste : »> len(jour) 7 Une autre fonction integree permet de supprimer d'une liste un element quelconque (a partir de son index). II s'agit de la fonction del() 18 : »> del (jour [4] ) >» print jour [ ' lundi ' , ' mardi ' , ' mercredi ' , ' juillet ' , ' jeudi ' , ' vendredi ' ] II est egalement tout a fait possible d'ajouter un element a une liste, mais pour ce faire, il faut considerer que la liste est un objet, dont on va utiliser l'une des methodes. Les concepts informatiques d'objet et de methode ne seront expliques qu'un peu plus loin dans ces notes, mais nous pouvons des a present montrer « comment 9a marche » dans le cas particulier d'une liste : »> jour . append (' samedi ' ) »> print jour [ ' lundi ' , ' mardi ' , ' mercredi ' , ' juillet ' , ' jeudi ' , ' vendredi ' , ' samedi ' ] »> Dans la premiere ligne de l'exemple ci-dessus, nous avons applique la methode append() a I'objet jour , avec I'argument 'samedi'. Si Ton se rappelle que le mot append signifie « ajouter » en anglais, on peut comprendre que la methode append() est une sorte de fonction qui est en quelque maniere attachee ou integree aux objets du type « liste ». L'argument que Ton utilise avec cette fonction est bien entendu l'element que Ton veut ajouter a la fin de la liste. Nous verrons plus loin qu'il existe ainsi toute une serie de ces methodes (e'est-a-dire des fonctions integrees, ou plutot « encapsulees » dans les objets de type « liste »). Notons simplement au passage que Yon applique une methode a un objet en reliant les deux a Vaide d'un point. (D'abord le nom de la variable qui reference I'objet, puis le point, puis le nom de la methode, cette derniere toujours accompagnee d'une paire de parentheses). 18 II existe en fait tout un ensemble de techniques qui permettent de decouper une liste en tranches, d'y inserer des groupes d'elements, d'en enlever d'autres, etc., en utilisant une syntaxe particuliere ou n'interviennent que les index. Cet ensemble de techniques (qui peuvent aussi s'appliquer aux chaines de caracteres) porte le nom generique de slicing (tranchage). On le met en oeuvre en placant plusieurs indices au lieu d'un seul entre les crochets que Ton accole au nom de la variable. Ainsi jour[l:3] designe le sous-ensemble ['mardi', 'mercredi']. Ces techniques un peu particulieres sont decrites plus loin (voir pages 121 et suivantes). Gerard Swinnen : Apprendre a programmer avec Python 51. Comme les chaines de caracteres, les listes seront approfondies plus loin dans ces notes (voir page 132). Nous en savons cependant assez pour commencer a les utiliser dans nos programmes. Veuillez par exemple analyser le petit script ci-dessous et commenter son fonctionnement : jour = [ ' dimanche ' , ' lundi ' , 'mardi ' , 'mercredi ' , ' jeudi ' , ' vendredi ' , ' samedi ' ] a, b = 0, 0 while a<25: a = a + 1 b = a % 7 print a, jourfb] La 5 e ligne de cet exemple fait usage de l'operateur « modulo » deja rencontre precedemment et qui peut rendre de grands services en programmation. On le represente par % dans de nombreux langages (dont Python). Quelle est l'operation effectuee par cet operateur ? Exercices : 5.1 1. Soient les listes suivantes : tl = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] t2 = ['Janvier', 'Fevrier', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Aout', 'Septembre', 'Octobre', 'Novembre' , 'Decembre'] Ecrivez un petit programme qui cree une nouvelle liste t3. Celle-ci devra contenir tous les elements des deux listes en les alternant, de telle maniere que chaque nom de mois soit suivi du nombre de jours COrrespondant : [ 'Janvier' ,31, 'Fevrier' ,28, 'Mars' ,31, etc. . . ] . 5.12. Ecrivez un programme qui affiche « proprement » tous les elements d'une liste. Si on l'appliquait par exemple a la liste t2 de l'exercice ci-dessus, on devrait obtenir : Janvier Fevrier Mars Avril Mai Juin Juillet Aout Septembre Octobre Novembre Decembre 5.13. Ecrivez un programme qui recherche le plus grand element present dans une liste donnee. Par exemple, si on l'appliquait a la liste [32, 5, 12, 8, 3, 75, 2, 15] , ce programme devrait afficher : le plus grand element de cette liste a la valeur 75. 5.14. Ecrivez un programme qui analyse un par un tous les elements d'une liste de nombres (par exemple celle de l'exercice precedent) pour generer deux nouvelles listes. L'une contiendra seulement les nombres pairs de la liste initiale, et l'autre les nombres impairs. Par exemple, si la liste initiale est celle de l'exercice precedent, le programme devra construire une liste pairs qui contiendra [32, 12, 8, 2], et une liste impairs qui contiendra [5, 3, 75, 15] . Astuce : pensez a utiliser l'operateur modulo (%) deja cite precedemment. 5.15. Ecrivez un programme qui analyse un par un tous les elements d'une liste de mots (par exemple : ['Jean', 'Maximilien', 'Brigitte', 'Sonia', 'Jean-Pierre', 'Sandra'] pour generer deux nouvelles listes. L'une contiendra les mots comportant moins de 6 caracteres, l'autre les mots comportant 6 caracteres ou davantage. 52. Gerard Swinnen : Apprendre a programmer avec Python Chapitre 6 : Fonctions predefinies L'un des concepts les plus importants en programmation est celui de fonction 19 . Les fonctions permettent en effet de decomposer un programme complexe en une serie de sous-programmes plus simples, lesquels peuvent a leur tour etre decomposes eux-memes en fragments plus petit*, et ainsi de suite. D'autre part, les fonctions sont reutilisables : si nous disposons d'une fonction capable de calculer une racine carree, par exemple, nous pouvons l'utiliser un peu partout dans nos programmes sans avoir a la re-ecrire a chaque fois. 6. 1 Interaction avec I'utilisateur : la fonction inputQ La plupart des scripts elabores necessitent a un moment ou l'autre une intervention de I'utilisateur (entree d'un parametre, clic de souris sur un bouton, etc.). Dans un script simple en mode texte (comme ceux que nous avons crees jusqu'a present), la methode la plus simple consiste a employer la fonction integree input(). Cette fonction provoque une interruption dans le programme courant. L'utilisateur est invite a entrer des caracteres au clavier et a terminer avec . Lorsque cette touche est enfoncee, l'execution du programme se poursuit, et la fonction fournit en retour une valeur correspondant a ce que I'utilisateur a entre. Cette valeur peut alors etre assignee a une variable quelconque. On peut invoquer la fonction input() en laissant les parentheses vides. On peut aussi y placer en argument un message explicatif destine a I'utilisateur. Exemple : print 'Veuillez entrer un nombre posit if quelconque : ' , nn = input ( ) print 'Le carre de ' , nn, 'vaut', nn**2 ou encore : prenom = input (' Entrez votre prenom (entre guillemets) : ') print 'Bon jour, ', prenom Remarques importantes : • La fonction input() renvoie une valeur dont le type correspond a ce que I'utilisateur a entre. Dans notre exemple, la variable nn contiendra done un entier, une chaine de caracteres, un reel, etc. suivant ce que I'utilisateur aura decide. Si I'utilisateur souhaite entrer une chaine de caracteres, il doit l'entrer comme telle, e'est-a-dire incluse entre des apostrophes ou des guillemets. Nous verrons plus loin qu'un bon script doit toujours verifier si le type ainsi entre correspond bien a ce que Ton attend pour la suite du programme. • Pour cette raison, il sera souvent preferable d'utiliser dans vos scripts la fonction similaire raw_input(), laquelle renvoie toujours une chaine de caracteres. Vous pouvez ensuite convertir cette chaine en nombre a l'aide de int() ou de float(). Exemple : »> a = raw_input ( ' Entrez une donnee : ' ) Entrez une donnee : 52 . 37 >» type (a) »> b = float (a) # conversion en valeur numerique »> type (b) 19 Sous Python, le terme de "fonction" est utilise indifferemment pour designer a la fois de veritables fonctions mais egalement des procedures. Nous indiquerons plus loin la distinction entre ces deux concepts proches. Gerard Swinnen : Apprendre a programmer avec Python 53. 6.2 Importer un module de fonctions Vous avez deja rencontre des fonctions integrees au langage lui-meme, comme la fonction len(), par exemple, qui permet de connaitre la longueur d'une chaine de caracteres. II va de soi cependant qu'il n'est pas possible d'integrer toutes les fonctions imaginables dans le corps standard de Python, car il en existe virtuellement une infinite : vous apprendrez d'ailleurs tres bientot comment en creer vous-meme de nouvelles. Les fonctions integrees au langage sont relativement peu nombreuses : ce sont seulement celles qui sont susceptibles d'etre utilisees tres frequemment. Les autres sont regroupees dans des fichiers separes que Ton appelle des modules. Les modules sont done des fichiers qui regroupent des ensembles de fonctions. Vous verrez plus loin comme il est commode de decouper un programme important en plusieurs fichiers de taille modeste pour en faciliter la maintenance. Une application Python typique sera alors constitute d'un programme principal accompagne de un ou plusieurs modules contenant chacun les definitions d'un certain nombre de fonctions accessoires. II existe un grand nombre de modules pre -programmes qui sont fournis d'office avec Python. Vous pouvez en trouver d' autres chez divers fournisseurs. Souvent on essaie de regrouper dans un meme module des ensembles de fonctions apparentees que Ton appelle des bibliotheques. Le module math, par exemple, contient les definitions de nombreuses fonctions mathematiques telles que sinus, cosinus, tangente, racine carree, etc. Pour pouvoir utiliser ces fonctions, il vous suffit d'incorporer la ligne suivante au debut de votre script : from math import * Cette ligne indique a Python qu'il lui faut inclure dans le programme courant toutes les fonctions (e'est la la signification du symbole *) du module math, lequel contient une bibliotheque de fonctions mathematiques pre-programmees. Dans le corps du script lui-meme, vous ecrirez par exemple : racine = sqrt (nombre) pour assigner a la variable racine la racine carree de nombre, sinusx = sin (angle) pour assigner a la variable sinusx le sinus de angle (en radians !), etc. Exemple : # Demo : utilisation des fonctions du module from math import * nombre = 121 angle = pi/6 # soit 30° (la bibliotheque math inclut aussi la definition de pi) print 'racine carree de ' , nombre, '=', sqrt (nombre) print 'sinus de ' , angle, 'radians', '=', sin (angle) L'execution de ce script provoque l'affichage suivant : racine carree de 121 = 11.0 sinus de 0.523598775598 radians = 0.5 54. Gerard Swinnen : Apprendre a programmer avec Python Ce court exemple illustre deja fort bien quelques caracteristiques importantes des fonctions : ♦ une fonction apparait sous la forme d'un nom quelconque associe a des parentheses exemple : sqrt ( ) ♦ dans les parentheses, on transmet a la fonction un ou plusieurs arguments exemple : sqrt (121) ♦ la fonction fournit une valeur de retour (on dira aussi qu'elle « renvoie » une valeur) exemple : 11 . 0 Nous allons developper tout ceci dans les pages suivantes. Veuillez noter au passage que les fonctions mathematiques utilisees ici ne represented qu'un tout premier exemple. Un simple coup d'ceil dans la documentation des bibliotheques Python vous permettra de constater que de tres nombreuses fonctions sont d'ores et deja disponibles pour realiser une multitude de taches, y compris des algorithmes mathematiques tres complexes (Python est couramment utilise dans les universites pour la resolution de problemes scientifiques de haut niveau). II est done hors de question de fournir ici une liste detaillee. Une telle liste est aisem*nt accessible dans le systeme d'aide de Python : Documentation HTML — > Python documentation — > Modules index — > math Au chapitre suivant, nous apprendrons comment creer nous-memes de nouvelles fonctions. (6) Exercices : (Note : Dans tous ces exercices, utilisez la fonction raw_input() pour I'entree des donnees) 6.1. Ecrivez un programme qui convertisse en metres par seconde et en km/h une vitesse fournie par l'utilisateur en miles/heure. (Rappel : 1 mile = 1609 metres) 6.2. Ecrivez un programme qui calcule le perimetre et l'aire d'un triangle quelconque dont l'utilisateur fournit les 3 cotes. {Rappel : l'aire d'un triangle quelconque se calcule a I'aide de la formule : 5 , = V d-(d — a)-(d — b)-(d — c) dans laquelle d designe la longueur du demi-perimetre, et a, b, c celles des trois cotes). 6.3. Ecrivez un programme qui calcule la periode d'un pendule simple de longueur donnee. La formule qui permet de calculer la periode d'un pendule simple est T = 2 n I representant la longueur du pendule et g la valeur de l'acceleration de la pesanteur au lieu d'experience. 6.4. Ecrivez un programme qui permette d'encoder des valeurs dans une liste. Ce programme devrait fonctionner en boucle, l'utilisateur etant invite a entrer sans cesse de nouvelles valeurs, jusqu'a ce qu'il decide de terminer en frappant en guise d'entree. Le programme se terminerait alors par l'affichage de la liste. Exemple de fonctionnement : Veuillez entrer une valeur : 25 Veuillez entrer une valeur : 18 Veuillez entrer une valeur : 6284 Veuillez entrer une valeur : [25, 18, 6284] Gerard Swinnen : Apprendre a programmer avec Python 55. 6.3 Un peu de detente avec le module turtle Comme nous venons de le voir, l'une des grandes qualites de Python est qu'il est extremement facile de lui ajouter de nombreuses fonctionnalites par importation de divers modules. Pour illustrer cela, et nous amuser un peu avec d'autres objets que des nombres, nous allons explorer un module Python qui permet de realiser des « graphiques tortue », c'est-a-dire des dessins geometriques correspondant a la piste laissee derriere elle par une petite « tortue » virtuelle, dont nous controlons les deplacements sur l'ecran de l'ordinateur a l'aide d'instructions simples. Activer cette tortue est un vrai jeu d' enfant. Pluto t que de vous dormer de longues explications, nous vous invitons a essayer tout de suite : >>> from turtle import * »> forward (120) »> left (90) »> color ( ' red' ) >» forward (80) L'exercice est evidemment plus riche si Ton utilise des boucles : »> reset () »> a = 0 »> while a <12 : a = a +1 forward (150) left (150) Attention cependant : avant de lancer un tel script, assurez-vous toujours qu'il ne comporte pas de boucle sans fin (voir page 36), car si c'est le cas vous risquez de ne plus pouvoir reprendre le controle des operations (en particulier sous Windows). Amusez-vous a ecrire des scripts qui realisent des dessins suivant un modele impose a l'avance. Les principales fonctions mises a votre disposition dans le module turtle sont les suivantes : reset() goto(x, y) forward(distance) backward(distance) up() down() color(couleur) left(angle) right(angle) width(epaisseur) ffll(l) write(texte) On efface tout et on recommence Aller a l'endroit de coordonnees x, y Avancer d'une distance donnee Reader Relever le crayon (pour pouvoir avancer sans dessiner) Abaisser le crayon(pour recommencer a dessiner) peut etre une chaine predefinie ('red', 'blue', 'green', etc.) Tourner a gauche d'un angle donne (exprime en degres) Tourner a droite Choisir l'epaisseur du trace Remplir un contour ferme a l'aide de la couleur selectionnee doit etre une chaine de caracteres delimitee avec des " ou des 56. Gerard Swinnen : Apprendre a programmer avec Python 6.4 Veracite/faussete d'une expression Lorsqu'un programme contient des instructions telles que while ou if, l'ordinateur qui execute ce programme doit evaluer la veracite d'une condition, c'est-a-dire determiner si une expression est vraie ou fausse. Par exemple, une boucle initiee par while c<20: s'executera aussi longtemps que la condition c<20 restera vraie. Mais comment un ordinateur peut-il determiner si quelque chose est vrai ou faux ? En fait - et vous le savez deja - un ordinateur ne manipule strictement que des nombres. Tout ce qu'un ordinateur doit traiter doit d'abord toujours etre converti en valeur numerique. Cela s'applique aussi a la notion de vrai/faux. En Python, tout comme en C, en Basic et en de nombreux autres langages de programmation, on considere que toute valeur numerique autre que zero est « vraie ». Seule la valeur zero est « fausse ». Exemple : a = input ( 'Entrez une valeur quelconque ' ) if a: print "vrai" else : print "faux" Le petit script ci-dessus n'affiche « faux » que si vous entrez la valeur 0. Pour toute autre valeur numerique, vous obtiendrez « vrai ». Si vous entrez une chaine de caracteres ou une liste, vous obtiendrez encore « vrai ». Seules les chaines ou les listes vides seront considerees comme « fausses ». Tout ce qui precede signifie done qu'une expression a evaluer, telle par exemple la condition a > 5 , est d'abord convertie par l'ordinateur en une valeur numerique. (Generalement 1 si l'expression est vraie, et zero si l'expression est fausse). Exemple : a = input (' entrez une valeur numerique : ') b = (a < 5) print 'la valeur de b est', b, ' : ' if b: print "la condition b est vraie" else : print "la condition b est fausse" Le script ci-dessus vous renvoie une valeur b = 1 (condition vraie) si vous avez entre un nombre plus petit que 5. Ces explications ne sont qu'une premiere information a propos d'un systeme de representation des operations logiques que Ton appelle algebre de Boole. Vous apprendrez plus loin que Ton peut appliquer aux nombres binaires des operateurs tels que and, or, not, etc. qui permettent d'effectuer a l'aide de ces nombres des traitements logiques complexes. Gerard Swinnen : Apprendre a programmer avec Python 57. 6.5 Revision Dans ce qui suit, nous n'allons pas apprendre de nouveaux concepts mais simplement utiliser tout ce que nous connaissons deja pour realiser de vrais petit* programmes. 6.5.1 Controle du flux - Utilisation d'une liste simple Commencons par un petit retour sur les branchements conditionnels (il s'agit peut-etre la du groupe d' instructions le plus important dans n'importe quel langage !) : # Utilisation d'une liste et de branchements conditionnels print "Ce script recherche le plus grand de trois nombres" print 'Veuillez entrer trois nombres separes par des virgules : ' # Note : la fonction list() convertit en liste la sequence de donnees qu'on # lui fournit en argument. L ' instruction ci-dessous convertira done les # donnees fournies par 1 ' utilisateur en une liste nn : nn = list (input () ) max, index = nn[0], 'premier' if nn[l] > max: # ne pas omettre le double point ! max = nn [ 1 ] index = ' second ' if nn[2] > max: max = nn [ 2 ] index = 'troisieme' print "Le plus grand de ces nombres est", max print "Ce nombre est le", index, "de votre liste." Note : Dans cet exercice, vous retrouvez a nouveau le concept de « bloc d'instructions », deja abondamment commente aux chapitres 3 et 4, et que vous devez absolument assimiler. Pour rappel, les blocs d'instructions sont delimites par I 'indentation. Apres la premiere instruction if, par exemple, il y a deux lignes indentees definissant un bloc d'instructions. Ces instructions ne seront executees que si la condition nn[l] > max est vraie. La ligne suivante, par contre (celle qui contient la deuxieme instruction if) n'est pas indentee. Cette ligne se situe done au meme niveau que celles qui definissent le corps principal du programme. L'instruction contenue dans cette ligne est done toujours executee, alors que les deux suivantes (qui constituent encore un autre bloc) ne sont executees que si la condition nn[2] > max est vraie. En suivant la meme logique, on voit que les instructions des deux dernieres lignes font partie du bloc principal et sont done toujours executees. 58. Gerard Swinnen : Apprendre a programmer avec Python 6.5.2 Boucle while - Instructions imbriquees Continuons dans cette voie en imbriquant d'autres structures # Instructions composees - - - # 1 print 'Choisissez un nombre de 1 a 3 (ou zero pour terminer) ', # 3 a = input ( ) # 4 while a != 0: # l'operateur != signifie "different de" # 5 if a == 1: # 6 print "Vous avez choisi un : " # 7 print "le premier, 1' unique, 1' unite ..." # 8 elif a == 2: # 9 print "Vous preferez le deux : " # 10 print "la paire, le couple, le duo ..." # 11 elif a == 3: # 12 print "Vous optez pour le plus grand des trois : " # 13 print "le trio, la trinite, le triplet ..." # 14 else : # 15 print "Un nombre entre UN et TROIS, s.v.p." # 16 print 'Choisissez un nombre de 1 a 3 (ou zero pour terminer) ', # 17 a = input ( ) # 18 print "Vous avez entre zero : " # 19 print "L'exercice est done termine . " # 20 Nous retrouvons ici une boucle while, associee a un groupe destructions if, elif et else. Notez bien cette fois encore comment la structure logique du programme est creee a l'aide des indentations (... et n'oubliez pas le caractere « : » a la fin de chaque ligne d'en-tete !) L'instruction while est utilisee ici pour relancer le questionnement apres chaque reponse de l'utilisateur (du moins jusqu'a ce que celui-ci decide de « quitter » en entrant une valeur nulle : rappelons a ce sujet que l'operateur de comparaison != signifie « est different de »). Dans le corps de la boucle, nous trouvons le groupe d'instructions if, elif et else (de la ligne 6 a la ligne 16), qui aiguille le flux du programme vers les differentes reponses, ensuite une instruction print et une instruction input() (lignes 17 & 18) qui seront executees dans tous les cas de figure : notez bien leur niveau d'indentation, qui est le meme que celui du bloc if, elif et else, Apres ces instructions, le programme boucle et l'execution reprend a l'instruction while (ligne 5). Les deux dernieres instructions print (lignes 19 & 20) ne sont executees qu'a la sortie de la boucle. Gerard Swinnen : Apprendre a programmer avec Python 59. Exercices 6.5. Que fait le programme ci-dessous, dans les quatre cas ou Ton aurait defini au prealable que la variable a vaut 1, 2, 3 ou 15 ? if a !=2: print 'perdu' elif a ==3: print 'un instant, s.v.p.' else : print ' gagne ' 6.6. Que font ces programmes ? a) a = 5 b = 2 if (a==5) & (b<2) : print '"&" signifie "et" ; on peut aussi utiliser le mot "and"' b) a, b = 2, 4 if (a==4) or (b!=4) : print ' gagne ' elif (a==4) or (b==4) : print 'presque gagne' c) a = 1 if not a : print ' gagne ' elif a: print 'perdu' 6.7. Reprendre le programme c) avec a = 0 au lieu de a = 1. Que se passe-t-il ? Cone lure ! 6.8. Ecrire un programme qui, etant donnees deux bornes entieres a et b, additionne les nombres multiples de 3 et de 5 compris entre ces bornes. Prendre par exemple a = 0, b = 32 — > le resultat devrait etre alors 0 + 15 + 30 = 45. Modifier legerement ce programme pour qu'il additionne les nombres multiples de 3 ou de 5 compris entre les bornes a et b. Avec les bornes 0 et 32, le resultat devrait done etre : 0 + 3 + 5 + 6 + 9 + 10 + 12 + 15 + 18 + 20 + 21 + 24 + 25 + 27 + 30 = 225. 6.9. Determiner si une annee (dont le millesime est introduit par l'utilisateur) est bissextile ou non. (Une annee A est bissextile si A est divisible par 4. Elle ne Test cependant pas si A est un multiple de 100, a moins que A ne soit multiple de 400). 6.10. Demander a l'utilisateur son nom et son sexe (M ou F). En fonction de ces donnees, afficher « Cher Monsieur » ou « Chere Mademoiselle » suivi du nom de l'eleve. 6.11. Demander a l'utilisateur d'entrer trois longueurs a, b, c. A l'aide de ces trois longueurs, determiner s'il est possible de construire un triangle. Determiner ensuite si ce triangle est rectangle, isocele, equilateral ou quelconque. Attention : un triangle rectangle peut etre isocele. 60. Gerard Swinnen : Apprendre a programmer avec Python 6.12. Demander a l'utilisateur qu'il entre un nombre. Afficher ensuite : soit la racine carree de ce nombre, soit un message indiquant que la racine carree de ce nombre ne peut etre calculee. 6.13. Convertir une note scolaire N quelconque, entree par l'utilisateur sous forme de points (par exemple 27 sur 85), en une note standardisee suivant le code suivant : Note Appreciation N >= 80 % A 80%>N>=60% B 60%>N>=50% C 50 % > N >= 40 % D N < 40 % E 6.14. Soit la liste suivante : ['Jean-Michel', 'Marc', 'Vanessa', Anne', Maximilien', Alexandre-Benoif, 'Louise'] Ecrivez un script qui affiche chacun de ces noms avec le nombre de caracteres correspondant. 6.15. Ecrire une boucle de programme qui demande a l'utilisateur d'entrer des notes d'eleves. La boucle se terminera seulement si l'utilisateur entre une valeur negative. Avec les notes ainsi entrees, construire progressivement une liste. Apres chaque entree d'une nouvelle note (et done a chaque iteration de la boucle), afficher le nombre de notes entrees, la note la plus elevee, la note la plus basse, la moyenne de toutes les notes. 6.16. Ecrivez un script qui affiche la valeur de la force de gravitation s'exercant entre deux masses de 10000 kg , pour des distances qui augmentent suivant une progression geometrique de raison 2, a partir de 5 cm (0,05 metre). n m-m' La force de gravitation est regie par la formule F — 6,61 10 a Exemple d'affichage : d = .05 m : la force vaut 2 668 N d = .1 m : la force vaut 0 667 N d = .2 m : la force vaut 0 167 N d = .4m : la force vaut 0 0417 N etc . Gerard Swinnen : Apprendre a programmer avec Python 61. Chapitre 7 : Fonctions originales La programmation est l'art d'apprendre a un ordinateur comment accomplir des taches qu'il n'etait pas capable de realiser auparavant. L'une des methodes les plus interessantes pour y arriver consiste a aj outer de nouvelles instructions au langage de programmation que vous utilisez, sous la forme de fonctions originales. 7. 1 Definir une fonction Les scripts que vous avez ecrits jusqu'a present etaient a chaque fois tres courts, car leur objectif etait seulement de vous faire assimiler les premiers elements du langage. Lorsque vous commencerez a developper de veritables projets, vous serez confrontes a des problemes souvent fort complexes, et les lignes de programme vont commencer a s'accumuler... L'approche efficace d'un probleme complexe consiste souvent a le decomposer en plusieurs sous- problemes plus simples qui seront etudies separement (Ces sous-problemes peuvent eventuellement etre eux-memes decomposes a leur tour, et ainsi de suite). Or il est important que cette decomposition soit representee fidelement dans les algorithmes 20 pour que ceux-ci restent clairs. D'autre part, il arrivera souvent qu'une meme sequence d'instructions doive etre utilisee a plusieurs reprises dans un programme, et on souhaitera bien evidemment ne pas avoir a la reproduire systematiquement. Les fonctions 21 et les classes d'objets sont differentes structures de sous-programmes qui ont ete imaginees par les concepteurs des langages de haut niveau afin de resoudre les difficultes evoquees ci-dessus. Nous allons commencer par decrire ici la definition de fonctions sous Python. Les objets et les classes seront examines plus loin. Nous avons deja rencontre diverses fonctions pre-programmees. Voyons a present comment en definir nous-memes de nouvelles. La syntaxe Python pour la definition d'une fonction est la suivante : def nomDeLaFonct ion (list e ds parametres) : bloc d'instructions • Vous pouvez choisir n'importe quel nom pour la fonction que vous creez, a l'exception des mots reserves du langage 22 , et a la condition de n'utiliser aucun caractere special ou accentue (le caractere souligne « _ » est permis). Comme c'est le cas pour les noms de variables, il vous est conseille d'utiliser surtout des lettres minuscules, notamment au debut du nom (les noms commen?ant par une majuscule seront reserves aux classes que nous etudierons plus loin). 20 On appelle algorithme la sequence detaillee de toutes les operations a effectuer pour resoudre un probleme. 21 II existe aussi dans d'autres langages des routines (parfois appeles sous-programmes) et des procedures. II n'existe pas de routines en Python. Quant au terme de fonction, il designe a la fois les fonctions au sens strict (qui fournissent une valeur en retour), et les procedures (qui n'en fournissent pas). 22 La liste complete des mots reserves Python se trouve page 22. 62. Gerard Swinnen : Apprendre a programmer avec Python • Comme les instructions if et while que vous connaissez deja, l'instruction def est une instruction composee. La ligne contenant cette instruction se termine obligatoirement par un double point, lequel introduit un bloc d'instructions que vous ne devez pas oublier d'indenter. • La liste de parametres specifie quelles informations il faudra fournir en guise d'arguments lorsque Ton voudra utiliser cette fonction (Les parentheses peuvent parfaitement rester vides si la fonction ne necessite pas d'arguments). • Une fonction s'utilise pratiquement comme une instruction quelconque. Dans le corps d'un programme, un appel de fonction est constitue du nom de la fonction suivi de parentheses. Si c'est necessaire, on place dans ces parentheses le ou les arguments que Ton souhaite transmettre a la fonction. II faudra en principe fournir un argument pour chacun des parametres specifies dans la definition de la fonction, encore qu'il soit possible de definir pour ces parametres des valeurs par defaut (voir plus loin). 7. 1. 1 Fonction simple sans parametres Pour notre premiere approche concrete des fonctions, nous allons travailler a nouveau en mode interactif. Le mode interactif de Python est en effet ideal pour effectuer des petit* tests comme ceux qui suivent. C'est une facilite que n'offrent pas tous les langages de programmation ! »> def table7 () : n = 1 while n <11 : print n * 7 , n = n +1 En entrant ces quelques lignes, nous avons defini une fonction tres simple qui calcule et affiche les 10 premiers termes de la table de multiplication par 7. Notez bien les parentheses 23 , le double point, et l'indentation du bloc d'instructions qui suit la ligne d'en-tete (c'est ce bloc d'instructions qui constitue le corps de la fonction proprement dite). Pour utiliser la fonction que nous venons de definir, il suffit de l'appeler par son nom. Ainsi : »> table7 () provoque l'affichage de : 7 14 21 28 35 42 49 56 63 70 23 Un nom de fonction doit toujours etre accompagne de parentheses, meme si la fonction n'utilise aucun parametre. II en resulte une convention d'ecriture qui stipule que dans un texte quelconque traitant de programmation d'ordinateur, un nom de fonction soit toujours accompagne d'une paire de parentheses vides. Nous respecterons cette convention dans la suite de ce texte. Gerard Swinnen : Apprendre a programmer avec Python 63. Nous pouvons maintenant reutiliser cette fonction a plusieurs reprises, autant de fois que nous le souhaitons. Nous pouvons egalement l'incorporer dans la definition d'une autre fonction, comme dans l'exemple ci-dessous : »> def table7triple () : . . . print 'La table par 7 en triple exemplaire : ' table7() table7() table7() Utilisons cette nouvelle fonction, en entrant la commande : »> table7triple () l'affichage resultant devrait etre : La table par 7 en triple exemplaire : 7 14 21 28 35 42 49 56 63 70 7 14 21 28 35 42 49 56 63 70 7 14 21 28 35 42 49 56 63 70 Une premiere fonction peut done appeler une deuxieme fonction, qui elle-meme en appelle une troisieme, etc. Au stade ou nous sommes, vous ne voyez peut-etre pas encore tres bien l'utilite de tout cela, mais vous pouvez deja noter deux proprietes interessantes : • Creer une nouvelle fonction vous offre l'opportunite de donner un nom a tout un ensemble d' instructions. De cette maniere, vous pouvez simplifier le corps principal d'un programme, en dissimulant un algorithme secondaire complexe sous une commande unique, a laquelle vous pouvez donner un nom tres explicite, en francais si vous voulez. • Creer une nouvelle fonction peut servir a raccourcir un programme, par elimination des portions de code qui se repetent. Par exemple, si vous devez afficher la table par 7 plusieurs fois dans un meme programme, vous n'avez pas a reecrire chaque fois l'algorithme qui accomplit ce travail. Une fonction est done en quelque sorte une nouvelle instruction personnalisee, que vous ajoutez vous-meme librement a votre langage de programmation. 64. Gerard Swinnen : Apprendre a programmer avec Python 7.1.2 Fonction avec para metre Dans nos derniers exemples, nous avons defini et utilise une fonction qui affiche les termes de la table par 7. Supposons a present que nous voulions faire de meme avec la table par 9. Nous pouvons bien entendu reecrire entierement une nouvelle fonction pour cela. Mais si nous nous interessons plus tard a la table par 13, il nous faudra encore recommencer. Ne serait-il done pas plus interessant de definir une fonction qui soit capable d'afficher n'importe quelle table, a la demande ? Lorsque nous appellerons cette fonction, nous devrons bien evidemment pouvoir lui indiquer quelle table nous souhaitons afficher. Cette information que nous voulons transmettre a la fonction au moment meme ou nous l'appelons s'appelle un argument. Nous avons deja rencontre a plusieurs reprises des fonctions integrees qui utilisent des arguments. La fonction sin(a), par exemple, calcule le sinus de Tangle a. La fonction sin() utilise done la valeur numerique de a comme argument pour effectuer son travail. Dans la definition d'une telle fonction, il faut prevoir une variable particuliere pour recevoir l'argument transmis. Cette variable particuliere s'appelle un parametre. On lui choisit un nom en respectant les memes regies de syntaxe que d'habitude (pas de lettres accentuees, etc.), et on place ce nom entre les parentheses qui accompagnent la definition de la fonction. Voici ce que cela donne dans le cas qui nous interesse : »> def table (base) : n = 1 while n <11 : print n * base, n = n +1 La fonction table() telle que definie ci-dessus utilise le parametre base pour calculer les dix premiers termes de la table de multiplication correspondante. Pour tester cette nouvelle fonction, il nous suffit de l'appeler avec un argument. Exemples : »> table (13) 13 26 39 52 65 78 91 104 117 130 »> table (9) 9 18 27 36 45 54 63 72 81 90 Dans ces exemples, la valeur que nous indiquons entre parentheses lors de l'appel de la fonction (et qui est done un argument) est automatiquement affectee au parametre base. Dans le corps de la fonction, base joue le meme role que n'importe quelle autre variable. Lorsque nous entrons la commande table(9), nous signifions a la machine que nous voulons executer la fonction table() en affectant la valeur 9 a la variable base. Gerard Swinnen : Apprendre a programmer avec Python 65. 7.1.3 Utilisation d'une variable comme argument Dans les 2 exemples qui precedent, l'argument que nous avons utilise en appelant la fonction table() etait a chaque fois une constante (la valeur 13, puis la valeur 9). Cela n'est nullement obligatoire. L'argument que nous utilisons dans I'appel d'une fonction peut etre une variable lui aussi, comme dans l'exemple ci-dessous. Analysez bien cet exemple, essayez-le concretement, et decrivez le mieux possible dans votre cahier d'exercices ce que vous obtenez, en expliquant avec vos propres mots ce qui se passe. Cet exemple devrait vous donner un premier apercu de l'utilite des fonctions pour accomplir simplement des taches complexes : »> a = 1 >» while a <20: table (a) a = a +1 Remarque importante : Dans l'exemple ci-dessus, l'argument que nous passons a la fonction table() est le contenu de la variable a . A l'interieur de la fonction, cet argument est affecte au parametre base, qui est une tout autre variable. Notez done bien des a present que : Le nom d'une variable que nous passons comme argument n'a rien a voir avec le nom du parametre correspondant dans la fonction. Ces noms peuvent etre identiques si vous le voulez, mais vous devez bien comprendre qu'ils ne designent pas la meme chose (en depit du fait qu'ils puissent contenir une valeur identique). (7) Exercice : 7.1. Importez le module turtle pour pouvoir effectuer des dessins simples. Vous allez dessiner une serie de triangles equilateraux de differentes couleurs. Pour ce faire, definissez d'abord une fonction triangle() capable de dessiner un triangle d'une couleur bien determinee (ce qui signifie done que la definition de votre fonction doit comporter un parametre pour recevoir le nom de cette couleur) Utilisez ensuite cette fonction pour reproduire ce meme triangle en differents endroits, en changeant de couleur a chaque fois. 66. Gerard Swinnen : Apprendre a programmer avec Python 7.1.4 Fonction avec plusieurs parametres La fonction table() est certainement interessante, mais elle n'affiche toujours que les dix premiers termes de la table de multiplication, alors que nous pourrions souhaiter qu'elle en affiche d'autres. Qu'a cela ne tienne. Nous allons l'ameliorer en lui ajoutant des parametres supplementaires, dans une nouvelle version que nous appellerons cette fois tableMulti() : »> def tableMulti (base, debut, fin): ... print 'Fragment de la table de multiplication par', base, ':' ... n = debut while n <= fin : ... print n, 'x', base, '=', n * base n = n +1 Cette nouvelle fonction utilise done trois parametres : la base de la table comme dans l'exemple precedent, l'indice du premier terme a afficher, l'indice du dernier terme a afficher. Essayons cette fonction en entrant par exemple : »> tableMulti (8, 13, 17) ce qui devrait provoquer l'affichage de : Fragment de la table de multiplication par 8 : 13 x 8 = 104 14 x 8 = 112 15 x 8 = 120 16 x 8 = 128 17 x 8 = 136 Notes : • Pour definir une fonction avec plusieurs parametres, il suffit d'inclure ceux-ci entre les parentheses qui suivent le nom de la fonction, en les separant a l'aide de virgules. • Lors de l'appel de la fonction, les arguments utilises doivent etre fournis dans le meme ordre que celui des parametres correspondants (en les separant eux aussi a l'aide de virgules). Le premier argument sera affecte au premier parametre, le second argument sera affecte au second parametre, et ainsi de suite. • A titre d'exercice, essayez la sequence d'instructions suivantes et decrivez dans votre cahier d'exercices le resultat obtenu : »> t, d, f = n, 5, 10 »> while t<21: tableMulti (t , d, f ) t, d, f = t +1, d +3, f +5 Gerard Swinnen : Apprendre a programmer avec Python 67. 7.2 Variables locales, variables globales Lorsque nous definissons des variables a l'interieur du corps d'une fonction, ces variables ne sont accessibles qu'a la fonction elle-meme. On dit que ces variables sont des variables locales a la fonction. C'est par exemple le cas des variables base, debut, fin et n dans l'exercice precedent. Chaque fois que la fonction tableMulti() est appelee, Python reserve pour elle (dans la memoire de l'ordinateur) un nouvel espace de noms 24 . Les contenus des variables base, debut, fin et n sont stockes dans cet espace de noms qui est inaccessible depuis l'exterieur de la fonction. Ainsi par exemple, si nous essayons d'afficher le contenu de la variable base juste apres avoir effectue l'exercice ci-dessus, nous obtenons un message d'erreur : »> print base Traceback (innermost last) : File "", line 1, in ? print base NameError : base La machine nous signale clairement que le symbole base lui est inconnu, alors qu'il etait correctement imprime par la fonction tableMulti() elle-meme. L'espace de noms qui contient le symbole base est strictement reserve au fonctionnement interne de tableMulti(), et il est automatiquement detruit des que la fonction a termine son travail. Les variables definies a l'exterieur d'une fonction sont des variables globales. Leur contenu est « visible » de l'interieur d'une fonction, mais la fonction ne peut pas le modifier. Exemple : »> def mask () : p = 20 print p, q »> p, q = 15, 38 »> mask() 20 38 »> print p, q 15 38 Analysons attentivement cet exemple : Nous commencons par definir une fonction tres simple (qui n'utilise d'ailleurs aucun parametre). A l'interieur de cette fonction, une variable p est definie, avec 20 comme valeur initiale. Cette variable p qui est definie a l'interieur d'une fonction sera done une variable locale. Une fois terminee la definition de la fonction, nous revenons au niveau principal pour y definir les deux variables p et q auxquelles nous attribuons les contenus 15 et 38. Ces deux variables definies au niveau principal seront done des variables globales. Ainsi le meme nom de variable p a ete utilise ici a deux reprises, pour definir deux variables differentes : l'une est globale et l'autre est locale. On peut constater dans la suite de l'exercice que ces deux variables sont bel et bien des variables distinctes, independantes, obeissant a une regie de priorite qui veut qu'a l'interieur d'une fonction (ou elles pourraient entrer en competition), ce sont les variables definies localement qui ont la priorite. 24 Ce concept d'espace de noms sera approfondi progressivement. Vous apprendrez egalement plus loin que les fonctions sont en fait des objets dont on cree a chaque fois une nouvelle instance lorsqu'on les appelle. 68. Gerard Swinnen : Apprendre a programmer avec Python On constate en effet que lorsque la fonction mask() est lancee, la variable globale q y est accessible, puisqu'elle est imprimee correctement. Pour p, par contre, c'est la valeur attribuee localement qui est affichee. On pourrait croire d'abord que la fonction mask() a simplement modifie le contenu de la variable globale p (puisqu'elle est accessible). Les lignes suivantes demontrent qu'il n'en est rien : en dehors de la fonction mask(), la variable globale p conserve sa valeur initiale. Tout ceci peut vous paraitre complique au premier abord. Vous comprendrez cependant tres vite combien il est utile que des variables soient ainsi definies comme etant locales, c'est-a-dire en quelque sorte confinees a l'interieur d'une fonction. Cela signifie en effet que vous pourrez toujours utiliser quantites de fonctions sans vous preoccuper le moins du monde des noms de variables qui y sont utilisees : ces variables ne pourront en effet jamais interferer avec celles que vous aurez vous- meme definies par ailleurs. Cet etat de choses peut toutefois etre modifie si vous le souhaitez. II peut se faire par exemple que vous ayez a definir une fonction qui soit capable de modifier une variable globale. Pour atteindre ce resultat, il vous suffira d'utiliser l'instruction global. Cette instruction permet d'indiquer - a l'interieur de la definition d'une fonction - quelles sont les variables a traiter globalement. Dans l'exemple ci-dessous, la variable a utilisee a l'interieur de la fonction monter() est non seulement accessible, mais egalement modifiable, parce qu'elle est signalee explicitement comme etant une variable qu'il faut traiter globalement. Par comparaison, essayez le meme exercice en supprimant l'instruction global : la variable a n'est plus incrementee a chaque appel de la fonction. »> def monter ( ) : global a ... a = a+1 print a »> a = 15 »> monter () 16 »> monter () 17 »> Gerard Swinnen : Apprendre a programmer avec Python 69. 7.3 « Vraies » fonctions et procedures Pour les puristes, les fonctions que nous avons decrites jusqu'a present ne sont pas tout a fait des fonctions au sens strict, mais plus exactement des procedures 25 . Une « vraie » fonction (au sens strict) doit en effet renvoyer une valeur lorsqu'elle se termine. Une « vraie » fonction peut s'utiliser a la droite du signe egale dans des expressions telles que y = sin (a) . On comprend aisem*nt que dans cette expression, la fonction sin() renvoie une valeur (le sinus de l'argument a) qui est directement affectee a la variable y. Commencons par un exemple extremement simple : »> def cube (w) : . . . return w*w*w L'instruction return definit ce que doit etre la valeur renvoyee par la fonction. En l'occurrence, il s'agit du cube de l'argument qui a ete transmis lors de l'appel de la fonction. Exemple : >» b = cube (9) »> print b 729 A titre d' exemple un peu plus elabore, nous allons maintenant modifier quelque peu la fonction table() sur laquelle nous avons deja pas mal travaille, afin qu'elle renvoie elle aussi une valeur. Cette valeur sera en l'occurrence une liste (la liste des dix premiers termes de la table de multiplication choisie). Voila done une occasion de reparler des listes. Nous en profiterons pour apprendre dans la foulee encore un nouveau concept : »> def table (base) : ... result = [] # result est d'abord une liste vide n = 1 . . . while n < 11 : ... b = n * base ... result . append (b) # ajout d'un terme a la liste ... n = n +1 # (voir explications ci-dessous) return result Pour tester cette fonction, nous pouvons entrer par exemple : »> ta9 = table (9) Ainsi nous affectons a la variable ta9 les dix premiers termes de la table de multiplication par 9, sous la forme d'une liste : »> print ta9 [9, 18, 27, 36, 45, 54, 63, 72, 81, 90] »> print ta9[0] 9 »> print ta9[3] 36 »> print ta9 [2 : 5] [27, 36, 45] >>> (Rappel : le premier element d'une liste correspond a l'indice 0) 25 Dans certains langages de programmation, les fonctions et les procedures sont definies a l'aide destructions differentes. Python utilise la meme instruction def pour defmir les unes et les autres. 70. Gerard Swinnen : Apprendre a programmer avec Python Notes: • Comme nous l'avons vu dans l'exemple precedent, l'instruction return definit ce que doit etre la valeur « renvoyee » par la fonction. En l'occurrence, il s'agit ici du contenu de la variable result, c'est-a-dire la liste des nombres generes par la fonction 26 . • L'instruction result.append(b) est notre second exemple de l'utilisation d'un concept important sur lequel nous reviendrons encore abondamment par la suite : dans cette instruction, nous appliquons la methode appendQ a I'objet result. Nous preciserons petit a petit ce qu'il faut entendre par objet en programmation. Pour l'instant, admettons simplement que ce terme tres general s'applique notamment aux listes de Python. Une methode n'est en fait rien d'autre qu'une fonction (que vous pouvez d'ailleurs reconnaitre comme telle a la presence des parentheses), mais une fonction qui est associee a un objet. Elle fait partie de la definition de cet objet, ou plus precisem*nt de la classe particuliere a laquelle cet objet appartient (nous etudierons ce concept de classe plus tard). Mettre en ceuvre une methode associee a un objet consiste en quelque sorte a faire « fonctionner » cet objet d'une maniere particuliere. Par exemple, on met en ceuvre la methode methode4() d'un objet objet3, a l'aide d'une instruction du type : objet3.methode4() , c'est-a- dire le nom de I'objet, puis le nom de la methode, relies l'un a l'autre par un point. Ce point joue un role essentiel : on peut le considerer comme un veritable operateur. Dans notre exemple, nous appliquons done la methode append() a I'objet result. Sous Python, les listes constituent un type particulier d'objets, auxquels on peut effectivement appliquer toute une serie de methodes. En l'occurrence, la methode appendO est done une fonction specifique des listes, qui sert a leur ajouter un element par la fin. L'element a ajouter est transmis entre les parentheses, comme tout argument qui se respecte. Remarque : Nous aurions obtenu un resultat similaire si nous avions utilise a la place de cette instruction une expression telle que « result = result + [b] ». Cette facon de proceder est cependant mo ins rationnelle et moins efficace, car elle consiste a redefinir a chaque iteration de la boucle une nouvelle liste result, dans laquelle la totalite de la liste precedente est a chaque fois recopiee avec ajout d'un element supplementaire. Lorsque Ton utilise la methode append(), par contre, l'ordinateur procede bel et bien a une modification de la liste existante (sans la recopier dans une nouvelle variable). Cette technique est preferable, car elle mobilise moins lourdement les ressources de l'ordinateur et elle est plus rapide (surtout lorsqu'il s'agit de traiter des listes volumineuses). • II n'est pas du tout indispensable que la valeur renvoyee par une fonction soit affectee a une variable (comme nous l'avons fait jusqu'ici dans nos exemples par souci de clarte). Ainsi, nous aurions pu tester les fonction cube() et table() en entrant les commandes : »> print cube (9) »> print table (9) »> print table (9) [3] ou encore plus simplement encore : >» cube (9) ... etc. 26 return peut egalement etre utilise sans aucun argument, a l'interieur d'une fonction, pour provoquer sa fermeture immediate. La valeur retournee dans ce cas est I'objet None (objet particulier, correspondant a "rien"). Gerard Swinnen : Apprendre a programmer avec Python 71. 7.4 Utilisation des fonctions dans un script Pour cette premiere approche des fonctions, nous n'avons utilise jusqu'ici que le mode interactif de l'interpreteur Python. II est bien evident que les fonctions peuvent aussi s'utiliser dans des scripts. Veuillez done essayer vous-meme le petit programme ci-dessous, lequel calcule le volume d'une sphere a l'aide de 4 3 la formule que vous connaissez certainement : V = — n R def cube (n) : return n**3 def volumeSphere (r) : return 4 * 3.1416 * cube(r) / 3 r = input (' Entrez la valeur du rayon : ') print 'Le volume de cette sphere vaut ' , volumeSphere (r) Notes : A bien y regarder, ce programme comporte trois parties : les deux fonctions cube() et volumeSphere(), et ensuite le corps principal du programme. Dans le corps principal du programme, il y a un appel de la fonction volumeSphere(). A l'interieur de la fonction volumeSphere(), il y a un appel de la fonction cube(). Notez bien que les trois parties du programme ont ete disposees dans un certain ordre : d'abord la definition des fonctions, et ensuite le corps principal du programme. Cette disposition est necessaire, parce que l'interpreteur execute les lignes d'instructions du programme l'une apres l'autre, dans l'ordre ou elles apparaissent dans le code source. Dans le script, la definition des fonctions doit done preceder leur utilisation. Pour vous en convaincre, intervertissez cet ordre (en placant par exemple le corps principal du programme au debut), et prenez note du type de message d'erreur qui est affiche lorsque vous essayez d'executer le script ainsi modifie. En fait, le corps principal d'un programme Python constitue lui-meme une entite un peu particuliere, qui est toujours reconnue dans le fonctionnement interne de l'interpreteur sous le nom reserve main (le mot « main » signifie « principal », en anglais. II est encadre par des caracteres « souligne » en double, pour eviter toute confusion avec d'autres symboles). L'execution d'un script commence toujours avec la premiere instruction de cette entite main , ou qu'elle puisse se trouver dans le listing. Les instructions qui suivent sont alors executees l'une apres l'autre, dans l'ordre, jusqu'au premier appel de fonction. Un appel de fonction est comme un detour dans le flux de l'execution : au lieu de passer a l'instruction suivante, l'interpreteur execute la fonction appelee, puis revient au programme appelant pour continuer le travail interrompu. Pour que ce mecanisme puisse fonctionner, il faut que l'interpreteur ait pu lire la definition de la fonction avant l'entite main , et celle-ci sera done placee en general a la fin du script. Dans notre exemple, l'entite main appelle une premiere fonction qui elle-meme en appelle une deuxieme. Cette situation est tres frequente en programmation. Si vous voulez comprendre correctement ce qui se passe dans un programme, vous devez done apprendre a lire un script, non pas de la premiere a la derniere ligne, mais plutot en suivant un cheminement analogue a ce qui se passe lors de l'execution de ce script. Cela signifie concretement que vous devrez souvent analyser un script en commencant par ses dernieres lignes ! 72. Gerard Swinnen : Apprendre a programmer avec Python 7.5 Modules de fonctions Afin que vous puissiez mieux comprendre encore la distinction entre la definition d'une fonction et son utilisation au sein d'un programme, nous vous suggerons de placer frequemment vos definitions de fonctions dans un module Python, et le programme qui les utilise dans un autre. Exemple : On souhaite realiser la serie de dessins ci-dessous, a l'aide du module turtle : Ecrivez les lignes de code suivantes, et sauvegardez-les dans un fichier auquel vous donnerez le nom dessins _tortue.py : from turtle import * def carre (taille, couleur) : "fonction qui dessine un carre de taille et de couleur determinees" color (couleur) c =0 while c <4 : forward (taille) right (90) c = c +1 Vous pouvez remarquer que la definition de la fonction carre() commence par une chaine de caracteres. Cette chaine ne joue aucun role fonctionnel dans le script : elle est traitee par Python comme un simple commentaire, mais qui est memorise a part dans un systeme de documentation interne automatique, lequel pourra ensuite etre exploite par certains utilitaires et editeurs "intelligents". Si vous programmez dans l'environnement IDLE, par exemple, vous verrez apparaitre cette chaine documentaire dans une "bulle d'aide", chaque fois que vous ferez appel aux fonctions ainsi documentees. En fait, Python place cette chaine dans une variable speciale dont le nom est doc (le mot "doc" entoure de deux paires de caracteres "souligne"), et qui est associee a l'objet fonction comme etant l'un de ses attributs (Vous en apprendrez davantage au sujet de ces attributs lorsque nous aborderons les classes d'objets. Cf. page 154). Ainsi, vous pouvez vous-meme retrouver la chaine de documentation d'une fonction quelconque en affichant le contenu de cette variable. Exemple : »> def essai() : ... "Cette fonction est bien documentee mais ne fait presque rien." print "rien a signaler" »> essai () rien a signaler »> print essai. doc Gerard Swinnen : Apprendre a programmer avec Python 73. Cette fonction est bien documentee mais ne fait presque rien. Prenez done la peine d'incorporer une telle chaine explicative dans toutes vos definitions de fonctions futures : il s'agit la d'une pratique hautement recommandable. Le fichier que vous aurez cree ainsi est dorenavant un veritable module de fonctions Python, au meme titre que les modules turtle ou math que vous connaissez deja. Vous pouvez done l'utiliser dans n'importe quel autre script, comme celui-ci, par exemple, qui effectuera le travail demande : from dessins_tortue import * up() # relever le crayon goto (-150, 50) # reculer en haut a gauche # dessiner dix carres rouges, alignes : i = 0 while i < 10: down() # abaisser le crayon carre(25, 'red') # tracer un carre up() # relever le crayon forward (30) # avancer + loin i = i +1 a = input () # attendre Note : Vous pouvez a priori nommer vos modules de fonctions comme bon vous semble. Sachez cependant qu'il vous sera impossible d'importer un module si son nom est Vun des 29 mots reserves Python signales a la page 22, car le nom du module importe deviendrait une variable dans votre script, et les mots reserves ne peuvent pas etre utilises comme noms de variables. Rappelons aussi qu'il vous faut eviter de donner a vos modules - et a tous vos scripts en general - le meme nom que celui d'un module Python preexistant, sinon vous devez vous attendre a des conflits. Par exemple, si vous donnez le nom turtle.py a un exercice dans lequel vous avez place une instruction d'importation du module turtle, e'est V exercice lui-meme que vous allez importer ! 74. Gerard Swinnen : Apprendre a programmer avec Python Resume : Structure d'un programme Python type #################################### # Programme Python type # # auteur : G.Swinnen, Liege, 2003 # # licence : GPL # #################################### ##################################### # Importation de fonctions externes : from math import sqrt ################################## # Definition locale de fonctions : def occurrences (car , ch) : nombre de caracteres \ dans la chaine " if ch[i] == car nc = nc + 1 ################################ # Corps principal du programme : print "Veuillez entrer un nombre : " nbr = input () print "Veuillez entrer une phrase : " phr = raw_input ( ) print "Entrez le caractere a compter cch = raw_input() no = occurrences (cch, phr) rc = sqrt(nbr**3) print "La racine carree du cube" , print "du nombre fourni vaut" , print rc print "La phrase contient" , print no, "caracteres", cch Un programme Python contient en general les blocs suivants, dans Vordre : - Quelques instructions d' initialisation (importation de fonctions et/ou de classes, definition eventuelle de variables globales) - Les definitions locales de fonctions et/ou de classes - Le corps principal du programme. Le programme pent utiliser un nombre quelconque de fonctions, lesquelles sont definies localement ou importees depuis des modules externes. (Vous pouvez vous-meme definir de tels modules). La definition d'une fonction comporte souvent une liste de parametres : ce sont toujours des variables, qui recevront leur valeur lorsque la fonction sera appelee. Une boucle de repetition de type 'while' doit en principe inclure les 4 elements suivants : - ['initialisation d'une variable 'compteur' ; - I'instruction while proprement dite, dans laquelle on exprime la condition de repetition des instructions qui suivent ; - le bloc d' instructions a repeter ; - une instruction ^incrementation du compteur. La fonction 'renvoie' toujours une valeur bien determinee au programme appelant. (Si I'instruction return n'est pas utilisee, ou si elle est utilisee sans argument, la fonction renvoie un objet vide : ) Le programme qui fait appel d une fonction lui transmet d'habitude une serie d 'arguments. lesquels peuvent etre des valeurs, des variables, ou meme des expressions. Gerard Swinnen : Apprendre a programmer avec Python 75 Exercices : 7.2. Definissez une fonction ligneCar(n, ca) qui renvoie une chaine de n caracteres ca. 7.3. Definissez une fonction surfCercle(R). Cette fonction doit renvoyer la surface (l'aire) d'un cercle dont on lui a fourni le rayon R en argument. Par exemple, l'execution de l'instruction : print surf cercle (2.5) doit donner le resultat 19.635 7.4. Definissez une fonction volBoite(xl,x2,x3) qui renvoie le volume d'une boite parallelipipedique dont on fournit les trois dimensions xl, x2, x3 en arguments. Par exemple, l'execution de l'instruction : print voiBoite(5.2, 7.7, 3.3) doit donner le resultat : 132.13 7.5. Definissez une fonction maximum(nl,n2,n3) qui renvoie le plus grand de 3 nombres nl, n2, n3 fournis en arguments. Par exemple, l'execution de l'instruction : print maximum (2,5,4) doit donner le resultat : 5 7.6. Completez le module de fonctions graphiques dessins_tortue.py decrit a la page 73. Commencez par ajouter un parametre angle a la fonction carre(), de maniere a ce que les carres puissent etre traces dans differentes orientations. Definissez ensuite une fonction triangle(taille, couleur, angle) capable de dessiner un triangle equilateral d'une taille, d'une couleur et d'une orientation bien determinees. Testez votre module a l'aide d'un programme qui fera appel a ces fonctions a plusieurs reprises, avec des arguments varies pour dessiner une serie de carres et de triangles : 7.7. Ajoutez au module de l'exercice precedent une fonction etoile5() specialisee dans le dessin d'etoiles a 5 branches. Dans votre programme principal, inserez une boucle qui dessine une rangee horizontale de de 9 petites etoiles de tailles variees : 76. Gerard Swinnen : Apprendre a programmer avec Python 7.8. Ajoutez au module de l'exercice precedent une fonction etoile6() capable de dessiner une etoile a 6 branches, elle-meme constitute de deux triangles equilateraux imbriques. Cette nouvelle fonction devra faire appel a la fonction triangle() definie precedemment. Votre programme principal dessinera egalement une serie de ces etoiles : 7.9. Definissez une fonction compteCar(ca,ch) qui renvoie le nombre de fois que Ton rencontre le caractere ca dans la chaine de caracteres ch. Par exemple, l'execution de l'instruction : print compteCar ( ' e ' , ' Cette phrase est un exemple ' ) doit dormer le resultat : 7 7.10. Definissez une fonction indexMax(liste) qui renvoie l'index de l'element ayant la valeur la plus elevee dans la liste transmise en argument. Exemple d'utilisation : serie = [5, 8, 2, 1, 9, 3, 6, 7] print indexMax (serie) 4 7.1 1. Definissez une fonction nomMois(n) qui renvoie le nom du n e mois de l'annee. Par exemple, l'execution de l'instruction : print nomMois(4) doit donner le resultat : Avril 7.12. Definissez une fonction inverse(ch) qui permette d'inverser les l'ordre des caracteres d'une chaine quelconque. (La chaine inversee sera renvoyee au programme appelant). 7.13. Definissez une fonction compteMots(ph) qui renvoie le nombre de mots contenus dans la phrase ph (On considere comme mots les ensembles de caracteres inclus entre des espaces). Gerard Swinnen : Apprendre a programmer avec Python 11. 7.6 Typage des parametres Vous avez appris que le typage des variables sous Python est un typage dynamique, ce qui signifie que le type d'une variable est defini au moment ou on lui affecte une valeur. Ce mecanisme fonctionne aussi pour les parametres d'une fonction. Le type d'un parametre sera le meme que celui de l'argument qui aura ete transmis a la fonction. Exemple : »> def af f icher3fois (arg) : print arg, arg, arg »> af f icher3fois (5) 5 5 5 »> af ficher3fois ( ' zut ' ) zut zut zut »> af ficher3fois ( [5, 7]) [5, 7] [5, 7] [5, 7] »> af ficher3fois (6**2) 36 36 36 Dans cet exemple, vous pouvez constater que la meme fonction afficher3fois() accepte dans tous les cas l'argument qu'on lui transmet, que cet argument soit un nombre, une chaine de caracteres, une liste, ou meme une expression. Dans ce dernier cas, Python commence par evaluer l'expression, et c'est le resultat de cette evaluation qui est transmis comme argument a la fonction. 7. 7 Valeurs par defaut pour les parametres Dans la definition d'une fonction, il est possible (et souvent souhaitable) de definir un argument par defaut pour chacun des parametres. On obtient ainsi une fonction qui peut etre appelee avec une partie seulement des arguments attendus. Exemples : >>> def politesse (nom, vedette = 'Monsieur ') : ... print "Veuillez agreer ,", vedette, nom, ", mes salutations distinguees . " »> politesse ( ' Dupont ' ) Veuillez agreer , Monsieur Dupont , mes salutations distinguees . >>> politesse (' Durand ' , 'Mademoiselle') Veuillez agreer , Mademoiselle Durand , mes salutations distinguees . Lorsque Ton appelle cette fonction en ne lui fournissant que le premier argument, le second recoit tout de meme une valeur par defaut. Si Ton fournit les deux arguments, la valeur par defaut pour le deuxieme est tout simplement ignoree. Vous pouvez definir une valeur par defaut pour tous les parametres, ou une partie d'entre eux seulement. Dans ce cas, cependant, les parametres sans valeur par defaut doivent preceder les autres dans la liste. Par exemple, la definition ci-dessous est incorrecte : »> def politesse (vedette ='Monsieur', nom): 78. Gerard Swinnen : Apprendre a programmer avec Python Autre exemple : »> def question (annonce, essais =4, please ='Oui ou non, s.v.p.!'): ... while essais >0 : reponse = raw_input (annonce) if reponse in ('o', ' oui ' , ' 0 ' , ' Oui ' , ' OUI ' ) : return 1 if reponse in ( ' n ' , ' non ' , 'N ' , 'Non ' , ' NON ' ) : return 0 print please essais = essais-1 »> Cette fonction peut etre appelee de differentes facons, telles par exemple : rep = question ( 'Voulez-vous vraiment terminer ? ') OU bien : rep = question ( 'Faut-il ef facer ce fichier ? ' , 3) OU meme encore : rep = question ( 'Avez-vous compris ? ', 2, 'Repondez par oui ou par non !') (Prenez la peine d'essayer et de decortiquer cet exemple) 7.8 Arguments avec etiquettes Dans la plupart des langages de programmation, les arguments que Ton fournit lors de l'appel d'une fonction doivent etre fournis exactement dans le meme ordre que celui des parametres qui leur correspondent dans la definition de la fonction. Python autorise cependant une souplesse beaucoup plus grande. Si les parametres annonces dans la definition de la fonction ont recu chacun une valeur par defaut, sous la forme deja decrite ci- dessus, on peut faire appel a la fonction en fournissant les arguments correspondants dans n'importe quel ordre, d la condition de designer nommement les parametres correspondants. Exemple : »> def oiseau (voltage=100, etat= ' allume ' , action= ' danser la java'): print ' Ce perroquet ne pourra pas ' , action ... print 'si vous le branchez sur', voltage, 'volts !' ... print "L'auteur de ceci est completement", etat »> oiseau (etat= ' givre ' , voltage=250, action='vous approuver') Ce perroquet ne pourra pas vous approuver si vous le branchez sur 250 volts ! L ' auteur de ceci est completement givre >» oiseau () Ce perroquet ne pourra pas danser la java si vous le branchez sur 100 volts ! L ' auteur de ceci est completement allume Gerard Swinnen : Apprendre a programmer avec Python 79. Exercices : 7.14. Modifiez la fonction volBoite(xl,x2,x3) que vous avez definie dans un exercice precedent, de maniere a ce qu'elle puisse etre appelee avec trois, deux, un seul, ou meme aucun argument. Utilisez pour ceux ci des valeurs par defaut egales a) 10. Par exemple : print voiBoite () doit donner le resultat : 1000 print voiBoite (5 . 2) doit donner le resultat : 520.0 print voiBoite (5.2, 3) doit donner le resultat : 156.0 7.15. Modifiez la fonction volBoite(xl,x2,x3) ci-dessus de maniere a ce qu'elle puisse etre appelee avec un, deux, ou trois arguments. Si un seul est utilise, la boite est consideree comme cubique (l'argument etant l'arete de ce cube). Si deux sont utilises, la boite est consideree comme un prisme a base carree. (Dans ce cas le premier argument est le cote du carre, et le second la hauteur du prisme). Si trois arguments sont utilises, la boite est consideree comme un parallelepipede. Par exemple : print voiBoite ( ) doit donner le resultat : -1 (— > indication dune erreur). print voiBoite (5.2) doit donner le resultat : 140.608 print voiBoite (5.2, 3) doit donner le resultat : 81.12 print voiBoite (5.2, 3, 7.4) doit donner le resultat : 115.44 7.16. Definissez une fonction changeCar(ch,cal,ca2,debut,fin) qui remplace tous les caracteres cal par des caracteres ca2 dans la chaine de caracteres ch, a partir de l'indice debut et jusqu'a l'indice fin, ces deux derniers arguments pouvant etre omis (et dans ce cas la chaine est traitee d'une extremite a l'autre). Exemples de la fonctionnalite attendue : »> phrase = 'Ceci est une toute petite phrase.' »> print changeCar (phrase, ' ', '*') Ceci*est*une*toute*petite*phrase . »> print changeCar (phrase, ' ', '*', 8, 12) Ceci est*une*toute petite phrase . »> print changeCar (phrase, ' ', '*', 12) Ceci est une*toute*petite*phrase . >» print changeCar (phrase, ' ', '*', fin = 12) Ceci*est*une*toute petite phrase. 7.17. Definissez une fonction eleMax(liste,debut,fin) qui renvoie l'element ayant la plus grande valeur dans la liste transmise. Les deux arguments debut et fin indiqueront les indices entre lesquels doit s'exercer la recherche, et chacun d'eux pourra etre omis (comme dans l'exercice precedent). Exemples de la fonctionnalite attendue : »> serie = [9, 3, 6, 1, 7, 5, 4, 8, 2] >>> print eleMax (serie) 9 >>> print eleMax (serie, 2, 5) 7 >>> print eleMax (serie, 2) 8 »> print eleMax (serie, fin =3, debut =1) 6 80. Gerard Swinnen : Apprendre a programmer avec Python Chapitre 8 : Utilisation de fenetres et de graphismes Jusqu'a present, nous avons utilise Python exclusivement « en mode texte ». Nous avons procede ainsi parce qu'il nous fallait absolument d'abord degager un certain nombre de concepts elementaires ainsi que la structure de base du langage, avant d'envisager des experiences impliquant des objets informatiques plus elabores (fenetres, images, sons, etc.). Nous pouvons a present nous permettre une petite incursion dans le vaste domaine des interfaces graphiques, mais ce ne sera qu'un premier amuse-gueule : il nous reste en effet encore bien des choses fondamentales a apprendre, et pour nombre d'entre elles l'approche textuelle reste la plus abordable. 8.1 Interfaces graphiques (GUI) Si vous ne le saviez pas encore, apprenez des a present que le domaine des interfaces graphiques (ou GUI : Graphical User Interface) est extremement complexe. Chaque systeme d'exploitation peut en effet proposer plusieurs « bibliotheques » de fonctions graphiques de base, auxquelles viennent frequemment s'ajouter de nombreux complements, plus ou moins specifiques de langages de programmation particuliers. Tous ces composants sont generalement presentes comme des classes d'objets, dont il vous faudra etudier les attributs et les methodes. Avec Python, la bibliotheque graphique la plus utilisee jusqu'a present est la bibliotheque Tkinter, qui est une adaptation de la bibliotheque Tk developpee a l'origine pour le langage Tel. Plusieurs autres bibliotheques graphiques fort interessantes ont ete proposees pour Python : wxPython, pyQT, pyGTK, etc. II existe egalement des possibilites d'utiliser les bibliotheques de widgets Java et les MFC de Windows. Dans le cadre de ces notes, nous nous limiterons cependant a Tkinter, dont il existe fort heureusem*nt des versions similaires (et gratuites) pour les plates-formes Linux, Windows et Mac. 8.2 Premiers pas avec Tkinter Pour la suite des explications, nous supposerons bien evidemment que le module Tkinter a deja ete installe sur votre systeme. Pour pouvoir en utiliser les fonctionnalites dans un script Python, il faut que Tune des premieres lignes de ce script contienne l'instruction d'importation : from Tkinter import * Comme toujours sous Python, il n'est meme pas necessaire d'ecrire un — . — . — . script. Vous pouvez faire un grand nombre d'experiences directement a la Bfc^^H I ' ' ligne de commande, en ayant simplement lance Python en mode interactif. Bonjour tout le monde ! Dans l'exemple qui suit, nous allons creer une fenetre tres simple, et y ajouter Quitter deux widgets 27 typiques : un bout de texte (ou label) et un bouton (ou button). ' >>> from Tkinter import * >» fenl = Tk() »> texl = Label (fenl, text= ' Bon jour tout le monde !', fg='red') »> texl. pack () »> boul = Button (fenl, text= ' Quitter ' , command = fenl . destroy) >» boul. pack () >>> f enl . mainloop ( ) 27 "widget" est le resultat de la contraction de l'expression "window gadget". Dans certains environnements de programmation, on appellera cela plutot un "controle" ou un "composant graphique". Ce terme designe en fait toute entite susceptible d'etre placee dans une fenetre d'application, comme par exemple un bouton, une case a cocher, une image, etc., et parfois aussi la fenetre elle-meme. Gerard Swinnen : Apprendre a programmer avec Python 81. Note : Suivant la version de Python utilisee, vous verrez deja apparaitre la fenetre d' application immediatement apres avoir entre la deuxieme commande de cet exemple, ou bien seulement apres la septieme 28 . Examinons a present plus en detail chacune des lignes de commandes executees : 1. Comme cela a deja ete explique precedemment, il est aise de construire differents modules Python, qui contiendront des scripts, des definitions de fonctions, des classes d'objets, etc. On peut alors importer tout ou partie de ces modules dans n'importe quel programme, ou meme dans l'interpreteur fonctionnant en mode interactif (c'est-a-dire directement a la ligne de commande). C'est ce que nous faisons a la premiere ligne de notre exemple : « from Tkinter import * » consiste a importer toutes les classes contenues dans le module Tkinter. Nous devrons de plus en plus souvent parler de ces classes. En programmation, on appelle ainsi des generateurs d'objets, lesquels sont eux-memes des morceaux de programmes reutilisables. Nous n'allons pas essayer de vous fournir des a present une definition definitive et precise de ce que sont les objets et les classes, mais plutot vous proposer d'en utiliser directement quelques-un (e)s. Nous affinerons notre comprehension petit a petit par la suite. 2. A la deuxieme ligne de notre exemple : « feni = Tk() », nous utilisons l'une des classes du module Tkinter, la classe Tk(), et nous en creons une instance (autre terme designant un objet specifique), a savoir la fenetre fenl. Ce processus d'instanciation d'un objet a partir d'une classe est une operation fondamentale dans les techniques actuelles de programmation. Celles-ci font en effet de plus en plus souvent appel a une methodologie que Ton appelle « programmation orientee objet » (ou OOP : Object Oriented Programming) . La classe est en quelque sorte un modele general (ou un moule) a partir duquel on demande a la machine de construire un objet informatique particulier. La classe contient toute une serie de definitions et d'options diverses, dont nous n'utilisons qu'une partie dans l'objet que nous creons a partir d'elle. Ainsi la classe Tk() , qui est l'une des classes les plus fondamentales de la bibliotheque Tkinter, contient tout ce qu'il faut pour engendrer differents types de fenetres d'application, de tailles ou de couleurs diverses, avec ou sans barre de menus, etc. Nous nous en servons ici pour creer notre objet graphique de base, a savoir la fenetre qui contiendra tout le reste. Dans les parentheses de Tk(), nous pourrions preciser differentes options, mais nous laisserons cela pour un peu plus tard. L'instruction d'instanciation ressemble a une simple affectation de variable. Comprenons bien cependant qu'il se passe ici deux choses a la fois : • la creation d'un nouvel objet, (lequel peut etre complexe et done occuper un espace memoire considerable) • V affectation d'une variable qui va desormais servir de reference pour manipuler l'objet 29 . 28 Si vous effectuez cet exercice sous Windows, nous vous conseillons d'utiliser de preference une version standard de Python dans une fenetre DOS ou dans IDLE plutot que PythonWin. Vous pourrez mieux observer ce qui se passe apres l'entree de chaque commande. 29 Cette concision du langage est une consequence du typage dynamique des variables en vigueur sous Python. D'autres langages utilisent une instruction particuliere (telle que new) pour instancier un nouvel objet. Exemple : maVoiture = new Cadillac (instanciation d'un objet de classe Cadillac, reference dans la variable maVoiture) 82. Gerard Swinnen : Apprendre a programmer avec Python 3. A la troisieme ligne : « texl = Label(fenl, text='Bonjour tout le monde !', fg='red') », nous creons un autre objet (un widget), cette fois a partir de la classe Label(). Comme son nom l'indique, cette classe definit toutes sortes ^etiquettes (ou de « libelles »). En fait, il s'agit tout simplement de fragments de texte quelconques, utilisables pour afficher des informations et des messages divers a l'interieur d'une fenetre. Nous efforcant d'apprendre au passage la maniere correcte d'exprimer les choses, nous dirons done que nous creons ici l'objet texl par instanciation de la classe Label(). Remarquons ici que nous faisons appel a une classe, de la meme maniere que nous faisons appel a une fonction : e'est-a-dire en fournissant un certain nombre d' arguments dans des parentheses. Nous verrons plus loin qu'une classe est en fait une sorte de 'conteneur' dans lequel sont regroupees des fonctions et des donnees. Quels arguments avons-nous done fournis pour cette instanciation ? • Le premier argument transmis (fenl), indique que le nouveau widget que nous sommes en train de creer sera contenu dans un autre widget preexistant, que nous designons done ici comme son « maitre » : l'objet fenl est le widget maitre de l'objet texl. (On pourra dire aussi que l'objet texl est un widget esclave de l'objet fenl). • Les deux arguments suivants servent a preciser la forme exacte que doit prendre notre widget. Ce sont en effet deux options de creation, chacune fournie sous la forme d'une chaine de caracteres : d'abord le texte de l'etiquette, ensuite sa couleur d'avant-plan (ou foreground, en abrege fg). Ainsi le texte que nous voulons afficher est bien defini, et il doit apparaitre colore en rouge. Nous pourrions encore preciser bien d'autres caracteristiques : la police a utiliser, ou la couleur d'arriere-plan, par exemple. Toutes ces caracteristiques ont cependant une valeur par defaut dans les definitions internes de la classe Label(). Nous ne devons indiquer des options que pour les caracteristiques que nous souhaitons differentes du modele standard. 4. A la quatrieme ligne de notre exemple : « texl.pack() » , nous activons une methode associee a l'objet texl : la methode pack(). Nous avons deja rencontre ce terme de methode (a propos des listes, notamment). Une methode est une fonction integree a un objet (on dira aussi qu'elle est encapsulee dans l'objet). Nous apprendrons bientot qu'un objet informatique est en fait un morceau de programme contenant toujours : • un certain nombre de donnees (numeriques ou autres), contenues dans des variables de types divers : on les appelle les attributs (ou les proprietes) de l'objet. • un certain nombre de procedures ou de fonctions (qui sont done des algorithmes) : on les appelle les methodes de l'objet. La methode pack() fait partie d'un ensemble de methodes qui sont applicables non seulement aux widgets de la classe Label(), mais aussi a la plupart des autres widgets Tkinter, et qui agissent sur leur disposition geometrique dans la fenetre. Comme vous pouvez le constater par vous-meme si vous entrez les commandes de notre exemple une par une, la methode pack() reduit automatiquement la taille de la fenetre « maitre » afin qu'elle soit juste assez grande pour contenir les widgets « esclaves » definis au prealable. Gerard Swinnen : Apprendre a programmer avec Python 83. 5. A la cinquieme ligne : « boul = Button(fenl, text='Quitter', command = fenl.destroy) », nous creons notre second widget « esclave » : un bouton. Comme nous l'avons fait pour le widget precedent, nous appelons la classe Button() en fournissant entre parentheses un certain nombre d'arguments. Etant donne qu'il s'agit cette fois dun objet interactif, nous devons preciser avec l'option command ce qui devra se passer lorsque l'utilisateur effectuera un clic sur le bouton. Dans ce cas precis, nous actionnerons la methode destroy associee a l'objet fenl, ce qui devrait provoquer l'effacement de la fenetre. 6. La sixieme ligne utilise la methode pack() pour adapter la geometrie de la fenetre au nouvel objet que nous venons d'y integrer. 7. La septieme ligne : « fenl.mainloop() » est tres importante, parce que c'est elle qui provoque le demarrage du receptionnaire d'evenements associe a la fenetre. Cette instruction est necessaire pour que votre application soit « a l'affut » des clics de souris, des pressions exercees sur les touches du clavier, etc. C'est done cette instruction qui « la met en marche », en quelque sorte. Comme son nom l'indique {mainloop), il s'agit dune methode de l'objet fenl, qui active une boucle de programme, laquelle « tournera » en permanence en tache de fond, dans l'attente de messages emis par le systeme d'exploitation de l'ordinateur. Celui-ci interroge en effet sans cesse son environnement, notamment au niveau des peripheriques d'entree (souris, clavier, etc.). Lorsqu'un evenement quelconque est detecte, divers messages decrivant cet evenement sont expedies aux programmes qui souhaitent en etre avertis. Voyons cela un peu plus en detail. 84. Gerard Swinnen : Apprendre a programmer avec Python Initialisation 1 Fonctionnalite centrale du programme 8.3 Programmes pilotes par des evenements Vous venez d'experimenter votre premier programme utilisant une interface graphique. Ce type de programme est structure d'une maniere differente des scripts « textuels » etudies auparavant. Tous les programmes d'ordinateur comportent grosso-modo trois phases principales : une phase d 'initialisation, laquelle contient les instructions qui preparent le travail a effectuer (appel des modules externes necessaires, ouverture de fichiers, connexion a un serveur de bases de donnees ou a l'internet, etc.), une phase centrale ou Ton trouve la veritable fonctionnalite du programme (c'est-a- dire tout ce qu'il est cense faire : afficher des donnees a l'ecran, effectuer des calculs, modifier le contenu d'un fichier, imprimer, etc.), et enfin une phase de terminaison qui sert a cloturer « proprement » les operations (c'est-a-dire fermer les fichiers restes ouverts, couper les connexions externes, etc.) Dans un programme « en mode texte », ces trois phases sont simplement organisees suivant un schema lineaire comme dans l'illustration ci-contre. En consequence, ces programmes se caracterisent par une interactivite tres limitee avec l'utilisateur. Celui-ci ne dispose pratiquement d'aucune liberte : il lui est demande de temps a autre d'entrer des donnees au clavier, mais toujours dans un ordre predetermine correspondant a la sequence destructions du programme. Dans le cas d'un programme qui utilise une interface graphique, par contre, l'organisation interne est differente. On dit d'un tel programme qu'il est pilote par les evenements. Apres sa phase d'initialisation, un programme de ce type se met en quelque sorte « en attente », et passe la main a un autre logiciel, lequel est plus ou moins intimement integre au systeme d'exploitation de l'ordinateur et « tourne » en permanence. Ce re'ceptionnaire d'evenements scrute sans cesse tous les peripheriques (clavier, souris, horloge, modem, etc.) et reagit immediatement lorsqu'un evenement y est detecte. Un tel evenement peut etre une action quelconque de l'utilisateur : deplacement de la souris, appui sur une touche, etc., mais aussi un evenement externe ou un automatisme (top d'horloge, par ex.) i Terminaison Initialisation messages Fonctionnalite centrale du programme Gerard Swinnen : Apprendre a programmer avec Python 85. Lorsqu'il detecte un evenement, le receptionnaire envoie un message specifique au programme 30 , lequel doit etre concu pour reagir en consequence. La phase d'initialisation d'un programme utilisant une interface graphique comporte un ensemble d'instructions qui mettent en place les divers composants interactifs de cette interface (fenetres, boutons, cases a cocher, etc.). D'autres instructions definissent les messages d'evenements qui devront etre pris en charge : on peut en effet decider que le programme ne reagira qu'a certains evenements en ignorant tous les autres. Alors que dans un programme « textuel », la phase centrale est constitute d'une suite d'instructions qui decrivent a l'avance l'ordre dans lequel la machine devra executer ses differentes taches (meme s'il est prevu des cheminements differents en reponse a certaines conditions rencontrees en cours de route), on ne trouve dans la phase centrale d'un programme avec interface graphique qu'un ensemble de fonctions independantes. Chacune de ces fonctions est appelee specifiquement lorsqu'un evenement particulier est detecte par le systeme d'exploitation : elle effectue alors le travail que Ton attend du programme en reponse a cet evenement, et rien d' autre 31 . II est important de bien comprendre ici que pendant tout ce temps, le receptionnaire continue a « tourner » et a guetter l'apparition d'autres evenements eventuels. S'il arrive d'autres evenements, il peut done se faire qu'une seconde fonction (ou une 3e, une 4e, ...) soit activee et commence a effectuer son travail « en parallele » avec la premiere qui n'a pas encore termine le sien 32 . Les systemes d'exploitation et les langages modernes permettent en effet ce parallelisme que Ton appelle aussi multitache. Au chapitre precedent de ces notes, nous vous avons deja fait remarquer que la structure du texte d'un programme n'indique pas directement l'ordre dans lequel les instructions seront finalement executees. Cette remarque s'applique encore bien davantage dans le cas d'un programme avec interface graphique, puisque l'ordre dans lequel les fonctions sont appelees n'est plus inscrit nulle part dans le programme. Ce sont les evenements qui pilotent ! Tout ceci doit vous paraitre un peu complique. Nous allons l'illustrer dans quelques exemples. 30 Ces messages sont souvent notes WM (Window messages) dans un environnement graphique constitue de fenetres (avec de nombreuses zones reactives : boutons, cases a cocher, menus deroulants, etc.). Dans la description des algorithmes, il arrive frequemment aussi qu'on confonde ces messages avec les evenements eux-memes. 3 1 Au sens strict, une telle fonction qui ne devra renvoyer aucune valeur est done plutot une procedure (cfr. page 70). 32 En particulier, la meme fonction peut etre appelee plusieurs fois en reponse a l'occurrence de quelques evenements identiques, la meme tache etant alors effectuee en plusieurs exemplaires concurrents. Nous verrons plus loin qu'il peut en resulter des "effets de bords" genants. 86. Gerard Swinnen : Apprendre a programmer avec Python 8.3.1 Exemple graphique : trace de lignes dans un canevas Le script decrit ci-dessous cree une fenetre comportant trois boutons et un canevas. Suivant la terminologie de Tkinter, un canevas est une surface rectangulaire delimitee, dans laquelle on peut installer ensuite divers dessins et images a l'aide de methodes specifiques 33 . Lorsque Ton actionne le bouton « Tracer une ligne », une nouvelle ligne coloree apparait sur le canevas, avec a chaque fois une inclinaison differente de la precedente. Si Ton actionne le bouton « Autre couleur », une nouvelle couleur est tiree au hasard dans une serie limitee. Cette couleur est celle qui 1- n|x -is ^ Tracer une ligne Autre couleur Quitter s'appliquera aux traces suivants. Le bouton « Quitter » sert bien evidemment a terminer l'application en refermant la fenetre. # Petit exercice utilisant la bibliotheque graphique Tkinter from Tkinter import * from random import randrange # definition des fonctions gestionnaires d'evenements : def drawline() : "Trace d'une ligne dans le canevas canl" global xl, yl, x2, y2, coul canl . create_line (xl , yl , x2 , y2 , width=2 , f ill=coul) # modification des coordonnees pour la ligne suivante : y2, yl = y2+10, yl-10 def changecolor () : "Changement aleatoire de la couleur du trace" global coul pal= [ ' purple ' , ' cyan ' , ' maroon ' , ' green ' , ' red ' , ' blue ' , ' orange ' , ' yellow ' ] c = randrange (8) # => genere un nombre aleatoire de 0 a 7 coul = pal[c] # Programme principal # les variables suivantes seront utilisees de maniere globale : xl, yl, x2, y2 = 10, 190, 190, 10 # coordonnees de la ligne coul = ' dark green ' # couleur de la ligne # Creation du widget principal ("maitre") : fenl = Tk() # creation des widgets "esclaves" : canl = Canvas (fenl, bg=' dark grey ' , height=200, width=200) canl .pack (side=LEFT) 33 Ces dessins pourront eventuellement etre animes dans une phase ulterieure (voir plus loin) Gerard Swinnen : Apprendre a programmer avec Python 87. boul = Button (fenl, text =' Quitter ', command=fenl .quit) boul .pack (side=BOTTOM) bou2 = Button (fenl , text =' Tracer une ligne ' , command=drawline) bou2 .pack () bou3 = Button (fenl, text = 'Autre couleur ' , command=changecolor) bou3 .pack () f enl . mainloop ( ) # demarrage du receptionnaire d'evenements fenl . destroy () # destruction (fermeture) de la fenetre Conformement a ce que nous avons explique dans le texte des pages precedentes, la fonctionnalite de ce programme est essentiellement assuree par les deux fonctions drawline() et changecolor(), qui seront activees par des evenements, ceux-ci etant eux-memes definis dans la phase d'initialisation. Dans cette phase d'initialisation, on commence par importer l'integralite du module Tkinter ainsi qu'une fonction du module random qui permet de tirer des nombres au hasard. On cree ensuite les differents widgets par instanciation a partir des classes Tk(), CanvasO et Button(). (Remarquons au passage que le mot canevas s'ecrit differemment en francais et en anglais !) L'initialisation se termine avec l'instruction fenl.mainloop() qui demarre le receptionnaire d'evenements. Les instructions qui suivent ne seront executees qu'a la sortie de cette boucle, sortie elle-meme declenchee par la methode fenl.quit() (voir ci-apres). L'option command utilisee dans l'instruction d'instanciation des boutons permet de designer la fonction qui devra etre appelee lorsqu'un evenement se produira. II s'agit en fait d'un raccourci pour cet evenement particulier, qui vous est propose par Tkinter pour votre facilite parce que cet evenement est celui que Ton associe naturellement a un widget de type bouton. Nous verrons plus loin qu'il existe d'autres techniques plus generates pour associer n'importe quel type d' evenement a n'importe quel widget. Les fonctions de ce script peuvent modifier les valeurs de certaines variables qui ont ete definies au niveau principal du programme. Cela est rendu possible grace a l'instruction global utilisee dans la definition de ces fonctions. Nous nous permettrons de proceder ainsi pendant quelque temps encore (ne serait-ce que pour vous habituer a distinguer les comportements des variables locales et globales), mais comme vous le comprendrez plus loin, cette pratique n'est pas tout a fait recommandable, surtout lorsqu'il s'agit d'ecrire de grands programmes. Nous apprendrons une meilleure technique lorsque nous aborderons l'etude des classes (a partir de la page 152). Dans notre fonction changecolor(), une couleur est choisie au hasard dans une liste. Nous utilisons pour ce faire la fonction randrange() importee du module random. Appelee avec un argument N, cette fonction renvoie un nombre entier, tire au hasard entre zero et N-l. La commande liee au bouton « Quitter » appelle la methode quit() de la fenetre fenl. Cette methode sert a fermer (quitter) le receptionnaire d'evenements {mainloop) associe a cette fenetre. Lorsque cette methode est activee, l'execution du programme se poursuit avec les instructions qui suivent l'appel de mainloop. Dans notre exemple, cela consiste done a effacer (destroy) la fenetre. 88. Gerard Swinnen : Apprendre a programmer avec Python (8) Exercices : modifications au programme « Trace de lignes » ci-dessus. 8.1. Comment faut-il modifier le programme pour ne plus avoir que des lignes de couleur cyan, maroon et green ? 8.2. Comment modifier le programme pour que toutes les lignes tracees soient horizontales et paralleles ? 8.3. Agrandissez le canevas de maniere a lui donner une largeur de 500 unites et une hauteur de 650. Modifiez egalement la taille des lignes, afin que leurs extremites se confondent avec les bords du canevas. 8.4. Ajoutez une fonction « drawline2 » qui tracera deux lignes rouges en croix au centre du canevas : l'une horizontale et l'autre verticale. Ajoutez egalement un bouton portant l'indication « viseur ». Un clic sur ce bouton devra provoquer l'affichage de la croix. 8.5. Reprenez le programme initial. Remplacez la methode « create line » par « create rectangle ». Que se passe-t-il ? De la meme facon, essayez aussi « create_arc », « create_oval », et « create_polygon ». Pour chacune de ces methodes, notez ce qu'indiquent les coordonnees fournies en parametres. (Remarque : pour le polygone, il est necessaire de modifier legerement le programme !) 8.6. - Supprimez la ligne « global xl, yl, x2, y2 » dans la fonction « drawline » du programme original. Que se passe-t-il ? Pourquoi ? - Si vous placez plutot « xl, yl, x2, y2 » entre les parentheses, dans la ligne de definition de la fonction « drawline », de maniere a transmettre ces variables a la fonction en tant que parametres, le programme fonctionne-t-il encore ? (N'oubliez pas de modifier aussi la ligne du programme qui fait appel a cette fonction !) - Si vous definissez « xl, yl, x2, y2 = 10, 390, 390, 10 » a la place de « global xl, yl, ... », que se passe-t-il ? Pourquoi ? Quelle conclusion pouvez-vous tirer de tout cela ? 8.7. a) Creez un court programme qui dessinera les 5 anneaux olympiques dans un rectangle de fond blanc (white). Un boutton « Quitter » doit permettre de fermer la fenetre. b) Modifiez le programme ci-dessus en y ajoutant 5 boutons. Chacun de ces boutons provoquera le trace de chacun des 5 anneaux 8.8. Dans votre cahier, etablissez un tableau a deux colonnes. Vous y noterez a gauche les definitions des classes d'objets deja rencontrees (avec leur liste de parametres), et a droite les methodes associees a ces classes (egalement avec leurs parametres). Laisser de la place pour completer ulterieurement. Gerard Swinnen : Apprendre a programmer avec Python 89. 8.3.2 Exemple graphique : deux dessins alternes Cet autre exemple vous montrera comment vous pouvez exploiter les connaissances que vous avez acquises precedemment concernant les boucles, les listes et les fonctions, afin de realiser de nombreux dessins avec seulement quelques lignes de code. II s'agit d'une petite application qui affiche l'un ou l'autre des deux dessins reproduits ci-contre, en fonction du bouton choisi. from Tkinter import * def cercle (x, y, r, coul ='black'): "trace d'un cercle de centre (x,y) et de rayon r" can . create_oval (x-r, y-r, x+r, y+r, outline=coul) def figure_l(): "dessiner une cible" # Ef facer d'abord tout dessin preexistant : can . delete (ALL) # tracer les deux lignes (vert . et horiz . ) : can.create_line(100, 0, 100, 200, fill ='blue') can.create_line(0, 100, 200, 100, fill ='blue') # tracer plusieurs cercles concentriques : rayon = 15 while rayon < 100: cercle (100, 100, rayon) rayon += 15 def figure_2 () : "dessiner un visage simplifie" # Ef facer d'abord tout dessin preexistant : can . delete (ALL) # Les caracteristiques de chaque cercle sont # placees dans une liste de listes : cc =[[100, 100, 80, 'red'], # visage [70, 70, 15, 'blue'], # yeux [130, 70, 15, 'blue'], [70, 70, 5, 'black'], [130, 70, 5, 'black'], [44, 115, 20, 'red'], # joues [156, 115, 20, 'red'], [100, 95, 15, 'purple'], # nez [100, 145, 30, 'purple']] # bouche trace tous les cercles a l'aide d'une boucle u n dessin 1 dessin Z dessin 1 dessin Z # on i =0 while i < len(cc) el = cc[i] cercle (el [0] , i += 1 el[l] # parcours de la liste # chaque element est lui-meme une liste el[2], el [3]) ##### Programme principal : ############ fen = Tk() can = Canvas(fen, width =200, height =200, bg ='ivory') can .pack (side =TOP, padx =5, pady =5) bl = Button (fen, text =' dessin 1', command =figure_l) bl. pack (side =LEFT, padx =3, pady =3) b2 = Button (fen, text =' dessin 2', command =figure_2) b2.pack(side =RIGHT, padx =3, pady =3) fen . mainloop ( ) 90. Gerard Swinnen : Apprendre a programmer avec Python Commencons par analyser le programme principal, a la fin du script : Nous y creons une fenetre, par instanciation d'un objet de la classe Tk() dans la variable fen. Ensuite, nous installons 3 widgets dans cette fenetre : un canevas et deux boutons. Le canevas est instancie dans la variable can, et les deux boutons dans les variables bl et b2. Comme dans le script precedent, les widgets sont mis en place dans la fenetre a l'aide de leur methode pack(), mais cette fois nous utilisons celle-ci avec des options : • l'option side peut accepter les valeurs TOP, BOTTOM, LEFT ou RIGHT, pour « pousser » le widget du cote correspondant dans la fenetre. • les options padx et pady permettent de reserver un petit espace autour du widget. Cet espace est exprime en nombre de pixels : padx reserve un espace a gauche et a droite du widget, pady reserve un espace au-dessus et au-dessous du widget. Les boutons commandent l'affichage des deux dessins, en invoquant les fonctions figure_l() et figure_2(). Considerant que nous aurions a tracer un certain nombre de cercles dans ces dessins, nous avons estime qu'il serait bien utile de definir d'abord une fonction cercle() specialisee. En effet : Vous savez probablement deja que le canevas Tkinter est dote d'une methode create_oval() qui permet de dessiner des ellipses quelconques (et done aussi des cercles), mais cette methode doit etre invoquee avec quatre arguments qui seront les coordonnees des coins superieur gauche et inferieur droit d'un rectangle fictif, dans lequel l'ellipse viendra alors s'inscrire. Cela n'est pas tres pratique dans le cas particulier du cercle : il nous semblera plus naturel de commander ce trace en fournissant les coordonnees de son centre ainsi que son rayon. C'est ce que nous obtiendrons avec notre fonction cercle(), laquelle invoque la methode create_oval() en effectuant la conversion des coordonnees. Remarquez que cette fonction attend un argument facultatif en ce qui concerne la couleur du cercle a tracer (noir par defaut). L'efficacite de cette approche apparait clairement dans la fonction figure_l(), ou nous trouvons une simple boucle de repetition pour dessiner toute la serie de cercles (de meme centre et de rayon croissant). Notez au passage l'utilisation de l'operateur += qui permet d'incrementer une variable (dans notre exemple, la variable r voit sa valeur augmenter de 15 unites a chaque iteration). Le second dessin est un peu plus complexe, parce qu'il est compose de cercles de tailles variees centres sur des points differents. Nous pouvons tout de meme tracer tous ces cercles a l'aide d'une seule boucle de repetition, si nous mettons a profit nos connaissances concernant les listes. En effet. Ce qui differencie les cercles que nous voulons tracer tient en quatre caracteristiques : coordonnees x et y du centre, rayon et couleur. Pour chaque cercle, nous pouvons placer ces quatre caracteristiques dans une petite liste, et rassembler toutes les petites listes ainsi obtenues dans une autre liste plus grande. Nous disposerons ainsi d'une liste de listes, qu'il suffira ensuite de parcourir a l'aide d'une boucle pour effectuer les traces correspondants. JJ)'!^^- - lglXl Exercices : 8.9. Inspirez-vous du script precedent pour ecrire une petite application qui fait apparaitre un damier (dessin de cases noires sur fond blanc) lorsque Ton clique sur un bouton : 8.10. A l'application de l'exercice precedent, ajoutez un bouton qui fera apparaitre des pions au hasard sur le damier (chaque pression sur le bouton fera apparaitre un nouveau pion). damier pions Gerard Swinnen : Apprendre a programmer avec Python 91. 8.3.3 Exemple graphique : calculatrice minimaliste Bien que tres court, le petit script ci-dessous implemente une calculatrice complete, avec laquelle vous pourrez meme effectuer des calculs comportant des parentheses et des fonctions scientifiques. N'y voyez rien | (57 + 12)/ 3.^ d'extraordinaire. Toute cette fonctionnalite n'est qu'une consequence du fait Resultat = 21.5625 que vous utilisez un interpreteur plutot qu'un compilateur pour executer vos programmes. Comme vous le savez, le compilateur n'intervient qu'une seule fois, pour traduire l'ensemble de votre code source en un programme executable. Son role est done termine avant meme l'execution du programme. L'interpreteur, quant a lui, est toujours actif pendant l'execution du programme, et done tout a fait disponible pour traduire un nouveau code source quelconque, comme par exemple une expression mathematique entree au clavier par l'utilisateur. Les langages interpretes disposent done toujours de fonctions permettant d' evaluer une chaine de caracteres comme une suite destructions du langage lui-meme. II devient alors possible de construire en peu de lignes des structures de programmes tres dynamiques. Dans l'exemple ci- dessous, nous utilisons la fonction integree eval() pour analyser l'expression mathematique entree par l'utilisateur dans le champ prevu a cet effet, et nous n'avons plus ensuite qu'a afficher le resultat. # Exercice utilisant la bibliotheque graphique Tkinter et le module math from Tkinter import * from math import * # definition de 1' action a effectuer si l'utilisateur actionne # la touche "enter" alors qu'il edite le champ d' entree : def evaluer (event) : chaine . configure (text = "Resultat = " + str (eval (entree . get ())) ) # Programme principal : fenetre = Tk() entree = Entry (fenetre) entree .bind ( "" , evaluer) chaine = Label (fenetre) entree .pack () chaine .pack () fenetre . mainloop ( ) Au debut du script, nous commencons par importer les modules Tkinter et math, ce dernier etant necessaire afin que la dite calculatrice puisse disposer de toutes les fonctions mathematiques et scientifiques usuelles : sinus, cosinus, racine carree, etc. Ensuite nous definissons une fonction evaluer(), qui sera en fait la commande executee par le programme lorsque l'utilisateur actionnera la touche Return (ou Enter) apres avoir entre une expression mathematique quelconque dans le champ d'entree decrit plus loin. 92. Gerard Swinnen : Apprendre a programmer avec Python Cette fonction utilise la methode configure() du widget chaine 34 , pour modifier son attribut text. L'attribut en question recoit done ici une nouvelle valeur, determinee par ce que nous avons ecrit a la droite du signe egale : il s'agit en l'occurrence d'une chaine de caracteres construite dynamiquement, a l'aide de deux fonctions integrees dans Python : eval() et str(), et d'une methode associee a un widget Tkinter : la methode get(). eval() fait appel a l'interpreteur pour evaluer une expression Python qui lui est transmise dans une chaine de caracteres. Le resultat de revaluation est fourni en retour. Exemple : chaine = "(25 + 8)/3" # chaine contenant une expression mathematique res = eval (chaine) # evaluation de 1' expression contenue dans la chaine print res +5 # => le contenu de la variable res est numerique str() transforme une expression numerique en chaine de caracteres. Nous devons faire appel a cette fonction parce que la precedente renvoie une valeur numerique, que nous convertissons a nouveau en chaine de caracteres pour pouvoir l'incorporer au message « Resultat = ». get() est une methode associee aux widgets de la classe Entry. Dans notre petit programme exemple, nous utilisons un widget de ce type pour permettre a l'utilisateur d'entrer une expression numerique quelconque a l'aide de son clavier. La methode get() permet en quelque sorte « d'extraire » du widget « entree » la chaine de caracteres qui lui a ete fournie par l'utilisateur. Le corps du programme principal contient la phase d'initialisation, qui se termine par la mise en route de l'observateur d'evenements (mainloop). On y trouve l'instanciation d'une fenetre Tk(), contenant un widget « chaine » instancie a partir de la classe Label(), et un widget « entree » instancie a partir de la classe Entry(). Attention, a present : afin que ce dernier widget puisse vraiment faire son travail, e'est-a-dire transmettre au programme l'expression que l'utilisateur y aura encodee, nous lui associons un evenement a l'aide de la methode bind() 35 : entree . bind ( " " , evaluer) Cette instruction signifie : « Lier I'evenement a I'objet , le gestionnaire de cet evenement etant la fonction ». L'evenement a prendre en charge est decrit dans une chaine de caracteres specifique (dans notre exemple, il s'agit de la chaine « »). II existe un grand nombre de ces evenements (mouvements et clics de la souris, enfoncement des touches du clavier, positionnement et redimensionnement des fenetres, passage au premier plan, etc.). Vous trouverez la liste des chaines specifiques de tous ces evenements dans les ouvrages de reference traitant de Tkinter. Profitons de l'occasion pour observer encore une fois la syntaxe des instructions destinees a mettre en oeuvre une methode associee a un objet : objet.methode(arguments) On ecrit d'abord le nom de I'objet sur lequel on desire intervenir, puis le point (qui fait office d'operateur), puis le nom de la methode a mettre en oeuvre ; entre les parentheses associees a cette methode, on indique enfin les arguments qu'on souhaite lui transmettre. 34 La methode configure() peut s'appliquer a n'importe quel widget preexistant, pour en modifier les proprietes. 35 En anglais, le mot bind signifie "lier" Gerard Swinnen : Apprendre a programmer avec Python 93. 8.3.4 Exemple graphique : detection et positionnement d'un clic de souris Dans la definition de la fonction « evaluer » de l'exemple precedent, vous aurez remarque que nous avons fourni un argument event (entre les parentheses). Cet argument est obligatoire. Lorsque vous definissez une fonction gestionnaire d'evenement qui est associee a un widget quelconque a l'aide de sa methode bind(), vous devez toujours l'utiliser comme premier argument. II s'agit d'un objet Python standard, cree automatiquement, qui permet de transmettre au gestionnaire d'evenement un certain nombre d'attributs de cet evenement : • le type d'evenement : deplacement de la souris, enfoncement ou relachement de l'un de ses boutons, appui sur une touche du clavier, entree du curseur dans une zone predefinie, ouverture ou fermeture d'une fenetre, etc. • une serie de proprietes de l'evenement : l'instant ou il s'est produit, ses coordonnees, les caracteristiques du ou des widget(s) concerne(s), etc. Nous n'allons pas entrer dans trop de details. Si vous voulez bien encoder et experimenter le petit script ci-dessous, vous aurez vite compris le principe. # Detection et positionnement d'un clic de souris dans une fenetre : from Tkinter import * def point eur (event) : chaine . configure (text = "Clic detecte en X =" + str (event. x) +\ ", Y =" + str (event. y) ) fen = Tk() cadre = Frame (fen, width =200, height =150, bg="light yellow") cadre .bind("" , pointeur) cadre .pack () chaine = Label (fen) chaine .pack () fen . mainloop ( ) Le script fait apparaitre une fenetre contenant un cadre Hj^^^^^^v^ (frame) rectangulaire de couleur jaune pale, dans lequel l'utilisateur est invite a effectuer des clics de souris. La methode bind() du widget cadre associe l'evenement au gestionnaire d'evenement « pointeur ». Ce gestionnaire d'evenement peut utiliser les attributs x et y de l'objet event genere automatiquement par Python, pour construire la chaine de caracteres qui affichera la position de la ^^^^^^^^^^^^^^^ souris au moment du clic. Clic detecte en X =75, Y =45 Exercice : 8.1 1. Modifiez le script ci-dessus de maniere a faire apparaitre un petit cercle rouge a l'endroit ou l'utilisateur a effectue son clic (vous devrez d'abord remplacer le widget Frame par un widget Canvas). 94. Gerard Swinnen : Apprendre a programmer avec Python 8.4 Les classes de widgets Tkinter Note : Au long de ce cours, nous vous presenterons petit d petit le mode d'utilisation d'un certain nombre de widgets. Comprenez bien cependant qu'il n'entre pas dans nos intentions de fournir ici un manuel de reference complet sur Tkinter. Nous limiterons nos explications aux widgets qui nous semblent les plus interessants d'un point de vue didactique, c'est-d-dire ceux qui pourront nous aider a mettre en evidence des concepts importants, tel le concept de classe. Veuillez done consulter la litterature (voir page 8) si vous souhaitez davantage de precisions. II existe 15 classes de base pour les widgets Tkinter : Widget Description Button Un bouton classique, a utiliser pour provoquer 1' execution d'une commande quelconque. Canvas Un espace pour disposer divers elements graphiques. Ce widget peut etre utilise pour dessiner, creer des editeurs graphiques, et aussi pour implementer des widgets personnalises. Checkbutton Une « case a cocher » qui peut prendre deux etats distincts (la case est cochee ou non). Un clic sur ce widget provoque le changement d'etat. Entry Un champ d' entree, dans lequel l'utilisateur du programme pourra inserer un texte quelconque a partir du clavier. Frame Une surface rectangulaire dans la fenetre, ou Ton peut disposer d'autres widgets. Cette surface peut etre coloree. Elle peut aussi etre decoree d'une bordure. j^aoei Un texte (ou libelle) quelconque (eventuellement une image). Listbox Une liste de choix proposes a l'utilisateur, generalement presentes dans une sorte de boite. On peut egalement configurer la Listbox de telle maniere qu'elle se comporte comme une serie de « boutons radio » ou de cases a cocher. ivienu Un menu. Ce peut etre un menu deroulant attache a la barre de titre, ou bien un menu « pop up » apparaissant n'importe ou a la suite d'un clic. Menubutton Un bouton-menu, a utiliser pour implementer des menus deroulants. iviessage rermei u aincner un lexie. v^e wiugei est une vananie uu wiugei i^aoei, qui permei d'adapter automatiquement le texte affiche a une certaine taille ou a un certain rapport largeur/hauteur. Radiobutton Represente (par un point noir dans un petit cercle) une des valeurs d'une variable qui peut en posseder plusieurs. Cliquer sur un « bouton radio » donne la valeur correspondante a la variable, et "vide" tous les autres boutons radio associes a la meme variable. Scale Vous permet de faire varier de maniere tres visuelle la valeur d'une variable, en depla?ant un curseur le long d'une regie. Scrollbar « ascenseur » ou « barre de defilement » que vous pouvez utiliser en association avec les autres widgets : Canvas, Entry, Listbox, Text. Text Affichage de texte formatte. Permet aussi a l'utilisateur d'editer le texte affiche. Des images peuvent egalement etre inserees. Toplevel Une fenetre affichee separement, « par-dessus ». Ces classes de widgets integrent chacune un grand nombre de methodes. On peut aussi leur associer (lier) des evenements, comme nous venons de le voir dans les pages precedentes. Vous allez apprendre en outre que tous ces widgets peuvent etre positionnes dans les fenetres a l'aide de trois methodes differentes : la methode grid(), la methode pack() et la methode place(). Gerard Swinnen : Apprendre a programmer avec Python 95. L'utilite de ces methodes apparait clairement lorsque Ton s'efforce de realiser des programmes portables (c'est-a-dire susceptibles de fonctionner indifferemment sur des systemes d'exploitation aussi differents que Unix, MacOS ou Windows), et dont les fenetres soient redimensionnables. 8.5 Utilisation de la methode gridQ pour contrdler la disposition des widgets Jusqu'a present, nous avons toujours dispose les widgets dans leur fenetre, a l'aide de la methode pack(). Cette methode presentait l'avantage d'etre extraordinairement simple, mais elle ne nous donnait pas beaucoup de liberie pour disposer les widgets a notre guise. Comment faire, par exemple, pour obtenir la fenetre ci-contre ? Nous pourrions effectuer un certain nombre de tentatives en fournissant a la methode pack() des arguments de type « side = », comme nous l'avons deja fait precedemment, mais cela ne nous mene pas tres loin. Essayons par exemple : from Tkinter import * tk - n|x| Premier champ : Second : fenl = Tk() txtl = Label (fenl, text txt2 = Label (fenl, text entrl = Entry (fenl) entr2 = Entry (fenl) txtl .pack (side =LEFT) txt2 .pack (side =LEFT) entrl .pack (side =RIGHT) entr2 .pack (side =RIGHT) ' Premier champ ' Second : ' ) fenl . mainloop ( ) ... mais le resultat n'est pas vraiment celui que nous recherchions Ik H.lnlxl Premier champ : Second : Pour mieux comprendre comment fonctionne la methode pack(), vous pouvez encore essayer differentes combinaisons d'options, telles que side =TOP, side =BOTTOM, pour chacun de ces quatre widgets. Mais vous n'arriverez certainement pas a obtenir ce qui vous a ete demande. Vous pourriez peut-etre y parvenir en definissant deux widgets Frame() supplementaires, et en y incorporant ensuite separement les widgets LabelQ et Entry(). Cela devient fort complique. 96. Gerard Swinnen : Apprendre a programmer avec Python II est temps que nous apprenions a utiliser une autre approche du probleme. Veuillez done analyser le script ci-dessous : il contient en effet (presque) la solution : from Tkinter import * fenl = Tk() txtl = Label (fenl, text = 'Premier champ : ') txt2 = Label (fenl, text = 'Second :') entrl = Entry (fenl) entr2 = Entry (fenl) txtl . grid (row =0) txt2 . grid (row =1) Premier champ : Second : entrl . grid (row =0, column =1) entr2 . grid (row =1, column =1) fenl . mainloop ( ) Dans ce script, nous avons done remplace la methode pack() par la methode grid(). Comme vous pouvez le constater, l'utilisation de la methode grid() est tres simple. Cette methode considere la fenetre comme un tableau (ou une grille). II suffit alors de lui indiquer dans quelle ligne (row) et dans quelle colonne (column) de ce tableau on souhaite placer les widgets. On peut numeroter les lignes et les colonnes comme on veut, en partant de zero, ou de un, ou encore d'un nombre quelconque : Tkinter ignorera les lignes et colonnes vides. Notez cependant que si vous ne fournissez aucun numero pour une ligne ou une colonne, la valeur par defaut sera zero. Tkinter determine automatiquement le nombre de lignes et de colonnes necessaire. Mais ce n'est pas tout : si vous examinez en detail la petite fenetre produite par le script ci-dessus, vous constaterez que nous n'avons pas encore tout a fait atteint le but poursuivi. Les deux chaines apparaissant dans la partie gauche de la fenetre sont centrees, alors que nous souhaitions les aligner l'une et l'autre par la droite. Pour obtenir ce resultat, il nous suffit d'ajouter un argument dans l'appel de la methode grid() utilisee pour ces widgets. L'option sticky peut prendre l'une des quatre valeurs N, S, W, E (les quatre points cardinaux en anglais). En fonction de cette valeur, on obtiendra un alignement des widgets par le haut, par le bas, par la gauche ou par la droite. Remplacez done les deux premieres instructions grid() du script par : txtl . grid (row =0, sticky =E) txt2 . grid (row =1, sticky =E) ... et vous atteindrez enfin exactement le but recherche. Gerard Swinnen : Apprendre a programmer avec Python 97. Analysons a present la fenetre suivante : Cette fenetre comporte 3 colonnes : une premiere avec les 3 chaines de caracteres, une seconde avec les 3 champs d'entree, et une troisieme avec l'image. Les deux premieres colonnes comportent chacune 3 lignes, mais l'image situee dans la derniere colonne s'etale en quelque sorte sur les trois. Le code correspondant est le suivant : from Tkinter import * fenl = Tk() # creation de widgets 'Label' et 'Entry' : txtl = Label (fenl, text =' Premier champ :') txt2 = Label (fenl, text =' Second :') txt3 = Label(fenl, text ='Troisieme :') entrl = Entry (fenl) entr2 = Entry (fenl) entr3 = Entry (fenl) # creation d'un widget 'Canvas' contenant une image bitmap : canl = Canvas (fenl, width =160, height =160, bg ='white') photo = Photolmage (f ile = 'Martin_P . gif ' ) item = canl . create_image (80, 80, image =photo) # Mise en page a 1 ' aide de la methode ' grid ' txtl . grid (row =1, sticky =E) txt2 . grid (row =2, sticky =E) txt3 . grid (row =3, sticky =E) entrl . grid (row =1, column =2) entr2 . grid (row =2, column =2) entr3 .grid (row =3, column =2) canl . grid (row =1, column =3, rowspan =3, padx =10, pady =5) # demarrage : fenl . mainloop ( ) 98. Gerard Swinnen : Apprendre a programmer avec Python Pour pouvoir faire fonctionner ce script, il vous faudra probablement remplacer le nom du fichier image (Martin_P.gif) par le nom d'une image de votre choix. Attention : la bibliotheque Tkinter standard n'accepte qu'un petit nombre de formats pour cette image. Choisissez de preference le format GIF. Nous pouvons remarquer un certain nombre de choses dans ce script : 1 . La technique utilisee pour incorporer une image : Tkinter ne permet pas d'inserer directement une image dans une fenetre. II faut d'abord installer un canevas, et ensuite positionner l'image dans celui-ci. Nous avons opte pour un canevas de couleur blanche, afin de pouvoir le distinguer de la fenetre. Vous pouvez remplacer le parametre bg ='white' par bg ='gray' si vous souhaitez que le canevas devienne invisible. Etant donne qu'il existe de nombreux types d' images, nous devons en outre declarer l'objet image comme etant un bitmap GIF, a partir de la classe PhotoImage() 36 . 2. La ligne ou nous installons l'image dans le canevas est la ligne : item = canl . create_image (80 , 80, image =photo) Pour employer un vocabulaire correct, nous dirons que nous utilisons ici la methode create_image() associee a l'objet canl (lequel objet est lui-meme une instance de la classe Canvas). Les deux premiers arguments transmis (80, 80) indiquent les coordonnees x et y du canevas ou il faut placer le centre de l'image. (Les dimensions du canevas etant de 160x160, notre choix aboutira done a un centrage de l'image au milieu du canevas). 3. La numerotation des lignes et colonnes dans la methode grid() : On peut constater que la numerotation des lignes et des colonnes dans la methode grid() utilisee ici commence cette fois a partir de 1 (et non a partir de zero comme dans le script precedent). Comme nous l'avons deja signale plus haut, ce choix de numerotation est tout a fait libre. On pourrait tout aussi bien numeroter : 5, 10, 15, 20... puisque Tkinter ignore les lignes et les colonnes vides. Numeroter a partir de 1 augmente probablement la lisibilite de notre code. 4. Les arguments utilises avec grid() pour positionner le canevas : canl .grid (row =1, column =3, rowspan =3, padx =10, pady =5) Les deux premiers arguments indiquent que le canevas sera place dans la premiere ligne de la troisieme colonne. Le troisieme (rowspan =3) indique qu'il pourra « s'etaler » sur trois lignes. Les deux derniers (padx =10, pady =5) indiquent la dimension de l'espace qu'il faut reserver autour de ce widget (en largeur et en hauteur). 5. Et tant que nous y sommes, profitons de cet exemple de script que nous avons deja bien decortique, pour apprendre a simplifier quelque peu notre code ... 36 II existe d'autres classes d'images, mais pour les utiliser il faut importer dans le script d'autres modules graphiques que la seule bibliotheque Tkinter. Vous pouvez par exemple experimenter la bibliotheque PIL {Python Imaging Library). Gerard Swinnen : Apprendre a programmer avec Python 99. 8.6 Composition constructions pour ecrire un code plus compact Du fait que Python est un langage de programmation de haut niveau, il est souvent possible (et souhaitable) de retravailler un script afin de le rendre plus compact. Vous pouvez par exemple assez frequemment utiliser la composition d'instructions pour appliquer la methode de mise en page des widgets (grid(), pack() ou place()) au moment meme ou vous creez ces widgets. Le code correspondant devient alors un peu plus simple, et parfois plus lisible. Vous pouvez par exemple remplacer les deux lignes : txtl = Label (fenl, text =' Premier champ :') txtl . grid (row =1, sticky =E) du script precedent par une seule, telle que : Label(fenl, text = ' Premier champ :'). grid (row =1, sticky =E) Dans cette nouvelle ecriture, vous pouvez constater que nous faisons l'economie de la variable intermediaire txtl. Nous avions utilise cette variable pour bien degager les etapes successives de notre demarche, mais elle n'est pas toujours indispensable. Le simple fait d'invoquer la classe Label () provoque en effet l'instanciation dun objet de cette classe, meme si Ton ne memorise pas la reference de cet objet dans une variable {Tkinter la conserve de toute facon dans sa representation interne de la fenetre). Si Ton procede ainsi, la reference est perdue pour le restant du script, mais elle peut tout de meme etre transmise a une methode de mise en page telle que grid() au moment meme de l'instanciation, en une seule instruction composee. Voyons cela un peu plus en detail : Jusqu'a present, nous avons cree des objets divers (par instanciation a partir d'une classe quelconque), en les affectant a chaque fois a des variables. Par exemple, lorsque nous avons ecrit : txtl = Label (fenl, text =' Premier champ :') nous avons cree une instance de la classe Label(), que nous avons assignee a la variable txtl. La variable txtl peut alors etre utilisee pour faire reference a cette instance, partout ailleurs dans le script, mais dans les faits nous ne l'utilisons qu'une seule fois pour lui appliquer la methode grid (), le widget dont il est question n'etant rien d'autre qu'une simple etiquette descriptive. Or, creer ainsi une nouvelle variable pour n'y faire reference ensuite qu'une seule fois (et directement apres sa creation) n'est pas une pratique tres recommandable, puisqu'elle consiste a reserver inutilement un certain espace memoire. Lorsque ce genre de situation se presente, il est plus judicieux d'utiliser la composition d'instructions. Par exemple, on preferera le plus souvent remplacer les deux instructions : somme =45+72 print somme par une seule instruction composee, telle que : print 45 + 72 on fait ainsi l'economie d'une variable. De la meme maniere, lorsque Ton met en place des widgets auxquels on ne souhaite plus revenir par apres, comme c'est souvent le cas pour les widgets de la classe Label(), on peut en general appliquer la methode de mise en page (grid() , pack() ou placeO) directement au moment de la creation du widget, en une seule instruction composee. Cela s'applique seulement aux widgets qui ne sont plus references apres qu'on les ait crees. Tous les autres doivent imperativement etre assigned a des variables, afin que Ton puisse encore interagir avec eux ailleurs dans le script. 100. Gerard Swinnen : Apprendre a programmer avec Python Et dans ce cas, il faut obligatoirement utiliser deux instructions distinctes, l'une pour instancier le widget et l'autre pour lui appliquer ensuite la methode de mise en page. Vous ne pouvez pas, par exemple, construire une instruction composee telle que : entree = Entry (fenl) .pack () # faute de programmation !!! En apparence, cette instruction devrait instancier un nouveau widget et l'assigner a la variable entree, la mise en page s'effectuant dans la meme operation a l'aide de la methode pack(). Dans la realite, cette instruction produit bel et bien un nouveau widget de la classe EntryO, et la methode pack() effectue bel et bien sa mise en page dans la fenetre, mais la valeur qui est memorisee dans la variable entree est la valeur de retour de la methode pack() : ce n 'est pas la reference du widget. Et vous ne pouvez rien faire de cette valeur de retour : il s'agit d'un objet vide (None). Pour obtenir une vraie reference du widget, vous devez utiliser deux instructions : entree = Entry (fenl) # instanciation du widget entree .pack () # application de la mise en page Note : Lorsque vous utilisez la methode grid(), vous pouvez simplifier encore un peu votre code, en omettant l'indication de nombreux numeros de lignes et de colonnes. A partir du moment ou c'est la la methode grid() qui est utilisee pour positionner les widgets, Tkinter considere en effet qu'il existe forcement des lignes et des colonnes 37 . Si un numero de ligne ou de colonne n'est pas indique, le widget correspondant est place dans la premiere case vide disponible. Le script ci-dessous integre les simplifications que nous venons d'expliquer : from Tkinter import * fenl = Tk() # creation de widgets Label (), EntryO, et Checkbutton ( ) : Label (fenl, text = 'Premier champ :'). grid (sticky =E) Label (fenl, text = 'Second :'). grid (sticky =E) Label (fenl, text = 'TroisiA'me :'). grid (sticky =E) entrl = Entry (fenl) entr2 = Entry (fenl) # entr3 = Entry (fenl) # entrl . grid (row =0, column =1) # entr2 . grid (row =1, column =1) # entr3 . grid (row =2, column =1) chekl = Checkbutton (fenl, text ='Case a cocher, pour voir') chekl . grid (columnspan =2) ces widgets devront certainement etre references plus loin : il faut done les assigner chacun a une variable distincte # creation d ' un widget ' Canvas ' contenant une image bitmap canl = Canvas (fenl, width =160, height =160, bg =' white') photo = Photolmage (file = ' Martin_P . gif ' ) canl . create_image (80, 80, image =photo) canl . grid (row =0, column =2, rowspan =4, padx =10, pady =5) # demarrage : fenl . mainloop ( ) 37 Surtout, n'utilisez pas plusieurs methodes de positionnement differentes dans la meme fenetre ! Les methodes grid(), pack() et placeQ sont mutuellement exclusives. Gerard Swinnen : Apprendre a programmer avec Python 101. 8.7 Modification des proprietes d'un objet - Animation A ce stade de votre apprentissage, vous souhaitez certainement pouvoir faire apparaitre un petit dessin quelconque dans un canevas, et puis le deplacer a volonte, par exemple a l'aide de boutons. Veuillez done ecrire, tester, puis analyser le script ci-dessous : from Tkinter import * # procedure generale de deplacement : def avance (gd, hb) : global xl, yl xl, yl = xl +gd, yl +hb canl . coords (ovall , xl, yl, xl+30, # gestionnaires d'evenements : def depl_gauche ( ) : avance (-10, 0) def depl_droite ( ) : avance (10, 0) def depl_haut ( ) : avance (0, -10) Exercice d'animation avec Tk... HOB yl+30) def depl_bas() : avance (0, 10) # Programme principal # les variables suivantes seront utilisees de maniere globale : xl, yl = 10, 10 # coordonnees initiales # Creation du widget principal ("maitre") : fenl = Tk() fenl . title ( "Exercice d'animation avec Tkinter") # creation des widgets "esclaves" : canl = Canvas (fenl, bg=' dark grey' ,height=300,width=300) ovall = canl . create_oval (xl, yl, xl+30, yl+30, width=2 , fill= ' red' ) canl .pack (side=LEFT) Button (fenl, text= ' Quitter ' , command=f enl . quit) . pack (side=BOTTOM) Button (fenl, text= ' Gauche ' , command=depl_gauche) .pack() Button (fenl, text =' Droite ' , command=depl_droite) .pack() Button (fenl , text= ' Haut ' , command=depl_haut ) . pack ( ) Button ( f enl , text = ' Bas ' , command=depl_bas ) . pack ( ) # demarrage du receptionnaire d'evenements (boucle principale) fenl . mainloop ( ) 102. Gerard Swinnen : Apprendre a programmer avec Python Le corps de ce programme reprend de nombreuses elements connus : nous y creons une fenetre fenl, dans laquelle nous installons un canevas contenant lui-meme un cercle colore, plus cinq boutons de controle. Veuillez remarquer au passage que nous n'instancions pas les widgets boutons dans des variables (c'est inutile, puisque nous n'y faisons plus reference par apres) : nous devons done appliquer la methode pack() directement au moment de la creation de ces objets. La vraie nouveaute de ce programme reside dans la fonction avanceO definie au debut du script. Chaque fois qu'elle sera appelee, cette fonction redefinira les coordonnees de l'objet « cercle colore » que nous avons installe dans le canevas, ce qui provoquera l'animation de cet objet. Cette maniere de proceder est tout a fait caracteristique de la programmation « orientee objet » : On commence par creer des objets, et puis on agit sur ces objets en modifiant leurs proprietes, par l'intermediaire de methodes. En programmation procedurale « a l'ancienne » (e'est-a-dire sans utilisation d'objets), on anime des figures en les effacant a un endroit pour les redessiner ensuite un petit peu plus loin. En programmation « orientee objet », par contre, ces taches sont prises en charge automatiquement par les classes dont les objets derivent, et il ne faut done pas perdre son temps a les reprogrammer. Exercices : 8.12. Ecrivez un programme qui fait apparaitre une fenetre avec un canevas. Dans ce canevas on verra deux cercles (de tailles et de couleurs differentes), qui sont censes representer deux astres. Des boutons doivent permettre de les deplacer a volonte tous les deux dans toutes les directions. Sous le canevas, le programme doit afficher en permanence : a) la distance separant les deux astres; b) la force gravitationnelle qu'ils exercent l'un sur l'autre (Penser a afficher en haut de fenetre les masses choisies pour chacun d'eux, ainsi que l'echelle des distances). Dans cet exercice, vous utiliserez evidemment la loi de la gravitation universelle de Newton (cfr. exercice 42, page 61, et votre cours de Physique generale). 8.13. En vous inspirant du programme qui detecte les clics de souris dans un canevas, modifiez le programme ci-dessus pour y reduire le nombre de boutons : pour deplacer un astre, il suffira de le choisir avec un bouton, et ensuite de cliquer sur le canevas pour que cet astre se positionne a l'endroit ou Ton a clique. 8.14. Extension du programme ci-dessus. Faire apparaitre un troisieme astre, et afficher en permanence la force resultante agissant sur chacun des trois (en effet : chacun subit en permanence l'attraction gravitationnelle exercee par les deux autres !). 8.15. Meme exercice avec des charges electriques (loi de Coulomb). Donner cette fois une possibility de choisir le signe des charges. 8.16. Ecrivez un petit programme qui fait apparaitre une fenetre avec deux champs : l'un indique une temperature en degres Celsius, et l'autre la meme temperature exprimee en degres Fahrenheit. Chaque fois que Ton change une quelconque des deux temperatures, l'autre est corrigee en consequence. Pour convertir les degres Fahrenheit en Celsius et vice-versa, on utilise la formule T F — T C X 1,80 +32 (cfr. cours de Physique generale). Revoyez aussi le petit programme concernant la calculatrice simplifiee. Gerard Swinnen : Apprendre a programmer avec Python 103. 8.17. Ecrivez un programme qui fasse apparaitre une fenetre avec un canevas. Dans ce canevas, placez un petit cercle cense representer une balle. Sous le canevas, placez un bouton. Chaque fois que Ton clique sur le bouton, la balle doit avancer d'une petite distance vers la droite, jusqu'a ce qu'elle atteigne l'extremite du canevas. Si Ton continue a cliquer, la balle doit alors revenir en arriere jusqu'a l'autre extremite, et ainsi de suite. 8.18. Ameliorez le programme ci-dessus pour que la balle decrive cette fois une trajectoire circulaire ou elliptique dans le canevas (lorsque Ton clique continuellement). Note : pour arriver au resultat escompte, vous devrez necessairement definir une variable qui representera Tangle decrit, et utiliser les fonctions sinus et cosinus pour positionner la balle en fonction de cet angle. 8.19. Modifiez le programme ci-dessus, de telle maniere que la balle en se deplacant laisse derriere elle une trace de la trajectoire decrite. 8.20. Modifiez le programme ci-dessus de maniere a tracer d'autres figures. Consultez votre professeur pour des suggestions (courbes de Lissajous). 8.21. Ecrivez un programme qui fasse apparaitre une fenetre avec un canevas et un bouton. Dans le canevas, tracez un rectangle gris fonce, lequel representera une route, et par-dessus, une serie de rectangles jaunes censes representer un passage pour pietons. Ajoutez quatre cercles colores pour figurer les feux de circulation concernant les pietons et les vehicules. Chaque utilisation du bouton devra provoquer le changement de couleur des feux : Changer 8.22. Ecrivez un programme qui montre un canevas dans lequel est dessine un circuit electrique simple (generateur + interrupteur + resistance). La fenetre doit etre pourvue de champs d'entree qui permettront de parametrer chaque element (c'est-a-dire choisir les valeurs des resistances et tensions). L'interrupteur doit etre fonctionnel (Prevoyez un bouton « Marche/arret » pour cela). Des « etiquettes » doivent afficher en permanence les tensions et intensites resultant des choix operes par l'utilisateur. 104. Gerard Swinnen : Apprendre a programmer avec Python 8.8 Animation automatique - Recursivite Pour conclure cette premiere prise de contact avec l'interface graphique Tkinter, voici un dernier exemple d'animation, qui fonctionne cette fois de maniere autonome des qu'on l'a mise en marche. from Tkinter import * # definition des gestionnaires # d'evenements : def move ( ) : "deplacement de la balle" global xl, yl, dx, dy, flag xl, yl = xl +dx, yl + dy if xl >210: xl, dx, dy = 210, 0, 15 if yl >210: yl, dx, dy = 210, -15, 0 if xl <10: xl, dx, dy = 10, 0, -15 if yl <10: yl, dx, dy = 10, 15, 0 canl . coords (ovall , xl, yl, xl+30, yl+30) if flag >0: fenl . after (50 , move) # => boucler apres 50 millisecondes def stop_it ( ) : "arret de 1' animation" global flag flag =0 def start_it ( ) : "demarrage de 1' animation" global flag if flag ==0: # pour ne lancer qu'une seule boucle flag =1 move ( ) #========== Programme principal ============= # les variables suivantes seront utilisees de maniere globale : xl, yl = 10, 10 # coordonnees initiales dx, dy = 15, 0 # 'pas' du deplacement flag =0 # commutateur # Creation du widget principal ("parent") : fenl = Tk() fenl . title ( "Exercice d'animation avec Tkinter") # creation des widgets "enfants" : canl = Canvas (fenl, bg=' dark grey ' , height=250, width=250) canl .pack (side=LEFT, padx =5, pady =5) ovall = canl . create_oval (xl , yl, xl+30, yl+30, width=2, fill='red') boul = Button (fenl, text= ' Quitter ' , width =8, command=fenl . quit ) boul .pack (side=BOTTOM) bou2 = Button(fenl, text= ' Demarrer ' , width =8, command=start_it ) bou2 .pack () bou3 = Button (fenl, text= ' Arreter ' , width =8, command=stop_it ) bou3 .pack () # demarrage du receptionnaire d'evenements (boucle principale) : fenl . mainloop ( ) Gerard Swinnen : Apprendre a programmer avec Python 105. La seule nouveaute mise en oeuvre dans ce script se trouve tout a la fin de la definition de la fonction move() : vous y noterez l'utilisation de la methode after(). Cette methode peut s'appliquer a un widget quelconque. Elle declenche l'appel d'une fonction apres qu 'un certain laps de temps se soit ecoule. Ainsi par exemple, window.after(200,qqc) declenche pour le widget window un appel de la fonction qqc() apres une pause de 200 millisecondes. Dans notre script, la fonction qui est appelee par la methode after() est la fonction move() elle- meme. Nous utilisons done ici pour la premiere fois une technique de programmation tres puissante, que Ton appelle recursivite. Pour faire simple, nous dirons que la recursivite est ce qui se passe lorsqu'une fonction s'appelle elle-meme. On obtient bien evidemment ainsi un bouclage, qui peut se perpetuer indefiniment si Ton ne prevoit pas aussi un moyen pour l'interrompre. Voyons comment cela fonctionne dans notre exemple : La fonction move() est invoquee une premiere fois lorsque Ton clique sur le bouton « Demarrer ». Elle effectue son travail (e'est-a-dire positionner la balle), puis elle s'invoque elle- meme apres une petite pause. Elle repart done pour un second tour, puis s'invoque elle-meme a nouveau, et ainsi de suite indefiniment... C'est du moins ce qui se passerait si nous n'avions pas pris la precaution de placer quelque part dans la boucle une instruction de sortie. En l'occurrence, il s'agit dun simple test conditionnel : a chaque iteration de la boucle, nous examinons le contenu de la variable flag a l'aide d'une instruction if. Si le contenu de la variable flag est zero, alors le bouclage ne s'effectue plus et l'animation s'arrete. flag etant une variable globale, nous pouvons aisem*nt changer sa valeur a l'aide d'autres fonctions, celles que nous avons associees aux boutons « Demarrer » et « Arreter ». Nous obtenons ainsi un mecanisme simple pour lancer ou arreter notre animation : Un premier clic sur le bouton « Demarrer » assigne une valeur non-nulle a la variable flag, puis provoque immediatement un premier appel de la fonction move(). Celle-ci s'execute et continue ensuite a s'appeler elle-meme toutes les 50 millisecondes, tant que flag ne revient pas a zero. Si Ton continue a cliquer sur le bouton « Demarrer », la fonction move() ne peut plus etre appelee tant que la valeur de flag vaut 1 . On evite ainsi le demarrage de plusieurs boucles concurrentes. Le bouton « Arreter » remet flag a zero, et la boucle s'interrompt. Exercices : 8.23. Dans la fonction start_it(), supprimez l'instruction if flag == 0: (et l'indentation des deux lignes suivantes). Que se passe-t-il ? (Cliquez plusieurs fois sur le bouton « Demarrer »). Tachez d'exprimer le plus clairement possible votre explication des faits observes. 8.24. Modifiez le programme de telle facon que la balle change de couleur a chaque « virage ». 8.25. Modifiez le programme de telle facon que la balle effectue des mouvements obliques comme une bille de billard qui rebondit sur les bandes (« en zig-zag »). 8.26. Modifiez le programme de maniere a obtenir d'autres mouvements. Tachez par exemple d'obtenir un mouvement circulaire. (Comme dans les exercices de la page 104). 106. Gerard Swinnen : Apprendre a programmer avec Python 8.27. Modifiez ce programme, ou bien ecrivez-en un autre similaire, de maniere a simuler le mouvement d'une balle qui tombe (sous l'effet de la pesanteur), et rebondit sur le sol. Attention : il s'agit cette fois de mouvements acceleres ! 8.28. A partir des scripts precedents, vous pouvez a present ecrire un programme de jeu fonctionnant de la maniere suivante : Une balle se deplace au hasard sur un canevas, a vitesse faible. Le joueur doit essayer de cliquer sur cette balle a l'aide de la souris. S'il y arrive, il gagne un point mais la balle se deplace desormais un peu plus vite, et ainsi de suite. Arreter le jeu apres un certain nombre de clics et afficher le score atteint. 8.29. Variante du jeu precedent : chaque fois que le joueur parvient a « l'attraper », la balle devient plus petite (elle peut egalement changer de couleur). 8.30. Ecrivez un programme dans lequel evoluent plusieurs balles de couleurs differentes, qui rebondissent les unes sur les autres ainsi que sur les parois. 8.31. Perfectionnez le jeu des precedents exercices en y integrant l'algorithme ci-dessus. II s'agit a present pour le joueur de cliquer seulement sur la balle rouge. Un clic errone (sur une balle d'une autre couleur) lui fait perdre des points. 8.32. Ecrivez un programme qui simule le mouvement de 2 planetes tournant autour du soleil sur des orbites circulaires differentes (ou deux electrons tournant autour d'un noyau d'atome...). 8.33. Ecrivez un programme pour le jeu du serpent : un « serpent » (constitute en faite d'une courte ligne de carres) se deplace sur le canevas dans l'une des 4 directions : droite, gauche, haut, bas. Le joueur peut a tout moment changer la direction suivie par le serpent a l'aide des touches flechees du clavier. Sur le canevas se trouvent egalement des « proies » (des petit* cercles fixes disposes au hasard). II faut diriger le serpent de maniere a ce qu'il « mange » les proies sans arriver en contact avec les bords du canevas. A chaque fois qu'une proie est mangee, le serpent s'allonge d'un carre, le joueur gagne un point, et une nouvelle proie apparait ailleurs. La partie s'arrete lorsque le serpent touche l'une des parois, ou lorsqu'il a atteint une certaine taille. 8.34. Perfectionnement du jeu precedent : la partie s'arrete egalement si le serpent « se recoupe ». Gerard Swinnen : Apprendre a programmer avec Python 107. Chapitre 9 : Les fichiers Jusqu'a present, les programmes que nous avons realises ne traitaient qu'un tres petit nombre de donnees. Nous pouvions done a chaque fois inclure ces donnees dans le corps du programme lui- meme (par exemple dans une liste). Cette facon de proceder devient cependant tout a fait inadequate lorsque Ton souhaite traiter une quantite d'information plus importante. 9. 1 Utilite des fichiers Imaginons par exemple que nous voulons ecrire un petit programme exerciseur qui fasse apparaitre a l'ecran des questions a choix multiple, avec traitement automatique des reponses de l'utilisateur. Comment allons-nous memoriser le texte des questions elles-memes ? L'idee la plus simple consiste a placer chacun de ces textes dans une variable, en debut de programme, avec des instructions d'affectation du genre : a = "Quelle est la capitale du Guatemala ?" b = "Qui a succede a Henri IV ?" c = "Combien font 26 x 43 ?" . . . etc. Cette idee est malheureusem*nt beaucoup trop simpliste. Tout va se compliquer en effet lorsque nous essayerons d'elaborer la suite du programme, e'est-a-dire les instructions qui devront servir a selectionner au hasard l'une ou l'autre de ces questions pour les presenter a l'utilisateur. Employer par exemple une longue suite d'instructions if ... elif ... elif ... comme dans l'exemple ci-dessous n'est certainement pas la bonne solution (ce serait d'ailleurs bien penible a ecrire : n'oubliez pas que nous souhaitons traiter un grand nombre de questions !) : if choix == 1 : selection = a elif choix == 2 : selection = b elif choix == 3 : selection = c . . . etc. La situation se presente deja beaucoup mieux si nous faisons appel a une liste : liste = ["Qui a vaincu Napoleon a Waterloo ?", "Comment traduit-on ' inf ormatique ' en anglais ?", "Quelle est la formule chimique du methane ?", ... etc ...] On peut en effet extraire n'importe quel element de cette liste a l'aide de son indice. Exemple : print liste [2] ===> "Quelle est la formule chimique du methane ?" (rappel : rindi?age commence a partir de zero) 108. Gerard Swinnen : Apprendre a programmer avec Python Meme si cette facon de proceder est deja nettement meilleure que la precedente, nous sommes toujours confrontes a plusieurs problemes genants : ♦ La lisibilite du programme va se deteriorer tres vite lorsque le nombre de questions deviendra important. En corollaire, nous accroitrons la probability d'inserer l'une ou l'autre erreur de syntaxe dans la definition de cette longue liste. De telles erreurs seront bien difficiles a debusquer. ♦ L'ajout de nouvelles questions, ou la modification de certaines d'entre elles, imposeront a chaque fois de rouvrir le code source du programme. En corollaire, il deviendra malaise de retravailler ce meme code source, puisqu'il comportera de nombreuses lignes de donnees encombrantes. ♦ L'echange de donnees avec d'autres programmes (peut-etre ecrits dans d'autres langages) est tout simplement impossible, puisque ces donnees font partie du programme lui-meme. Cette derniere remarque nous suggere la direction a prendre : il est temps que nous apprenions a separer les donnees, et les programmes qui les traitent, dans des fichiers differents. Pour que cela devienne possible, nous devrons doter nos programmes de divers mecanismes permettant de creer des fichiers, d'y envoyer des donnees et de les recuperer par apres. Les langages de programmation proposent des jeux d'instructions plus ou moins sophistiques pour effectuer ces taches. Lorsque les quantites de donnees deviennent tres importantes, il devient d'ailleurs rapidement necessaire de structurer les relations entre ces donnees, et Ton doit alors elaborer des systemes appeles bases de donnees relationnelles, dont la gestion peut s'averer tres complexe. Ce sera la l'affaire de logiciels tres specialises tels que Oracle, IBM DB, Adabas, PostgreSQL, MySQL, etc. Python est parfaitement capable de dialoguer avec ces systemes, mais nous laisserons cela pour un peu plus tard (voir : « Gestion dune base de donnees », page 249). Nos ambitions presentes sont plus modestes. Nos donnees ne se comptent pas encore par centaines de milliers, aussi nous pouvons nous contenter de mecanismes simples pour les enregistrer dans un fichier de taille moyenne, et les en extraire ensuite. 9.2 Travailler avec des fichiers L'utilisation d'un fichier ressemble beaucoup a l'utilisation d'un livre. Pour utiliser un livre, vous devez d'abord le trouver (a l'aide de son titre), puis l'ouvrir. Lorsque vous avez fini de l'utiliser, vous le refermez. Tant qu'il est ouvert, vous pouvez y lire des informations diverses, et vous pouvez aussi y ecrire des annotations, mais generalement vous ne faites pas les deux a la fois. Dans tous les cas, vous pouvez vous situer a l'interieur du livre, notamment en vous aidant des numeros de pages. Vous lisez la plupart des livres en suivant l'ordre normal des pages, mais vous pouvez aussi decider de consulter n'importe quel paragraphe dans le desordre. Tout ce que nous venons de dire des livres s'applique aussi aux fichiers informatiques. Un fichier se compose de donnees enregistrees sur votre disque dur, sur une disquette ou sur un CD-ROM. Vous y accedez grace a son nom (lequel peut inclure aussi un nom de repertoire). Vous pouvez toujours considerer le contenu d'un fichier comme une suite de caracteres, ce qui signifie que vous pouvez traiter ce contenu, ou une partie quelconque de celui-ci, a l'aide des fonctions servant a traiter les chaines de caracteres. Gerard Swinnen : Apprendre a programmer avec Python 109. 9.3 Noms de fichiers - Repertoire courant Pour simplifier les explications qui vont suivre, nous indiquerons seulement des noms simples pour les fichiers que nous allons manipuler. Si vous procedez ainsi dans vos exercices, les fichiers en question seront crees et/ou recherches par Python dans le repertoire courant. Celui-ci est habituellement le repertoire ou se trouve le script lui-meme, sauf si vous lancez ce script depuis la fenetre d'un shell IDLE, auquel cas le repertoire courant est defini au lancement de IDLE lui-meme (Sous Windows, la definition de ce repertoire fait partie des proprietes de l'icone de lancement). Si vous travaillez avec IDLE, vous souhaiterez done certainement forcer Python a changer son repertoire courant, afin que celui-ci corresponde a vos attentes. Pour ce faire, utilisez les commandes suivantes en debut de session. (Nous supposons ici que le repertoire vise est le repertoire /home/jules/exercices . Vous pouvez franchement utiliser cette syntaxe (e'est-a-dire des caracteres / et non \ en guise de separateurs : e'est la convention en vigueur dans le monde Unix). Python effectuera automatiquement les conversions necessaires, suivant que vous travaillez sous MacOS, Linux, ou Windows?* »> from os import chdir >» chdir ( " /home/jules/exercices " ) La premiere commande importe la fonction chdir() du module os. Le module os contient toute une serie de fonctions permettant de dialoguer avec le systeme d'exploitation (os = operating system), quel que soit celui-ci. La seconde commande provoque le changement de repertoire (« chdir » = « change directory ») Notes : • Vous avez egalement la possibility d'inserer ces commandes en debut de script, ou encore d'indiquer le chemin d'acces complet dans le nom des fichiers que vous manipulez, mais cela risque peut-etre d'alourdir l'ecriture de vos programmes. • Choisissez de preference des noms de fichiers courts. Evitez dans toute la mesure du possible les caracteres accentues, les espaces et les signes typographiques speciaux. 9.4 Les deux formes d'importation Les lignes destructions que nous venons d'utiliser sont l'occasion d'expliquer un mecanisme interessant. Vous savez qu'en complement des fonctions integrees dans le module de base, Python met a votre disposition une tres grande quantite de fonctions plus specialisees, qui sont regroupees dans des modules. Ainsi vous connaissez deja fort bien le module math et le module Tkinter. Pour utiliser les fonctions d'un module, il suffit de les importer. Mais cela peut se faire de deux manieres differentes, comme nous allons le voir ci-dessous. Chacune des deux methodes presente des avantages et des inconvenients. Voici un exemple de la premiere methode : >>>>>> import os >» rep_cour = os.getcwd() >>> print rep_cour C : \Python22\essais 38 Dans le cas de Windows, vous pouvez egalement inclure dans ce chemin la lettre qui designe le peripherique de stockage ou se trouve le fichier. Par exemple : "D:/home/jules/exercices". 110. Gerard Swinnen : Apprendre a programmer avec Python La premiere ligne de cet exemple importe I'integralite du module os, lequel contient de nombreuses fonctions interessantes pour Faeces au systeme d'exploitation. La seconde ligne utilise la fonction getcwd() du module os 39 Comme vous pouvez le constater, la fonction getcwd() renvoie le nom du repertoire courant (getcwd = get current working directory). Par comparaison, voici un exemple similaire utilisant la seconde methode d'importation : »> from os import getcwd »> rep_cour = getcwd () »> print rep_cour C : \Python22\essais Dans ce nouvel exemple, nous n'avons importe du module os que la fonction getcwd() seule. Importee de cette maniere, la fonction s'integre a notre propre code comme si nous l'avions ecrite nous-memes. Dans les lignes ou nous l'utilisons, il n'est pas necessaire de rappeler qu'elle fait partie du module os. Nous pouvons de la meme maniere importer plusieurs fonctions du meme module : »> from math import sqrt, pi, sin, cos »> print pi 3.14159265359 »> print sqrt (5) # racine carree de 5 2.2360679775 »> print sin (pi/6) # sinus d'un angle de 30° 0.5 Nous pouvons meme importer toutes les fonctions d'un module, comme dans : from Tkinter import * Cette methode d'importation presente l'avantage d'alleger l'ecriture du code. Elle presente l'inconvenient (surtout dans sa derniere forme, celle qui importe toutes les fonctions d'un module) d'encombrer l'espace de noms courant. II se pourrait alors que certaines fonctions importees aient le meme nom que celui d'une variable definie par vous-meme, ou encore le meme nom qu'une fonction importee depuis un autre module. (Si cela se produit, l'un des deux noms en conflit n'est evidemment plus accessible). Dans les programmes d'une certaine importance, qui font appel a un grand nombre de modules d'origines diverses, il sera done toujours preferable de privilegier plutot la premiere methode, e'est- a-dire celle qui utilise des noms pleinement qualifies. On fait generalement exception a cette regie dans le cas particulier du module Tkinter, parce que les fonctions qu'il contient sont tres sollicitees (des lors que Ton decide d'utiliser ce module). 39 Le point separateur exprime done ici line relation d'appartenance. II s'agit d'un exemple de la qualification des noms qui sera de plus en plus largement exploitee dans la suite de ce cours. Relier ainsi des noms a l'aide de points est une maniere de designer sans ambigu'fte des elements faisant partie d'ensembles, lesquels peuvent eux-memes faire partie d'ensembles plus vastes, etc. Par exemple, l'etiquette systeme.machin.truc designe l'element true, qui fait partie de l'ensemble machin, lequel fait lui-meme partie de l'ensemble systeme. Nous verrons de nombreux exemples de cette technique de designation, notamment lors de notre etude des classes d'objets. Gerard Swinnen : Apprendre a programmer avec Python 111. 9.5 Ecriture sequentielle dans un fichier Sous Python, Faeces aux fichiers est assure par l'intermediaire d'un « objet-fichier » que Ton cree a l'aide de la fonction interne open(). Apres avoir appele cette fonction, vous pouvez lire et ecrire dans le fichier en utilisant les methodes specifiques de cet objet-fichier. L'exemple ci-dessous vous montre comment ouvrir un fichier « en ecriture », y enregistrer deux chaines de caracteres, puis le refermer. Notez bien que si le fichier n'existe pas encore, il sera cree automatiquement. Par contre, si le nom utilise concerne un fichier preexistant qui contient deja des donnees, les caracteres que vous y enregistrerez viendront s'aj outer a la suite de ceux qui s'y trouvent deja. Vous pouvez faire tout cet exercice directement a la ligne de commande : >>> obFichier = open ( 'Monf ichier ' , ' a ' ) >>> obFichier . write (' Bon jour , fichier !') »> obFichier .write ("Quel beau temps, aujourd'hui !") >>> obFichier . close ( ) >>> Notes : ♦ La premiere ligne cree l'objet-fichier « obFichier », lequel fait reference a un fichier veritable (sur disque ou disquette) dont le nom sera « Monfichier ». Ne confondez pas le nom de fichier avec le nom de l'objet-fichier qui y donne acces. A la suite de cet exercice, vous pouvez verifier qu'il s'est bien cree sur votre systeme (dans le repertoire courant) un fichier dont le nom est « Monfichier » (et vous pouvez en visualiser le contenu a l'aide d'un editeur quelconque). ♦ La fonction open() attend deux arguments, qui doivent etre des chaines de caracteres. Le premier argument est le nom du fichier a ouvrir, et le second est le mode d'ouverture. « a » indique qu'il faut ouvrir ce fichier en mode « ajout » {append), ce qui signifie que les donnees a enregistrer doivent etre ajoutees a la fin du fichier, a la suite de celles qui s'y trouvent eventuellement deja. Nous aurions pu utiliser aussi le mode « w » (pour write), mais lorsqu'on utilise ce mode, Python cree toujours un nouveau fichier (vide), et l'ecriture des donnees commence a partir du debut de ce nouveau fichier. S'il existe deja un fichier de meme nom, celui-ci est efface au prealable. ♦ La methode write() realise l'ecriture proprement dite. Les donnees a ecrire doivent etre fournies en argument. Ces donnees sont enregistrees dans le fichier les unes a la suite des autres (e'est la raison pour laquelle on parle de fichier a acces sequentiel). Chaque nouvel appel de write() continue l'ecriture a la suite de ce qui est deja enregistre. ♦ La methode closeQ referme le fichier. Celui-ci est desormais disponible pour tout usage. 112. Gerard Swinnen : Apprendre a programmer avec Python 9.6 Lecture sequentielle d'un fichier Vous allez maintenant rouvrir le fichier, mais cette fois « en lecture », de maniere a pouvoir y relire les informations que vous avez enregistrees dans l'etape precedente : »> ofi = open ( 'Monf ichier ' , 'r') »> t = of i. read () »> print t Bon jour, fichier !Quel beau temps, aujourd'hui ! »> ofi. close () Comme on pouvait s'y attendre, la methode read() lit les donnees presentes dans le fichier et les transfere dans une variable de type « chaine » (string) . Si on utilise cette methode sans argument, la totalite du fichier est transferee. Notes : • Le fichier que nous voulons lire s'appelle « Monfichier ». L'instruction d'ouverture de fichier devra done necessairement faire reference a ce nom-la. Si le fichier n'existe pas, nous obtenons un message d'erreur. Exemple : »> ofi = open (' Monf icier ',' r ' ) IOError: [Errno 2] No such file or directory: 'Monf icier' Par contre, nous ne sommes tenus a aucune obligation concernant le nom a choisir pour l'objet- fichier. C'est un nom de variable quelconque. Ainsi done, dans notre premiere instruction, nous avons choisi de creer un objet-fichier « ofi », faisant reference au fichier reel « Monfichier », lequel est ouvert en lecture (argument « r »). • Les deux chaines de caracteres que nous avions entrees dans le fichier sont a present accolees en une seule. C'est normal, puisque nous n'avons fourni aucun caractere de separation lorsque nous les avons enregistrees. Nous verrons un peu plus loin comment enregistrer des lignes de texte distinctes. • La methode read() peut egalement etre utilisee avec un argument. Celui-ci indiquera combien de caracteres doivent etre lus, a partir de la position deja atteinte dans le fichier : »> ofi = open (' Monf ichier ' , 'r') »> t = ofi. read (7) »> print t Bon jour »> t = ofi. read (15) »> print t , fichier !Quel S'il ne reste pas assez de caracteres au fichier pour satisfaire la demande, la lecture s'arrete tout simplement a la fin du fichier : »> t = ofi. read (1000) »> print t beau temps, aujourd'hui ! Si la fin du fichier est deja atteinte, read() renvoie une chaine vide : »> t = of i. read () »> print t »> of i. close () Gerard Swinnen : Apprendre a programmer avec Python 113. 9.7 L 'instruction break pour sortir d'une boucle II va de soi que les boucles de programmation s'imposent lorsque Ton doit traiter un fichier dont on ne connait pas necessairement le contenu a l'avance. L'idee de base consistera a lire ce fichier morceau par morceau, jusqu'a ce que Ton ait atteint la fin du fichier. La fonction ci-dessous illustre cette idee. Elle copie l'integralite dun fichier, quelle que soit sa taille, en transferant des portions de 50 caracteres a la fois : def copieFichier (source, destination): "copie integrale d'un fichier" fs = open (source, 'r') fd = open (destination, 'w') while 1 : txt = fs. read (50) if txt =="" : break fd. write (txt) fs . close () fd. close () return Si vous voulez tester cette fonction, vous devez lui fournir deux arguments : le premier est le nom du fichier original, le second est le nom a donner au fichier qui accueillera la copie. Exemple : copieFichier ( ' Monf ichier ' , ' Tonf ichier ' ) Vous aurez remarque que la boucle while utilisee dans cette fonction est construite d'une maniere differente de ce que vous avez rencontre precedemment. Vous savez en effet que l'instruction while doit toujours etre suivie d'une condition a evaluer ; le bloc d' instructions qui suit est alors execute en boucle, aussi longtemps que cette condition reste vraie. Or nous avons remplace ici la condition a evaluer par une simple constante, et vous savez egalement 40 que l'interpreteur Python considere comme vraie toute valeur numerique differente de zero. Une boucle while construite comme nous l'avons fait ci-dessus devrait done boucler indefiniment, puisque la condition de continuation reste toujours vraie. Nous pouvons cependant interrompre ce bouclage en faisant appel a l'instruction break, laquelle permet eventuellement de mettre en place plusieurs mecanismes de sortie differents pour une meme boucle : while : instructions diverses if : break instructions diverses if : break etc . Dans notre fonction copieFichier(), il est facile de voir que l'instruction break s'executera seulement lorsque la fin du fichier aura ete atteinte. 40 Voir page 57 : Veracite/faussete d'une expression Gerard Swinnen : Apprendre a programmer avec Python 114. 9.8 Fichiers texte Un fichier texte est un fichier qui contient des caracteres imprimables et des espaces organises en lignes successives, ces lignes etant separees les unes des autres par un caractere special non- imprimable appele « marqueur de fin de ligne » 41 . II est tres facile de traiter ce genre de fichiers sous Python. Les instructions suivantes creent un fichier texte de quatre lignes : »> f = open ( "Fichiertexte" , "w") »> f . write ( "Ceci est la ligne un\nVoici la ligne deux\n") »> f . write ( "Voici la ligne trois\nVoici la ligne quatre\n") »> f. close () Notez bien le marqueur de fin de ligne « \n » insere dans les chaines de caracteres, aux endroits ou Ton souhaite separer les lignes de texte dans 1'enregistrement. Sans ce marqueur, les caracteres seraient enregistres les uns a la suite des autres, comme dans les exemples precedents. Lors des operations de lecture, les lignes d'un fichier texte peuvent etre extraites separement les unes des autres. La methode readline(), par exemple, ne lit qu'une seule ligne a la fois (en incluant le caractere de fin de ligne) : »> f = open ( ' Fichiertexte ' , ' r ' ) »> t = f .readline() »> print t Ceci est la ligne un »> print f . readline ( ) Voici la ligne deux La methode readlines() transfere toutes les lignes restantes dans une liste de chaines : »> t = f .readlines () »> print t ['Voici la ligne trois\012 ' , 'Voici la ligne quatre\012'] »> f. close () 41 Suivant le systeme d' exploitation utilise, le codage correspondant au marqueur de fin de ligne peut etre different. Sous Windows, par exemple, il s'agit d'une sequence de deux caracteres (Retour chariot et Saut de ligne), alors que dans les systemes de type Unix (comme Linux) il s'agit d'un seul saut de ligne, MacOS pour sa part utilisant un seul retour chariot. En principe, vous n'avez pas a vous preoccuper de ces differences. Lors des operations d'ecriture, Python utilise la convention en vigueur sur votre systeme d'exploitation. Pour la lecture, Python interprete correctement chacune des trois conventions (qui sont done considerees comme equivalentes). Gerard Swinnen : Apprendre a programmer avec Python 115. Remarques : • La liste apparait ci-dessus en format brut, avec des apostrophes pour delimiter les chaines, et les caracteres speciaux sous forme de codes numeriques. Vous pourrez bien evidemment parcourir cette liste (a l'aide d'une boucle while, par exemple) pour en extraire les chaines individuelles. • La methode readlines() permet done de lire l'integralite d'un fichier en une instruction seulement. Cela n'est possible toutefois que si le fichier a lire n'est pas trop gros (Puisqu'il est copie integralement dans une variable, e'est-a-dire dans la memoire vive de l'ordinateur, il faut que la taille de celle-ci soit suffisante). Si vous devez traiter de gros fichiers, utilisez plutot la methode readline() dans une boucle, comme le montrera l'exemple de la page suivante. • Notez bien que readline() est une methode qui renvoie une chaine de caracteres, alors que la methode readlines() renvoie une liste. A la fin du fichier, readline() renvoie une chaine vide, tandis que readlines() renvoie une liste vide. Le script qui suit vous montre comment creer une fonction destinee a effectuer un certain traitement sur un fichier texte. En l'occurrence, il s'agit ici de recopier un fichier texte en omettant toutes les lignes qui commencent par un caractere '#' : def filtre (source, destination) : "recopier un fichier en eliminant les lignes de remarques" fs = open (source, 'r') fd = open (destination, 'w') while 1 : txt = f s . readline () if txt == ' ' : break if txt[0] != '#' : fd. write (txt) f s . close () fd. close () return Pour appeler cette fonction, vous devez utiliser deux arguments : le nom du fichier original, et le nom du fichier destine a recevoir la copie filtree. Exemple : filtreCtest.txt', 'test_f.txt') 116. Gerard Swinnen : Apprendre a programmer avec Python 9.9 Enregistrement et restitution de variables diverses L'argument de la methode write() doit etre une chaine de caracteres. Avec ce que nous avons appris jusqu'a present, nous ne pouvons done enregistrer d'autres types de valeurs qu'en les transformant d'abord en chaines de caracteres. Nous pouvons realiser cela a l'aide de la fonction integree str() : »> x = 52 »> f .write (str (x) ) Nous verrons plus loin qu'il existe d'autres possibilites pour convertir des valeurs numeriques en chaines de caracteres (voir a ce sujet : « Formatage des chaines de caracteres », page 130). Mais la question n'est pas vraiment la. Si nous enregistrons les valeurs numeriques en les transformant d'abord en chaines de caracteres, nous risquons de ne plus pouvoir les re-transformer correctement en valeurs numeriques lorsque nous allons relire le fichier. Exemple : »> a = 5 »> b = 2.83 »> c = 67 »> f = open ( 'Monfichier ' , 'w') »> f .write (str (a) ) »> f .write (str (b) ) >» f .write (str (c) ) »> f. close () »> f = open ( 'Monfichier ' , 'r') »> print f.read() 52.8367 »> f. close () Nous avons enregistre trois valeurs numeriques. Mais comment pouvons-nous les distinguer dans la chaine de caracteres resultante, lorsque nous effectuons la lecture du fichier ? C'est impossible ! Rien ne nous indique d'ailleurs qu'il y a la trois valeurs plutot qu'une seule, ou 2, ou 4,... II existe plusieurs solutions a ce genre de problemes. L'une des meilleures consiste a importer un module Python specialise : le module pickle 42 . Voici comment il s'utilise : >» import pickle »> f = open ( 'Monfichier ' , 'w') »> pickle . dump (a, f) »> pickle . dump (b, f) »> pickle . dump (c, f) »> f. close () »> f = open ( 'Monfichier ' , 'r') »> t = pickle. load (f) »> print t, type(t) 5 »> t = pickle. load (f) »> print t, type(t) 2.83 »> t = pickle. load (f) »> print t, type(t) 67 »> f. close () 42 En anglais, le terme pickle signifie "conserver". Le module a ete nomme ainsi parce qu'il sert effectivement a enregistrer des donnees en conservant leur type. Gerard Swinnen : Apprendre a programmer avec Python 117. Pour cet exemple, on considere que les variables a, b et c contiennent les memes valeurs que dans l'exemple precedent. La fonction dump() du module pickle attend deux arguments : le premier est la variable a enregistrer, le second est l'objet fichier dans lequel on travaille. La fonction pickle.load() effectue le travail inverse, c'est-a-dire la restitution de chaque variable avec son type. Vous pouvez aisem*nt comprendre ce que font exactement les fonctions du module pickle en effectuant une lecture « classique » du fichier resultant, a l'aide de la methode readQ par exemple. 9.10 Gestion des exceptions. Les instructions try - except - else Les exceptions sont les operations qu'effectue un interpreteur ou un compilateur lorsqu'une erreur est detectee au cours de l'execution d'un programme. En regie generale, l'execution du programme est alors interrompue, et un message d'erreur plus ou moins explicite est affiche. Exemple : »> print 55/0 ZeroDivisionError : integer division or modulo (D'autres informations complementaires sont qffichees, qui indiquent notamment a quel endroit du script V erreur a ete detectee, mais nous ne les reproduisons pas ici). Le message d'erreur proprement dit comporte deux parties separees par un double point : d'abord le type d'erreur, et ensuite une information specifique de cette erreur. Dans de nombreux cas, il est possible de prevoir a l'avance certaines des erreurs qui risquent de se produire a tel ou tel endroit du programme, et d'inclure a cet endroit des instructions particulieres, qui seront activees seulement si ces erreurs se produisent. Dans les langages de niveau eleve comme Python, il est egalement possible d'associer un mecanisme de surveillance a tout un ensemble d' instructions, et done de simplifier le traitement des erreurs qui peuvent se produire dans n'importe laquelle de ces instructions. Un mecanisme de ce type s'appelle en general mecanisme de traitement des exceptions. Celui de Python utilise l'ensemble d'instructions try - except - else , qui permettent d'intercepter une erreur et d'executer une portion de script specifique de cette erreur. II fonctionne comme suit : Le bloc d'instructions qui suit directement une instruction try est execute par Python sous reserve. Si une erreur survient pendant l'execution de l'une de ces instructions, alors Python annule cette instruction fautive et execute a sa place le code inclus dans le bloc qui suit l'instruction except. Si aucune erreur ne s'est produite dans les instructions qui suivent try, alors e'est le bloc qui suit l'instruction else qui est execute (si cette instruction est presente). Dans tous les cas, l'execution du programme peut se poursuivre ensuite avec les instructions ulterieures. Considerons par exemple un script qui demande a l'utilisateur d'entrer un nom de fichier, lequel fichier etant destine a etre ouvert en lecture. Si le fichier n'existe pas, nous ne voulons pas que le programme se « plante ». Nous voulons qu'un avertissem*nt soit affiche, et eventuellement que l'utilisateur puisse essayer d'entrer un autre nom. filename = raw_input ( "Veuillez entrer un nom de fichier : ") try : f = open (filename, "r") except : print "Le fichier", filename, "est introuvable" 118. Gerard Swinnen : Apprendre a programmer avec Python Si nous estimons que ce genre de test est susceptible de rendre service a plusieurs endroits d'un programme, nous pouvons aussi l'inclure dans une fonction : def existe (fname) : try: f = open ( fname , ' r ' ) f . close () return 1 except : return 0 filename = raw_input ( "Veuillez entrer le nom du fichier : ") if existe (filename) : print "Ce fichier existe bel et bien." else : print "Le fichier", filename, "est introuvable . " II est egalement possible de faire suivre l'instruction try de plusieurs blocs except, chacun d'entre eux traitant un type d'erreur specifique, mais nous ne developperons pas ces complements ici. Veuillez consulter un ouvrage de reference sur Python si necessaire. (9) Exercices : 9.1. Ecrivez un script qui permette de creer et de relire aisem*nt un fichier texte. Votre programme demandera d'abord a l'utilisateur d'entrer le nom du fichier. Ensuite il lui proposera le choix, soit d'enregistrer de nouvelles lignes de texte, soit d'afficher le contenu du fichier. L'utilisateur devra pouvoir entrer ses lignes de texte successives en utilisant simplement la touche pour les separer les unes des autres. Pour terminer les entrees, il lui suffira d'entrer une ligne vide (c'est-a-dire utiliser la touche seule). L'affichage du contenu devra montrer les lignes du fichier separees les unes des autres de la maniere la plus nature lie (les codes de fin de ligne ne doivent pas apparaitre). 9.2. Considerons que vous avez a votre disposition un fichier texte contenant des phrases de differentes longueurs. Ecrivez un script qui recherche et affiche la phrase la plus longue. 9.3. Ecrivez un script qui genere automatiquement un fichier texte contenant les tables de multiplication de 2 a 30 (chacune d'entre elles incluant 20 termes seulement). 9.4. Ecrivez un script qui recopie un fichier texte en triplant tous les espaces entre les mots. Gerard Swinnen : Apprendre a programmer avec Python 119. 9.5. Vous avez a votre disposition un fichier texte dont chaque ligne est la representation d'une valeur numerique de type reel (mais sans exposants). Par exemple : 14.896 7894.6 123.278 etc. Ecrivez un script qui recopie ces valeurs dans un autre fichier en les arrondissant en nombres entiers (l'arrondi doit etre correct). 9.6. Ecrivez un script qui compare les contenus de deux fichiers et signale la premiere difference rencontree. 9.7. A partir de deux fichiers preexistants A et B, construisez un fichier C qui contienne alternativement un element de A, un element de B, un element de A, ... et ainsi de suite jusqu'a atteindre la fin de l'un des deux fichiers originaux. Completez ensuite C avec les elements restant sur l'autre. 9.8. Ecrivez un script qui permette d'encoder un fichier texte dont les lignes contiendront chacune les noms, prenom, adresse, code postal et n° de telephone de differentes personnes (considerez par exemple qu'il s'agit des membres d'un club) 9.9. Ecrivez un script qui recopie le fichier utilise dans l'exercice precedent, en y ajoutant la date de naissance et le sexe des personnes (l'ordinateur devra afficher les lignes une par une, et demander a l'utilisateur d'entrer pour chacune les donnees complementaires). 9.10. Considerons que vous avez fait les exercices precedents et que vous disposez a present d'un fichier contenant les coordonnees d'un certain nombre de personnes. Ecrivez un script qui permette d'extraire de ce fichier les lignes qui correspondent a un code postal bien determine. 9.1 1. Modifiez le script de l'exercice precedent, de maniere a retrouver les lignes correspondant a des prenoms dont la premiere lettre est situee entre F et M (inclus) dans l'alphabet. 9.12. Ecrivez des fonctions qui effectuent le meme travail que celles du module pickle (voir page 117). Ces fonctions doivent permettre l'enregistrement de variables diverses dans un fichier texte, en les accompagnant systematiquement d' informations concernant leur format exact. 120. Gerard Swinnen : Apprendre a programmer avec Python Chapitre 10 : Approfondir les structures de donnees Jusqu'a present, nous nous sommes contentes d' operations assez simples. Nous allons maintenant passer a la vitesse superieure. Les structures de donnees que vous utilisez deja presentent quelques caracteristiques que vous ne connaissez pas encore, et il est egalement temps de vous faire decouvrir d'autres structures plus complexes. 10.1 Le point sur les chafnes de caracteres Nous avons deja rencontre les chaines de caracteres au chapitre 5. A la difference des donnees numeriques, qui sont des entries singulieres, les chaines de caracteres (ou string) constituent un type de donnee composite. Nous entendons par la une entite bien definie qui est faite elle-meme dun ensemble d'entites plus petites, en l'occurrence : les caracteres. Suivant les circonstances, nous serons amenes a traiter une telle donnee composite, tantot comme un seul objet, tantot comme une suite ordonnee d'elements. Dans ce dernier cas, nous souhaiterons probablement pouvoir acceder a chacun de ces elements a titre individuel. En fait, les chaines de caracteres font partie d'une categorie d'objets Python que Ton appelle des sequences, et dont font partie aussi les listes et les tuples. On peut effectuer sur les sequences tout un ensemble d'operations similaires. Vous en connaissez deja quelques unes, et nous allons en decrire quelques autres dans les paragraphes suivants. 10.1.1 Concatenation, Repetition Les chaines peuvent etre concatenees avec l'operateur + et repetees avec l'operateur * : »> n = 'abc' + 'def # concatenation »> m = ' zut ! ' * 4 # repetition »> print n, m abcdef zut ! zut ! zut ! zut ! Remarquez au passage que les operateurs + et * peuvent aussi etre utilises pour l'addition et la multiplication lorsqu'ils s'appliquent a des arguments numeriques. Le fait que les memes operateurs puissent fonctionner differemment en fonction du contexte dans lequel on les utilise est un mecanisme fort interessant que Ton appelle surcharge des operateurs. Dans d'autres langages, la surcharge des operateurs n'est pas toujours possible : on doit alors utiliser des symboles differents pour l'addition et la concatenation, par exemple. 10.1.2 Indicage, extraction, longueur Les chaines sont des sequences de caracteres. Chacun de ceux-ci occupe une place precise dans la sequence. Sous Python, les elements d'une sequence sont toujours indices (ou numerates) de la meme maniere, c'est-a-dire d partir de zero. Pour extraire un caractere d'une chaine, il suffit d'indiquer son indice entre crochets : »> nom = 1 Cedric ' >» print nom[l] , nom [3], nom [5] ere II arrive aussi tres frequemment, lorsque Ton travaille avec des chaines, que Ton souhaite extraire une petite chaine hors d'une chaine plus longue. Python propose pour cela une technique simple que Ton appelle slicing (« decoupage en tranches »). Elle consiste a indiquer entre crochets les indices correspondant au debut et a la fin de la « tranche » que Ton souhaite extraire : »> ch = "Juliette" »> print ch[0:3] Jul Gerard Swinnen : Apprendre a programmer avec Python 121. Dans la tranche [n,m], le n ieme caractere est inclus, mais pas le m ieme . Si vous voulez memoriser aisem*nt ce mecanisme, il faut vous representer que les indices pointent des emplacements situes entre les caracteres, comme dans le schema ci-dessous : ch = "Juliette" 012345678 Au vu de ce schema, il n'est pas difficile de comprendre que ch[3:7] extraira « iett » Les indices de decoupage ont des valeurs par defaut : un premier indice non defini est considere comme zero, tandis que le second indice omis prend par defaut la taille de la chaine complete : »> print ch[:3] # les 3 premiers caracteres Jul »> print ch[3:] # tout sauf les 3 premiers caracteres iette (10) Exercices 10.1. Determinez vous-meme ce qui se passe lorsque l'un ou l'autre des indices de decoupage est errone, et decrivez cela le mieux possible. (Si le second indice est plus petit que le premier, par exemple, ou bien si le second indice est plus grand que la taille de la chaine). 10.2. Decoupez une grande chaine en fragments de 5 caracteres chacun. Rassemblez ces morceaux dans l'ordre inverse. 10.3. Tachez d'ecrire une petite fonction trouve() qui fera exactement le contraire de ce que fait l'operateur d'indexage (c'est-a-dire les crochets [] ). Au lieu de partir dun index donne pour retrouver le caractere correspondant, cette fonction devra retrouver l'index correspondant a un caractere donne. En d'autres termes, il s'agit d'ecrire une fonction qui attend deux arguments : le nom de la chaine a traiter et le caractere a trouver. La fonction doit fournir en retour l'index du premier caractere de ce type dans la chaine. Ainsi par exemple, l'instruction : print trouve ("Juliette & Romeo", "&") devra afficher : 9 Attention : II faut penser a tous les cas possibles. II faut notamment veiller a ce que la fonction renvoie une valeur particuliere (par exemple la valeur -1) si le caractere recherche n'existe pas dans la chaine traitee. 10.4. Ameliorez la fonction de l'exercice precedent en lui ajoutant un troisieme parametre : l'index a partir duquel la recherche doit s'effectuer dans la chaine. Ainsi par exemple, l'instruction : print trouve ("Cesar & Cleopatre", "r", 5) devra afficher : 15 (et non 4 !) 10.5. Ecrivez une fonction comptecar() qui compte le nombre d'occurrences d'un caractere donne dans une chaine. Ainsi l'instruction : print comptecar ("ananas au jus", "a") devra afficher : 4 122. Gerard Swinnen : Apprendre a programmer avec Python 10.1.3 Parcours d'une sequence. L'instruction for... in ... II arrive tres souvent que Ton doive traiter l'integralite d'une chaine caractere par caractere, du premier jusqu'au dernier, pour effectuer a partir de chacun d'eux une operation quelconque. Nous appellerons cette operation un parcours. En nous limitant aux outils Python que nous connaissons deja, nous pouvons envisager d' encoder un tel parcours sur la base de l'instruction while : nom = ' Jacqueline ' index = 0 while index < len (nom) : print nomfindex] + ' *', index = index +1 Cette boucle « parcourt » done la chaine nom pour en extraire un a un tous les caracteres, lesquels sont ensuite imprimes avec interposition d'asterisques. Notez bien que la condition utilisee avec l'instruction while est « index < len(nom) », ce qui signifie que le bouclage doit s'effectuer jusqu'a ce que Ton soit arrive a l'indice numero 9 (la chaine compte en effet 10 caracteres). Nous aurons effectivement traite tous les caracteres de la chaine, puisque ceux-ci sont indices de zero a 9. Le parcours d'une sequence est une operation tres frequente en programmation. Pour en faciliter l'ecriture, Python vous propose une structure de boucle plus appropriee, basee sur le couple d'instructions for ... in ... : Avec ces instructions, le programme ci-dessus devient : nom = ' Jacqueline ' for caract in nom: print caract + ' * ' , Comme vous pouvez le constater, cette structure de boucle est plus compacte. Elle vous evite d'avoir a definir et a incrementer une variable specifique (un « compteur ») pour gerer l'indice du caractere que vous voulez traiter a chaque iteration. La variable caract contiendra successivement tous les caracteres de la chaine, du premier jusqu'au dernier. L'instruction for permet done d'ecrire des boucles, dans lesquelles I'iteration traite successivement tous les elements d'une sequence donne'e. Dans l'exemple ci-dessus, la sequence etait une chaine de caracteres. L'exemple ci-apres demontre que Ton peut appliquer le meme traitement aux listes (et il en sera de meme pour les tuples etudies plus loin) : liste = [' chien ',' chat ',' crocodile ' ] for animal in liste : print 'longueur de la chaine', animal, '=', len (animal) L'execution de ce script donne : longueur de la chaine chien = 5 longueur de la chaine chat = 4 longueur de la chaine crocodile = 9 L'instruction for est un nouvel exemple ^instruction compose'e. N'oubliez done pas le double point obligatoire a la fin de la ligne, et l'indentation du bloc d'instructions qui suit. Le nom qui suit le mot reserve in est celui de la sequence qu'il faut traiter. Le nom qui suit le mot reserve for est celui que vous choisissez pour la variable destinee a contenir successivement tous les elements de la sequence. Cette variable est definie automatiquement (e'est-a-dire qu'il est inutile de la definir au prealable), et son type est automatiquement adapte a celui de l'element de la sequence qui est en cours de traitement (rappel : dans le cas d'une liste, tous les elements ne sont pas necessairement du meme type). Gerard Swinnen : Apprendre a programmer avec Python 123. Exemple : divers = [ ' cheval ' , 3, 17.25, [5, 'Jean']] for e in divers : print e L'execution de ce script donne : cheval 3 17 .25 [5, 'Jean'] Bien que les elements de la liste divers soient tous de types differents (une chaine de caracteres, un entier, un reel, une liste), on peut affecter successivement leurs contenus a la variable e, sans qu'il s'ensuive des erreurs (ceci est rendu possible grace au typage dynamique des variables Python). Exercices : 10.6. Dans un conte americain, huit petit* canetons s'appellent respectivement : Jack, Kack, Lack, Mack, Nack, Oack, Pack et Qack. Ecrivez un script qui genere tous ces noms a partir des deux chaines suivantes : prefixes = 'JKLMNOP' et suffixe = 'ack' Si vous utilisez une instruction for ... in ... , votre script ne devrait comporter que deux lignes. 10.7. Rechercher le nombre de mots contenus dans une phrase donnee. 10.1.4 Appartenance d'un element a une sequence. L'instruction in utilisee seule L'instruction in peut etre utilisee independamment de for, pour verifier si un element donne fait partie ou non d'une sequence. Vous pouvez par exemple vous servir de in pour verifier si tel caractere alphabetique fait partie d'un groupe bien determine : car = "e" voyelles = "aeiouyAEIOUY" if car in voyelles : print car, "est une voyelle" D'une maniere similaire, vous pouvez verifier l'appartenance d'un element a une liste : n = 5 premiers = [1, 2, 3, 5, 7, 11, 13, 17] if n in premiers : print n, "fait partie de notre liste de nombres premiers" Note : Cette instruction tres puissante effectue done a elle seule un veritable parcours de la sequence. A titre d'exercice, ecrivez les instructions qui effectueraient le meme travail a l'aide d'une boucle classique utilisant l'instruction while. 124. Gerard Swinnen : Apprendre a programmer avec Python Exercices : Note : dans les exercices ci-apres, omettez deliberement les caracteres accentues et speciaux. 10.8. Ecrivez une fonction majuscule() qui renvoie « vrai » si I'argument transmis est une majuscule. 10.9. Ecrivez une fonction qui renvoie « vrai » si I'argument transmis est un chiffre. 10.10. Ecrivez une fonction qui convertit une phrase en une liste de mots. 10.11. Utilisez les fonctions definies dans les exercices precedents pour ecrire un script qui puisse extraire d'un texte tous les mots qui commencent par une majuscule. 10.1.5 Les chames sont des sequences non modifiables Vous ne pouvez pas modifier le contenu d'une chaine existante. En d'autres termes, vous ne pouvez pas utiliser l'operateur [] dans la partie gauche d'une instruction d'affectation. Essayez par exemple d'executer le petit script suivant (qui cherche a remplacer une lettre dans une chaine) : salut = 'bonjour a tous' salut[0] = 'B' print salut Au lieu d'afficher « Bonjour a tous », ce script « leve » une erreur du genre : « TypeError: object doesn't support item assignment ». Cette erreur est provoquee a la deuxieme ligne du script. On y essaie de remplacer une lettre par une autre dans la chaine, mais cela n'est pas permis. Par contre, le script ci-dessous fonctionne : salut = 'bonjour a tous' salut = 'B' + salut [1:] print salut Dans cet autre exemple, en effet, nous ne modifions pas la chaine salut. Nous en re-creons une nouvelle avec le meme nom a la deuxieme ligne du script (a partir d'un morceau de la precedente, soit, mais qu'importe : il s'agit bien d'une nouvelle chaine). Gerard Swinnen : Apprendre a programmer avec Python 125. 10.1.6 Les chames sont comparables Tous les operateurs de comparaison dont nous avons parle a propos des instructions de controle de flux (c'est-a-dire les instructions if ... elif ... else) fonctionnent aussi avec les chaines de caracteres. Cela vous sera tres utile pour trier des mots par ordre alphabetique : mot = raw_input ( "Entrez un mot quelconque : ") if mot < "limonade" : place = "precede" elif mot > "limonade" : place = "suit" else : place = "se confond avec" print "Le mot", mot, place, "le mot 'limonade' dans 1' ordre alphabetique" Ces comparaisons sont possibles, parce que les caracteres alphabetiques qui constituent une chaine de caracteres sont memorises dans la memoire de l'ordinateur sous forme de nombres binaires dont la valeur est liee a la place qu'occupe le caractere dans l'alphabet. Dans le systeme de codage ASCII, par exemple, A=65, B=66, C=67, etc. 43 10.1.7 Classem*nt des caracteres II est souvent utile de pouvoir determiner si tel caractere extrait d'une chaine est une lettre majuscule ou minuscule, ou plus generalement encore, de determiner s'il s'agit bien d'une lettre, d'un chiffre, ou encore d'un autre caractere typographique. Nous pouvons bien entendu ecrire differentes fonctions pour assurer ces taches. Une premiere possibility consiste a utiliser l'instruction in comme nous l'avons vu dans un precedent paragraphe. Mais puisque nous savons desormais que les caracteres forment une suite bien ordonnee dans le code ASCII, nous pouvons exploiter d'autres methodes. Par exemple, la fonction ci-dessous renvoie « vrai » si l'argument qu'on lui passe est une minuscule : def minuscule (ch) : if 'a' <= ch <= 'z' : return 1 else : return 0 43 En fait, il existe plusieurs systemes de codage : les plus connus sont les codages ASCII et ANSI, assez proches l'un de l'autre sauf en ce qui concerne les caracteres particuliers specifiques des langues autres que l'anglais (caracteres accentues, cedilles, etc.). Un nouveau systeme de codage integrant tous les caracteres speciaux de toutes les langues mondiales est apparu depuis quelques annees. Ce systeme appele Unicode devrait s'imposer petit a petit. Python l'integre a partir de sa version 2. 126. Gerard Swinnen : Apprendre a programmer avec Python Exercices : Note : dans les exercices ci-apres, omettez deliberement les caracteres accentues et speciaux. 10.12. Ecrivez une fonction majuscule() qui renvoie « vrai » si l'argument transmis est une majuscule (utilisez une autre methode que celle exploitee precedemment) 10.13. Ecrivez une fonction qui renvoie « vrai » si l'argument transmis est un caractere alphabetique quelconque (majuscule ou minuscule). Dans cette nouvelle fonction, utilisez les fonctions minuscule() et majuscule() definies auparavant. 10.14. Ecrivez une fonction qui renvoie « vrai » si l'argument transmis est un chiffre. 10.15. Ecrivez une fonction qui renvoie le nombre de caracteres majuscules contenus dans une phrase donnee en argument. Afin que vous puissiez effectuer plus aisem*nt toutes sortes de traitements sur les caracteres, Python met a votre disposition un certain nombre de fonctions predefinies : La fonction ord(ch) accepte n'importe quel caractere comme argument. En retour, elle fournit le code ASCII correspondant a ce caractere. Ainsi ord('A') renvoie la valeur 65. La fonction chr(num) fait exactement le contraire. L'argument qu'on lui transmet doit etre un entier compris entre 0 et 255. En retour, on obtient le caractere ASCII correspondant : Ainsi chr(65) renvoie le caractere A. Exercices : Note : dans les exercices ci-apres, omettez deliberement les caracteres accentues et speciaux. 10.16. Ecrivez un petit script qui affiche une table des codes ASCII. Le programme doit afficher tous les caracteres en regard des codes correspondants. A partir de cette table, etablissez les relations numeriques reliant chaque caractere majuscule a chaque caractere minuscule. 10.17. A partir des relations trouvees dans l'exercice precedent, ecrivez une fonction qui convertit tous les caracteres d'une phrase donnee en minuscules. 10.18. A partir des memes relations, ecrivez une fonction qui convertit tous les caracteres minuscules en majuscules, et vice-versa (dans une phrase fournie en argument). 10.19. Ecrivez une fonction qui compte le nombre de fois qu'apparait tel caractere (fourni en argument) dans une phrase donnee. 10.20. Ecrivez une fonction qui renvoie le nombre de voyelles contenues dans une phrase donnee. Gerard Swinnen : Apprendre a programmer avec Python 127. 10.1.8 Les chatnes sont des objets Dans les chapitres precedents, vous avez deja rencontre de nombreux objets. Vous savez done que Ton peut agir sur un objet a l'aide de methodes (e'est-a-dire des fonctions associees a cet objet). Sous Python, les chaines de caracteres sont des objets. On peut done effectuer de nombreux traitements sur les chaines de caracteres en utilisant des methodes appropriees. En voici quelques- unes, choisies parmi les plus utiles 44 : • split() : convertit une chaine en une liste de sous-chaines. On peut choisir le caractere separateur en le fournissant comme argument, sinon e'est un espace, par defaut : »> c2 ="Votez pour moi" >» a = c2. split () >>> print a ['Votez', 'pour', 'moi'] »> c4 ="Cet exemple, parmi d'autres, peut encore servir" »> c4. split (", ") ['Cet exemple', " parmi d'autres", ' peut encore servir'] • join(liste) : rassemble une liste de chaines en une seule (Cette methode fait done l'inverse de la precedente). Attention : la chaine a laquelle on applique cette methode est celle qui servira de separateur (un ou plusieurs caracteres); l'argument transmis est la liste des chaines a rassembler : »> b2 = ["Salut", "les", "copains"] »> print " M .join(b2) Salut les copains >» print " ".join(b2) Salut les copains • find(sch) : cherche la position d'une sous-chaine sch dans la chaine : »> chl = "Cette lecon vaut bien un fromage, sans doute ?" »> ch2 = "fromage" »> print chl . find (ch2) 25 • count(sch) : compte le nombre de sous-chaines sch dans la chaine : >>> chl = "Le heron au long bee emmanche d'un long cou" »> ch2 = 'long' >>> print chl . count (ch2) 2 • lower() : convertit une chaine en minuscules : >» ch = "ATTENTION : Danger !" >>> print ch. lower () attention : danger ! 44 II s'agit de quelques exemples seulement. La plupart de ces methodes peuvent etre utilisees avec differents parametres que nous n'indiquons pas tous ici (par exemple, certains parametres permettent de ne traiter qu'une partie de la chaine). Vous pouvez obtenir la liste complete de toutes les methodes associees a un objet a l'aide de la fonction integree dir(). Veuillez consulter l'un ou l'autre des ouvrages de reference (ou la documentation en ligne) si vous souhaitez en savoir davantage. 128. Gerard Swinnen : Apprendre a programmer avec Python • upper() : convertit une chaine en majuscules : »> ch = "Merci beaucoup" »> print ch . upper ( ) MERCI BEAUCOUP • capitalize() : convertit en majuscule la premiere lettre d'une chaine : »> b3 = "quel beau temps, aujourd'hui !" »> print b3 . capitalize () "Quel beau temps, aujourd'hui !" • swapcase() : convertit toutes les majuscules en minuscules et vice-versa : »> ch5 = "La CIGALE et la FOURMI" »> print ch5 . swapcase ( ) 1A cigale ET LA fourmi • strip() : enleve les espaces eventuels au debut et a la fin de la chaine : »> ch = " Monty Python »> ch. strip () ' Monty Python ' • replace(cl, c2) : remplace tous les caracteres cl par des caracteres c2 dans la chaine : »> ch8 = "Si ce n'est toi c'est done ton frere" »> print ch8 . replace ( " ","*") Si*ce*n ' est*toi*c ' est*donc*ton*f rere index(c) : retrouve l'index de la premiere occurrence du caractere c dans la chaine : »> ch9 ="Portez ce vieux whisky au juge blond qui fume" »> print ch9. index ("w") 16 Dans la plupart de ces methodes, il est possible de preciser quelle portion de la chaine doit etre traitee, en ajoutant des arguments supplementaires. Exemple : »> print ch9. index ("e") # cherche a partir du debut de la chaine 4 # et trouve le premier ' e ' »> print ch9 . index ( "e" , 5) # cherche seulement a partir de 1 ' indice 5 8 # et trouve le second ' e ' »> print ch9 . index ("e" , 15) # cherche a partir du caractere n° 15 29 # et trouve le quatrieme 'e' Etc., etc. Comprenez bien qu'il n'est pas possible de decrire toutes les methodes disponibles ainsi que leur parametrage dans le cadre de ce cours. Si vous souhaitez en savoir davantage, il vous faut consulter la documentation en ligne de Python (Library reference), ou un bon ouvrage de reference (comme par exemple la « Python Standard Library » de Fredrik Lundh - Editions O'Reilly). Gerard Swinnen : Apprendre a programmer avec Python 129. Fonctions integrees A toutes fins utiles, rappelons egalement ici que Ton peut aussi appliquer aux chaines un certain nombre de fonctions integrees dans le langage lui-meme. : • len(ch) renvoie la longueur de la chaine ch (c'est-a-dire son nombre de caracteres) • float(ch) convertit la chaine ch en un nombre reel (float) (bien entendu, cela ne pourra fonctionner que si la chaine represente bien un tel nombre) : »> a = float ("12.36") »> print a + 5 17.36 • int(ch) convertit la chaine ch en un nombre entier : »> a = int("184") »> print a + 20 204 10.1.9 Formatage des chaines de caracteres Pour terminer ce tour d'horizon des fonctionnalites associees aux chaines de caracteres, il nous semble utile de vous presenter encore une technique que Ton appelle formatage. Cette technique se revele particulierement utile dans tous les cas ou vous devez construire une chaine de caracteres complexe a partir d'un certain nombre de morceaux, tels que les valeurs de variables diverses. Considerons par exemple que vous avez ecrit un programme qui traite de la couleur et de la temperature d'une solution aqueuse, en chimie. La couleur est memorisee dans une chaine de caracteres nommee coul, et la temperature dans une variable nommee temp (variable de type float). Vous souhaitez a present que votre programme construise une nouvelle chaine de caracteres a partir de ces donnees, par exemple une phrase telle que la suivante : « La solution est devenue rouge et sa temperature atteint 12,7 °C ». Vous pouvez construire cette chaine en assemblant des morceaux a l'aide de l'operateur de concatenation (le symbole +), mais il vous faudra aussi utiliser la fonction str() pour convertir en chaine de caracteres la valeur numerique contenue dans la variable de type float (faites l'exercice). Python vous offre une autre possibilite. Vous pouvez construire votre chaine en assemblant deux elements a l'aide de l'operateur % : a gauche vous fournissez une chaine de formatage (un patron, en quelque sorte) qui contient des marqueurs de conversion, et a droite (entre parentheses) un ou plusieurs objets que Python devra inserer dans la chaine, en lieu et place des marqueurs. Exemple : »> coul ="verte" »> temp = 1.347 + 15.9 »> print "La couleur est %s et la temperature vaut %s °C" % (coul, temp) La couleur est verte et la temperature vaut 17.247 °C Dans cet exemple, la chaine de formatage contient deux marqueurs de conversion %s qui seront remplaces respectivement par les contenus des deux variables coul et temp. 130. Gerard Swinnen : Apprendre a programmer avec Python Le marqueur %s accepte n'importe quel objet (chaine, entier, float, ...). Vous pouvez experimenter d'autres mises en forme en utilisant d'autres marqueurs. Essayez par exemple de remplacer le deuxieme %s par %d , ou par %8 . 2f , ou encore par %8 . 2g . Le marqueur %d attend un nombre et le convertit en entier ; les marqueurs %f et %g attendent des reels et peuvent determiner la largeur et la precision qui seront affichees. La description complete de toutes les possibilites de formatage sort du cadre de ces notes. S'il vous faut un formatage tres particulier, veuillez consulter la documentation en ligne de Python ou des manuels plus specialises. Exercices : 10.21. Ecrivez un script qui compte dans un fichier texte quelconque le nombre de lignes contenant des caracteres numeriques. 10.22. Ecrivez un script qui compte le nombre de mots contenus dans un fichier texte. 10.23. Ecrivez un script qui recopie un fichier texte en veillant a ce que chaque ligne commence par une majuscule. 10.24. Ecrivez un script qui recopie un fichier texte en fusionnant (avec la precedente) les lignes qui ne commencent pas par une majuscule. 10.25. Vous disposez dun fichier contenant des valeurs numeriques. Considerez que ces valeurs sont les diametres d'une serie de spheres. Ecrivez un script qui utilise les donnees de ce fichier pour en creer un autre, organise en lignes de texte qui exprimeront « en clair » les autres caracteristiques de ces spheres (surface de section, surface exterieure et volume), dans des phrases telles que : Diam. 46.20 cm Section = 3 1676. 39 cm 2 Surf . = 6705 .54 cm 2 . Vol. = 51632 .67 Diam. cm 3 Diam. cm 3 Diam. cm 3 Diam. cm 3 etc. 120.00 cm Section = 11309. 73 cm 2 Surf. = 45238 . 93 cm 2 . Vol . = 904778 . 68 0.03 cm Section = 0. 00 cm 2 Surf. 0. 00 cm 2 . Vol . 0. 00 13.90 cm Section = 151. 75 cm 2 Surf. 606. 99 cm 2 . Vol. 1406. .19 88.80 cm Section = 6193 .21 cm 2 Surf. = 24772 .84 cm 2 . Vol. = 366638 .04 10.26. Vous avez a votre disposition un fichier texte dont les lignes representent des valeurs numeriques de type reel, sans exposant (et encodees sous forme de chaines de caracteres). Ecrivez un script qui recopie ces valeurs dans un autre fichier en les arrondissant de telle sorte que leur partie decimale ne comporte plus qu'un seul chiffre apres la virgule, celui-ci ne pouvant etre que 0 ou 5 (l'arrondi doit etre correct). Gerard Swinnen : Apprendre a programmer avec Python 131. 10.2 Le point sur les listes Nous avons deja rencontre les listes a plusieurs reprises, depuis leur presentation sommaire au chapitre 5. Les listes sont des collections ordonnees d'objets. Comme les chaines de caracteres, les listes font partie d'un type general que Ton appelle sequences sous Python. Comme les caracteres dans une chaine, les objets places dans une liste sont rendus accessibles par l'intermediaire d'un index (un nombre qui indique l'emplacement de l'objet dans la sequence). 10.2.1 Definition d'une liste - Acces a ses elements Vous savez deja que Ton delimite une liste a l'aide de crochets : >» nombres = [5, 38, 10, 25] »> mots = ["jambon", "fromage", "confiture", "chocolat"] »> stuff = [5000, "Brigitte", 3.1416, ["Albert", "Rene", 1947]] Dans le dernier exemple ci-dessus, nous avons rassemble un entier, une chaine, un reel et meme une liste, pour vous rappeler que Ton peut combiner dans une liste des donnees de n'importe quel type, y compris des listes, des dictionnaires et des tuples (ceux-ci seront etudies plus loin). Pour acceder aux elements d'une liste, on utilise les memes methodes (index, decoupage en tranches) que pour acceder aux caracteres d'une chaine : >>> print nombres [2] 10 >>> print nombres [1:3] [38, 10] >>> print nombres [2: 3] [10] >>> print nombres [2:] [10, 25] >>> print nombres [: 2] [5, 38] >>> print nombres [-1] 25 >>> print nombres [-2] 10 Les exemples ci-dessus devraient attirer votre attention sur le fait qu'une tranche decoupee dans une liste est toujours elle-meme une liste (meme s'il s'agit d'une tranche qui ne contient qu'un seul element, comme dans notre troisieme exemple), alors qu'un element isole peut contenir n'importe quel type de donnee. Nous allons approfondir cette distinction tout au long des exemples suivants. 132. Gerard Swinnen : Apprendre a programmer avec Python 10.2.2 Les listes sont modifiables Contrairement aux chaines de caracteres, les listes sont des sequences modifiables. Cela nous permettra de construire plus tard des listes de grande taille, morceau par morceau, d'une maniere dynamique (c'est-a-dire a l'aide d'un algorithme quelconque). Exemples : »> nombres[0] = 17 »> nombres [17, 38, 10, 25] Dans l'exemple ci-dessus, on a remplace le premier element de la liste nombres, en utilisant l'operateur [ ] (operateur d'indigage) a la gauche du signe egale. Si Ton souhaite acceder a un element faisant partie d'une liste, elle-meme situee dans une autre liste, il suffit d'indiquer les deux index entre crochets successifs : »> stuff [3] [1] = "Isabelle" »> stuff [5000, 'Brigitte', 3.1415999999999999, ['Albert', 'Isabelle', 1947]] Comme c'est le cas pour toutes les sequences, il ne faut jamais oublier que la numerotation des elements commence a partir de zero. Ainsi, dans l'exemple ci-dessus on remplace l'element n° 1 d'une liste, qui est elle-meme l'element n° 3 d'une autre liste : la liste stuff. 10.2.3 Les listes sont des objets Sous Python, les listes sont des objets a part entiere, et vous pouvez done leur appliquer un certain nombre de methodes particulierement efficaces : »> nombres = [17, 38, 10, 25, 72] >» nombres . sort ( ) »> nombres [10, 17, 25, 38, 72] »> nombres . append (12 ) »> nombres [10, 17, 25, 38, 72, 12] »> nombres . reverse ( ) »> nombres [12, 72, 38, 25, 17, 10] >» nombres . index (17) 4 >» nombres . remove (38) »> nombres [12, 72, 25, 17, 10] # trier la liste # ajouter un element a la fin # inverser 1 ' ordre des elements # retrouver 1 ' index d'un element # enlever (effacer) un element En plus de ces methodes, vous disposez encore de l'instruction integree del , qui vous permet d'effacer un ou plusieurs elements a partir de leur(s) index : »> del nombres [2] »> nombres [12, 72, 17, 10] »> del nombres [1:3] »> nombres [12, 10] Gerard Swinnen : Apprendre a programmer avec Python 133. Notez bien la difference entre la methode remove() et l'instruction del : del travaille avec un index ou une tranche d'index, tandis que remove() recherche une valeur (si plusieurs elements de la liste possedent la meme valeur, seul le premier est efface). Exercices : 10.27. Ecrivez un script qui genere la liste des carres et des cubes des nombres de 20 a 40. 10.28. Ecrivez un script qui cree automatiquement la liste des sinus des angles de 0° a 90° , par pas de 5°. Attention : la fonction sin() du module math considere que les angles sont fournis en radians (360° = 2 % radians) 10.29. Ecrivez un script qui permette d'obtenir a l'ecran les 15 premiers termes des tables de multiplication par 2, 3, 5, 7, 11, 13, 17, 19 (ces nombres seront places au depart dans une liste) sous la forme d'une table similaire a la suivante : 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 3 6 9 12 15 18 21 24 27 30 33 36 39 42 45 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 etc. 10.30. Soit la liste suivante : ['Jean-Michel', 'Marc', 'Vanessa', 'Anne', 'Maximilien', Alexandre-Benoit', 'Louise'] Ecrivez un script qui affiche chacun de ces noms avec le nombre de caracteres correspondant. 10.31. Vous disposez d'une liste de nombres entiers quelconques, certains d'entre eux etant presents en plusieurs exemplaires. Ecrivez un script qui recopie cette liste dans une autre, en omettant les doublons. La liste finale devra etre triee. 10.32. Ecrivez un script qui recherche le mot le plus long dans une phrase donnee (l'utilisateur du programme doit pouvoir entrer une phrase de son choix). 10.33. Ecrivez un script capable d'afficher la liste de tous les jours d'une annee imaginaire, laquelle commencerait un Jeudi. Votre script utilisera lui-meme trois listes : une liste des noms de jours de la semaine, une liste des noms des mois, et une liste des nombres de jours que comportent chacun des mois (ne pas tenir compte des annees bissextiles). Exemple de sortie : Jeudi 1 Janvier Vendredi 2 Janvier Samedi 3 Janvier Dimanche 4 Janvier ... et ainsi de suite jusqu'au Jeudi 31 Decembre. 10.34. Vous avez a votre disposition un fichier texte qui contient un certain nombre de noms d'eleves. Ecrivez un script qui effectue une copie triee de ce fichier. 10.35. Ecrivez une fonction permettant de trier une liste. Cette fonction ne pourra pas utiliser la methode integree sort() de Python : Vous devez done definir vous-meme l'algorithme de tri. (Note : cette question devra faire I'objet d'une discussion-synthese en classe) 134. Gerard Swinnen : Apprendre a programmer avec Python 10.2.4 Techniques de « slicing » avance pour modifier une liste Comme nous venons de le signaler, vous pouvez ajouter ou supprimer des elements dans une liste en utilisant une instruction (del) et une methode (append()) integrees. Si vous avez bien assimile le principe du « decoupage en tranches » (slicing), vous pouvez cependant obtenir les memes resultats a l'aide du seul operateur [ ]. L'utilisation de cet operateur est un peu plus delicate que celle d'instructions ou de methodes dediees, mais elle permet davantage de souplesse : a) Insertion d'un ou plusieurs elements n'importe ou dans une liste »> mots = [ ' jambon ' , ' f romage ' , ' confiture ' , ' chocolat ' ] »> mots [2: 2] =["miel"] »> mots ['jambon', ' f romage ' , 'miel', 'confiture', 'chocolat'] »> mots [5: 5] = [ ' saucisson ' , 'ketchup'] >» mots ['jambon', ' f romage ' , 'miel', 'confiture', 'chocolat', 'saucisson', 'ketchup'] Pour utiliser cette technique, vous devez prendre en compte les particularites suivantes : • Si vous utilisez l'operateur [ ] a la gauche du signe egale pour effectuer une insertion ou une suppression d'element(s) dans une liste, vous devez obligatoirement y indiquer une « tranche » dans la liste cible (c'est-a-dire deux index reunis par le symbole : ), et non un element isole dans cette liste. • L'element que vous fournissez a la droite du signe egale doit lui-meme etre une liste. Si vous n'inserez qu'un seul element, il vous faut done le presenter entre crochets pour le transformer d'abord en une liste d'un seul element. Notez bien que l'element mots[l] n'est pas une liste (e'est la chaine « fromage »), alors que l'element mots[l:3] en est une. Vous comprendrez mieux ces contraintes en analysant ce qui suit : b) Suppression / remplacement d'elements »> mots [2: 5] = [] # [] designe une liste vide »> mots [ ' jambon ' , ' fromage ' , ' saucisson ' , ' ketchup ' ] »> mots [1:3] = [ ' salade ' ] >» mots ['jambon', 'salade', 'ketchup'] »> mots[l:] = ['mayonnaise', 'poulet', 'tomate'] »> mots ['jambon', 'mayonnaise', 'poulet', 'tomate'] • A la premiere ligne de cet exemple, nous remplacons la tranche [2:5] par une liste vide, ce qui correspond a un effacement. • A la quatrieme ligne, nous remplacons une tranche par un seul element. (Notez encore une fois que cet element doit lui-meme etre « presente » comme une liste). • A la T ligne, nous remplacons une tranche de deux elements par une autre qui en comporte 3. Gerard Swinnen : Apprendre a programmer avec Python 135. 10.2.5 Creation d'une liste de nombres a I'aide de la fonction rangeQ Si vous devez manipuler des sequences de nombres, vous pouvez les creer tres aisem*nt a I'aide de cette fonction : »> range (10) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] La fonction range() genere une liste de nombres entiers de valeurs croissantes. Si vous appelez range() avec un seul argument, la liste contiendra un nombre de valeurs egal a l'argument fourni, mais en commencant a partir de zero (c'est-d-dire que range(n) genere les nombres de 0 a n-1). Notez bien que l'argument fourni n'est jamais dans la liste generee. On peut aussi utiliser range() avec deux, ou meme trois arguments separes par des virgules, afin de generer des sequences de nombres plus specifiques : »> range (5, 13) [5, 6, 7, 8, 9, 10, 11, 12] »> range (3, 16, 3) [3, 6, 9, 12, 15] Si vous avez du mal a assimiler l'exemple ci-dessus, considerez que range() attend toujours trois arguments, que Ton pourrait intituler FROM, TO & STEP. FROM est la premiere valeur a generer, TO est la derniere (ou plutot la derniere + un), et STEP le « pas » a sauter pour passer d'une valeur a la suivante. S'ils ne sont pas fournis, les parametres FROM et STEP prennent leurs valeurs par defaut, qui sont respectivement 0 et 1 . 10.2.6 Parcours d'une liste a I'aide de for, rangeQ et len() L'instruction for est l'instruction ideale pour parcourir une liste : »> prov = [ ' La ' , ' raison ' , ' du ' , 'plus ' , ' fort ' , ' est ' , ' toujours ' , ' la ' , 'meilleure ' ] >>> for mot in prov: print mot , La raison du plus fort est toujours la meilleure II est tres pratique de combiner les fonctions range() et len() pour obtenir automatiquement tous les indices d'une sequence (liste ou chaine). Exemple : fable = [ ' Mait re ' , ' Corbeau ' , ' sur ' , 'un 1 , ' arbre ' , ' perche ' ] for index in range (len (fable) ) : print index, fable [index] L'execution de ce script donne le resultat : 0 Mait re 1 Corbeau 2 sur 3 un 4 arbre 5 perche 136. Gerard Swinnen : Apprendre a programmer avec Python 10.2.7 Une consequence du typage dynamique Comme nous l'avons deja signale plus haut (page 124), le type de la variable utilisee avec l'instruction for est redefini continuellement au fur et a mesure du parcours : meme si les elements d'une liste sont de types differents, on peut parcourir cette liste a l'aide de for sans qu'il ne s'ensuive une erreur, car le type de la variable de parcours s'adapte automatiquement a celui de l'element en cours de lecture. Exemple : >» divers = [3, 17.25, [5, 'Jean'], 'Linux is not Windoze ' ] »> for item in divers : print item, type (item) 3 17.25 [5, 'Jean'] Linux is not Windoze Dans l'exemple ci-dessus, on utilise la fonction integree type() pour montrer que la variable item change effectivement de type a chaque iteration (ceci est rendu possible grace au typage dynamique des variables Python). 10.2.8 Operations sur les listes On peut appliquer aux listes les operateurs + (concatenation) et * (multiplication) : »> fruits = [' orange ',' citron ' ] »> legumes = [ 'poireau ' , ' oignon ' , ' tomate ' ] »> fruits + legumes ['orange', 'citron', 'poireau', 'oignon', 'tomate'] »> fruits * 3 [ ' orange ' , ' citron ' , ' orange ' , ' citron ' , ' orange ' , ' citron ' ] L'operateur * est particulierement utile pour creer une liste de n elements identiques : »> sept_zeros = [0]*7 »> sept_zeros [0, 0, 0, 0, 0, 0, 0] Supposons par exemple que vous voulez creer une liste B qui contienne le meme nombre d'elements qu'une autre liste A. Vous pouvez obtenir ce resultat de differentes manieres, mais l'une des plus simples consistera a effectuer : b = [0] *ien (A) 10.2.9 Test d'appartenance Vous pouvez aisem*nt determiner si un element fait partie d'une liste a l'aide de l'instruction in : »> v = ' tomate ' »> if v in legumes : print ' OK ' OK Gerard Swinnen : Apprendre a programmer avec Python 137. 10.2.10 Copie d'une liste Considerons que vous disposez d'une liste fable que vous souhaitez recopier dans une nouvelle variable que vous appellerez phrase. La premiere idee qui vous viendra a l'esprit sera certainement d'ecrire une simple affectation telle que : >>> phrase = fable En procedant ainsi, sachez que vous ne creez pas une veritable copie. A la suite de cette instruction, il n'existe toujours qu'une seule liste dans la memoire de l'ordinateur. Ce que vous avez cree est seulement une nouvelle reference vers cette liste. Essayez par exemple : ' romps ' , ' point ' ] »> fable = [ ' Je' , 'plie' , >>> phrase = fable »> fable [4] =' casse' »> phrase ['Je', 'plie', 'mais', 'ne' mais ' ' ne ' ' casse ' , 'point ' ] Si la variable phrase contenait une veritable copie de la liste, cette copie serait independante de l'original et ne devrait done pas pouvoir etre modifiee par une instruction telle que celle de la troisieme ligne, qui s'applique a la variable fable. Vous pouvez encore experimenter d'autres modifications, soit au contenu de fable, soit au contenu de phrase. Dans tous les cas, vous constaterez que les modifications de l'une sont repercutees dans l'autre, et vice-versa. En fait, les noms fable et phrase designent tous deux un seul et meme objet en memoire. Pour decrire cette situation, les informaticiens diront que le nom phrase est un alias du nom fable. fable phrase 'Je' 'plie' 'mais' 'ne' 'romps' 'point' phrase[4] = 'casse' fable phrase 'Je' 'plie' 'mais' 'ne' 'casse' 'point' Nous verrons plus tard l'utilite des alias. Pour l'instant, nous voudrions disposer d'une technique pour effectuer une veritable copie d'une liste. Avec les notions vues precedemment, vous devriez pouvoir en trouver une par vous-meme. 138. Gerard Swinnen : Apprendre a programmer avec Python Note : Python vous autorise a « etendre » une longue instruction sur plusieurs lignes, si vous continuez a encoder quelque chose qui est delimite par une paire de parentheses, de crochets ou d'accolades. Vous pouvez traiter ainsi des expressions parenthesees, ou encore la definition de longues listes, de grands tuples ou de grands dictionnaires (voir plus loin). Le niveau d' indentation n'a pas d'importance : l'interpreteur detecte la fin de l'instruction, la ou la paire syntaxique est refermee. Cette fonctionnalite vous permet d'ameliorer la lisibilite de vos programmes. Exemple : couleurs = ['noir', 'brun', 'rouge', ' orange ' , ' jaune ' , ' vert ' , 'bleu', 'violet', 'gris', 'blanc'] Exercices : 10.36. Soient les listes suivantes : tl = [31,28,31,30,31,30,31,31,30,31,30,31] t2 = [ ' Janvier ' , ' Fevrier ' , ' Mars ' , ' Avril ' , ' Mai ' , ' Juin ' , ' Juillet ' , ' Aout ' , ' Septembre ' , ' Octobre ' , ' Novembre ' , ' Decembre ' ] Ecrivez un petit programme qui insere dans la seconde liste tous les elements de la premiere, de telle sorte que chaque nom de mois soit suivi du nombre de jours COrrespondant : [ ' Janvier ' , 31 , ' Fevrier ' , 28 , 'Mars ' , 31 , etc . . . ] . 10.37. Creez une liste A contenant quelques elements. Effectuez une vraie copie de cette liste dans une nouvelle variable B. Suggestion : creez d'abord une liste B de meme taille que A mais ne contenant que des zeros. Remplacez ensuite tous ces zeros par les elements tires de A. 10.38. Meme question, mais autre suggestion : creez d'abord une liste B vide. Remplissez-la ensuite a l'aide des elements de A ajoutes l'un apres l'autre. 10.39. Meme question, autre suggestion encore : pour creer la liste B, decoupez dans la liste A une tranche incluant tous les elements (a l'aide de l'operateur [:]). 10.40. Un nombre premier est un nombre qui n'est divisible que par un et par lui-meme. Ecrivez un programme qui etablisse la liste de tous les nombres premiers compris entre 1 et 1000, en utilisant la methode du crible d'Eratosthene : - Creez une liste de 1000 elements, chacun initialise a la valeur 1. - Parcourez cette liste a partir de l'element d'indice 2 : si l'element analyse possede la valeur 1 , mettez a zero tous les autres elements de la liste, dont les indices sont des multiples entiers de l'indice auquel vous etes arrive. Lorsque vous aurez parcouru ainsi toute la liste, les indices des elements qui seront restes a 1 seront les nombres premiers recherches. En effet : A partir de l'indice 2, vous annulez tous les elements d'indices pairs : 4, 6, 8, 10, etc. Avec l'indice 3, vous annulez les elements d'indices 6, 9, 12, 15, etc., et ainsi de suite. Seuls resteront a 1 les elements dont les indices sont effectivement des nombres premiers. Gerard Swinnen : Apprendre a programmer avec Python 139. 10.2.11 Nombres aleatoires - Histogrammes La plupart des programmes d'ordinateur font exactement la meme chose chaque fois qu'on les execute. De tels programmes sont dits deterministes. Le determinisme est certainement une bonne chose : nous voulons evidemment qu'une meme serie de calculs appliquee aux memes donnees initiales aboutisse toujours au meme resultat. Pour certaines applications, cependant, nous pouvons souhaiter que l'ordinateur soit imprevisible. Le cas des jeux constitue un exemple evident, mais il en existe bien d'autres. Contrairement aux apparences, il n'est pas facile du tout d'ecrire un algorithme qui soit reellement non-deterministe (c'est-a-dire qui produise un resultat totalement imprevisible). II existe cependant des techniques mathematiques permettant de simuler plus ou moins bien l'effet du hasard. Des livres entiers ont ete ecrits sur les moyens de produire ainsi un hasard « de bonne qualite ». Nous n'allons evidemment pas developper ici une telle question, mais rien ne vous empeche de consulter a ce sujet votre professeur de mathematiques. Dans son module random, Python propose toute une serie de fonctions permettant de generer des nombres aleatoires qui suivent differentes distributions mathematiques. Nous n'examinerons ici que quelques-unes d'entre elles. Veuillez consulter la documentation en ligne pour decouvrir les autres. Vous pouvez importer toutes les fonctions du module par : >>> from random import * La fonction ci-dessous permet de creer une liste de nombres reels aleatoires, de valeur comprise entre zero et un. L'argument a fournir est la taille de la liste : »> def list_aleat (n) : s = [0]*n for i in range (n) : s [ i ] = random ( ) return s Vous pouvez constater que nous avons pris le parti de construire d'abord une liste de zeros de taille n, et ensuite de remplacer les zeros par des nombres aleatoires. Exercices : 10.41. Reecrivez la fonction list_aleat() ci-dessus, en utilisant la methode append() pour construire la liste petit a petit a partir d'une liste vide (au lieu de remplacer les zeros d'une liste preexistante comme nous l'avons fait). 10.42. Ecrivez une fonction imprime_liste() qui permette d'afficher ligne par ligne tous les elements contenus dans une liste de taille quelconque. Le nom de la liste sera fourni en argument. Utilisez cette fonction pour imprimer la liste de nombres aleatoires generes par la fonction list_aleat(). Ainsi par exemple, l'instruction imprime_liste(liste_aleat(8)) devra afficher une colonne de 8 nombres reels aleatoires. Les nombres ainsi generes sont-ils vraiment aleatoires ? C'est difficile a dire. Si nous ne tirons qu'un petit nombre de valeurs, nous ne pouvons rien verifier. Par contre, si nous utilisons un grand nombre de fois la fonction random(), nous nous attendons a ce que la moitie des valeurs produites soient plus grandes que 0,5 (et l'autre moitie plus petites). Affinons ce raisonnement. Les valeurs tirees sont toujours dans l'intervalle 0-1. Partageons cet intervalle en 4 fractions egales : de 0 a 0,25 , de 0,25 a 0,5 , de 0,5 a 0,75 , et de 0,75 a 1 . Si nous tirons un grand nombre de valeurs au hasard, nous nous attendons a ce qu'il y en ait autant qui se situent dans chacune de nos 4 fractions. Et nous pouvons generaliser ce raisonnement a un nombre quelconque de fractions, du moment qu'elles soient egales. 140. Gerard Swinnen : Apprendre a programmer avec Python Exercice : 10.43. Vous allez ecrire un programme destine a verifier le fonctionnement du generateur de nombres aleatoires de Python en appliquant la theorie exposee ci-dessus. Votre programme devra done : • Demander a l'utilisateur le nombre de valeurs a tirer au hasard a l'aide de la fonction random(). II serait interessant que le programme propose un nombre par defaut (1000 par exemple). • Demander a l'utilisateur en combien de fractions il souhaite partager l'intervalle des valeurs possibles (e'est-a-dire l'intervalle de 0 a 1). Ici aussi, il faudrait proposer un nombre de par defaut (5 fractions, par exemple). Vous pouvez egalement limiter le choix de l'utilisateur a un nombre compris entre 2 et le 1/1 0 e du nombre de valeurs tirees au hasard. • Construire une liste de N compteurs (N etant le nombre de fractions souhaitees). Chacun d'eux sera evidemment initialise a zero. • Tirer au hasard toutes les valeurs demandees, a l'aide de la fonction random() , et memoriser ces valeurs dans une liste. • Mettre en oeuvre un parcours de la liste des valeurs tirees au hasard (boucle), et effectuer un test sur chacune d'elles pour determiner dans quelle fraction de l'intervalle 0-1 elle se situe. Incrementer de une unite le compteur correspondant. • Lorsque e'est termine, afficher l'etat de chacun des compteurs. Exemple de resultats affiches par un programme de ce type : Nombre de valeurs a tirer au hasard (defaut = 1000) : 100 Nombre de fractions dans l'intervalle 0-1 (entre 2 et 10, defaut =5) : 5 Tirage au sort des 100 valeurs . . . Comptage des valeurs dans chacune des 5 fractions . . . 11 30 25 14 20 Nombre de valeurs a tirer au hasard (defaut = 1000) : 10000 Nombre de fractions dans l'intervalle 0-1 (entre 2 et 1000, defaut =5) : 5 Tirage au sort des 10000 valeurs . . . Comptage des valeurs dans chacune des 5 fractions . . . 1970 1972 2061 1935 2062 Une bonne approche de ce genre de probleme consiste a essayer d'imaginer quelles fonctions simples vous pourriez ecrire pour resoudre l'une ou l'autre partie du probleme, puis de les utiliser dans un ensemble plus vaste. Par exemple, vous pourriez chercher a definir d'abord une fonction numeroFraction() qui servirait a determiner dans quelle fraction de l'intervalle 0-1 une valeur tiree se situe. Cette fonction attendrait deux arguments (la valeur tiree, le nombre de fractions choisi par l'utilisateur) et fournirait en retour l'index du compteur a incrementer (e'est-a-dire le n° de la fraction corres-pondante). II existe peut-etre un raisonnement mathematique simple qui permette de determiner l'index de la fraction a partir de ces deux arguments. Pensez notamment a la fonction integree int() , qui permet de convertir un nombre reel en nombre entier en eliminant sa partie decimale. Si vous ne trouvez pas, une autre reflexion interessante serait peut-etre de construire d'abord une liste contenant les valeurs « pivots » qui delimitent les fractions retenues (par exemple 0 - 0,25 - 0,5 - 0,75 - 1 dans le cas de 4 fractions). La connaissance de ces valeurs faciliterait peut-etre l'ecriture de la fonction numeroFraction() que nous souhaitons mettre au point. Si vous disposez d'un temps suffisant, vous pouvez aussi realiser une version graphique de ce programme, qui presentera les resultats sous la forme d'un histogramme (diagramme « en batons »). Gerard Swinnen : Apprendre a programmer avec Python 141. Tirage au hasard de nombres entiers Lorsque vous developperez des projets personnels, il vous arrivera frequemment de souhaiter pouvoir disposer d'une fonction qui permette de tirer au hasard un nombre entier entre certaines limites. Par exemple, si vous voulez ecrire un programme de jeu dans lequel des cartes a jouer sont tirees au hasard (a partir d'un jeu ordinaire de 52 cartes), vous aurez certainement l'utilite d'une fonction capable de tirer au hasard un nombre entier compris entre 1 et 52. Vous pouvez pour ce faire utiliser la fonction randrange() du module random. Cette fonction peut etre utilisee avec 1, 2 ou 3 arguments. Avec un seul argument, elle renvoie un entier compris entre zero et la valeur de l'argument diminue d'une unite. Par exemple, randrange(6) renvoie un nombre compris entre 0 et 5. Avec deux arguments, le nombre renvoye est compris entre la valeur du premier argument et la valeur du second argument diminue d'une unite. Par exemple, randrange(2, 8) renvoie un nombre compris entre 2 et 7. Si Ton ajoute un troisieme argument, celui-ci indique que le nombre tire au hasard doit faire partie d'une serie limitee d'entiers, separes les uns des autres par un certain intervalle, defini lui- meme par ce troisieme argument. Par exemple, randrange(3, 13, 3) renverra un des nombres de la serie 3, 6, 9, 12 : >>> for i in range (15) : print random . r andrange (3,13,3) , 3 12 6966 12 636936 12 12 Exercices : 10.44. Ecrivez un script qui tire au hasard des cartes a jouer. Le nom de la carte tiree doit etre correctement presente, « en clair ». Le programme affichera par exemple : Frappez pour tirer une carte : Dix de Trefle Frappez pour tirer une carte : As de Carreau Frappez pour tirer une carte : Huit de Pique Frappez pour tirer une carte : etc . . . 142. Gerard Swinnen : Apprendre a programmer avec Python 10.3 Les tuples Nous avons etudie jusqu'ici deux types de donnees composites : les chaines, qui sont composees de caracteres, et les listes, qui sont composees d'elements de n'importe quel type. Vous devez vous rappeler une autre difference importante entre chaines et listes : il n'est pas possible de changer les caracteres au sein d'une chaine existante, alors que vous pouvez modifier les elements d'une liste. En d'autres termes, les listes sont des sequences modifiables, alors que les chaines sont des sequences non-modifiables. Exemple : »> liste = [ ' jambon ' , ' f romage ' , ' miel ' , ' confiture ' , ' chocolat ' ] »> liste [1:3] = [ ' salade ' ] »> print liste ['jambon', 'salade', 'confiture', 'chocolat'] »> chaine = ' Romeo pref ere Juliette ' »> chaine [14:] ='Brigitte' ***** == > Erreur: object doesn't support slice assignment ***** Nous essayons de modifier la fin de la chaine, mais cela ne marche pas. La seule possibility d'arriver a nos fins est de creer une nouvelle chaine et d'y recopier ce que nous voulons changer : »> chaine = chaine [: 14] +'Brigitte' »> print chaine Romeo prefere Brigitte Python propose un type de donnees appele tuple 45 , qui est assez semblable a une liste mais qui n'est pas modifiable. Du point de vue de la syntaxe, un tuple est une collection d'elements separes par des virgules : »> tuple = 'a', 'b', 'c', 'd', 'e' »> print tuple ('a', 'b', 'c', 'd', 'e') Bien que cela ne soit pas necessaire, il est vivement conseille de mettre le tuple en evidence en l'enfermant dans une paire de parentheses, comme l'instruction print de Python le fait elle-meme. (II s'agit simplement d'ameliorer la lisibilite du code, mais vous savez que c'est important) : »> tuple = ('a', 'b', *c', 'd', 'e') Les operations que Ton peut effectuer sur des tuples sont syntaxiquement similaires a celles que Ton effectue sur les listes, si ce n'est que les tuples ne sont pas modifiables : »> print tuple [2: 4] ('c', 'd') »> tuple [1:3] = ('x', 'y') ==> ***** erreur ***** »> tuple = ('Andre',) + tuple [1:] »> print tuple ('Andre', 'b', 'c', 'd', 'e') Remarquez qu'il faut toujours au moins une virgule pour definir un tuple (le dernier exemple ci- dessus utilise un tuple contenant un seul element : 'Andre'). Vous comprendrez l'utilite des tuples petit a petit. Signalons simplement ici qu'ils sont preferables aux listes partout ou Ton veut etre certain que les donnees transmises ne soient pas modifiees par erreur au sein d'un programme. En outre, les tuples sont moins « gourmands » en ressources systeme (ils occupent moins de place en memo ire). 45 ce terme n'est pas un mot anglais : il s'agit d'un neologisme informatique Gerard Swinnen : Apprendre a programmer avec Python 143. 10.4 Les dictionnaires Les types composites que nous avons abordes jusqu'a present (chaines, listes et tuples) etaient tous des sequences, c'est-a-dire des suites ordonnees d'elements. Dans une sequence, il est facile d'acceder a un element quelconque a l'aide d'un index (un nombre entier), mais a la condition expresse de connaitre son emplacement. Les dictionnaires que nous decouvrons ici constituent un autre type composite. lis ressemblent aux listes dans une certaine mesure (ils sont modifiables comme elles), mais ce ne sont pas des sequences. Les elements que nous allons y enregistrer ne seront pas disposes dans un ordre immuable. En revanche, nous pourrons acceder a n'importe lequel d'entre eux a l'aide d'un index specifique que Ton appellera une cle, laquelle pourra etre alphabetique, numerique, ou meme d'un type composite sous certaines conditions. Comme dans une liste, les elements memorises dans un dictionnaire peuvent etre de n'importe quel type. Ce peuvent etre des valeurs numeriques, des chaines, des listes, des tuples, des dictionnaires, mais aussi des fonctions, des classes ou des instances (voir plus loin). 10.4.1 Creation d'un dictionnaire A titre d'exemple, nous allons creer un dictionnaire de langue, pour la traduction de termes informatiques anglais en francais. Dans ce dictionnaire, les index seront des chaines de caracteres. Puisque le type dictionnaire est un type modifiable, nous pouvons commencer par creer un dictionnaire vide, puis le remplir petit a petit. Du point de vue syntaxique, on reconnait une structure de donnees de type dictionnaire au fait que ses elements sont enfermes dans une paire d' accolades. Un dictionnaire vide sera done note { } : »> dico = { } »> dico [ ' computer ' ] = ' ordinateur ' »> dico [ 'mouse ' ] ='souris' »> dico [' keyboard ' ] =' clavier' »> print dico {'computer': 'ordinateur', 'keyboard': 'clavier', 'mouse': ' souris ' } Comme vous pouvez l'observer dans la ligne ci-dessus, un dictionnaire apparait comme une serie d'elements separes par des virgules (le tout etant enferme entre deux accolades}. Chacun de ces elements est constitute d'une paire d'objets : un index et une valeur, separes par un double point. Dans un dictionnaire, les index s'appellent des cle's, et les elements peuvent done s'appeler des paires cle-valeur. Vous pouvez constater que l'ordre dans lequel les elements apparaissent a la derniere ligne ne correspond pas a celui dans lequel nous les avons fournis. Cela n'a strictement aucune importance : nous n'essaierons jamais d'extraire une valeur d'un dictionnaire a l'aide d'un index numerique. Au lieu de cela, nous utiliserons les cles : >>> print dico [ 'mouse ' ] souris Contrairement a ce qui se passe avec les listes, il n'est pas necessaire de faire appel a une methode particuliere (telle que append()) pour aj outer de nouveaux elements a un dictionnaire : il suffit de creer une nouvelle paire cle-valeur. 144. Gerard Swinnen : Apprendre a programmer avec Python 10.4.2 Operations sur les dictionnaires Vous savez deja comment ajouter des elements a un dictionnaire. Pour en enlever, vous utiliserez l'instruction del. Creons pour l'exemple un autre dictionnaire, destine cette fois a contenir l'inventaire d'un stock de fruits. Les index (ou cles) seront les noms des fruits, et les valeurs seront les masses de ces fruits repertoriees dans le stock (il s'agit done cette fois de valeurs de type numerique). »> invent = {'pommes': 430, 'bananes': 312, 'oranges' : 274, 'poires' : 137} »> print invent {'oranges': 274, 'pommes': 430, 'bananes': 312, 'poires': 137} Si le patron decide de liquider toutes les pommes et de ne plus en vendre, nous pouvons enlever cette entree dans le dictionnaire : »> del invent [ 'pommes ' ] »> print invent {'oranges': 274, 'bananes': 312, 'poires': 137} La fonction len() est utilisable avec un dictionnaire : elle en renvoie le nombre d'elements. 10.4.3 Les dictionnaires sont des objets On peut appliquer aux dictionnaires un certain nombre de methodes specifiques : La methode keys() renvoie la liste des cles utilisees dans le dictionnaire : »> print dico.keys() [ ' computer ' , ' keyboard ' , ' mouse ' ] La methode values() renvoie la liste des valeurs memorisees dans le dictionnaire : »> print invent . values ( ) [274, 312, 137] La methode has_key() permet de savoir si un dictionnaire comprend une cle determinee. On fournit la cle en argument, et la methode renvoie une valeur 'vraie' ou 'fausse' (en fait, 1 ou 0), suivant que la cle est presente ou pas : »> print invent . has_key ( ' bananes ' ) 1 »> if invent . has_key ( ' pommes ' ) : print ' nous avons des pommes ' else : print 'pas de pommes, sorry' pas de pommes, sorry La methode items() extrait du dictionnaire une liste equivalente de tuples : »> print invent . items ( ) [('oranges', 274), ('bananes', 312), ('poires', 137)] Gerard Swinnen : Apprendre a programmer avec Python 145. La methode copy() permet d'effectuer une vraie copie d'un dictionnaire. II faut savoir en effet que la simple affectation d'un dictionnaire existant a une nouvelle variable cree seulement une nouvelle reference vers le meme objet, et non un nouvel objet. Nous avons deja discute ce phenomene (aliasing) a propos des listes (voir page 138). Par exemple, l'instruction ci-dessous ne definit pas un nouveau dictionnaire (contrairement aux apparences) : >>> stock = invent »> print stock {'oranges': 274, 'bananes': 312, 'poires': 137} Si nous modifions invent, alors stock aussi est modifie, et vice-versa (ces deux noms designent en effet le meme objet dictionnaire dans la memo ire de l'ordinateur) : >>> del invent [ 'bananes ' ] »> print stock {'oranges': 274, 'poires': 137} Pour obtenir une vraie copie (independante) d'un dictionnaire preexistant, il faut employer la methode copy() : »> magasin = stock, copy () »> magasin [ 'prunes ' ] = 561 >>> print magasin {'oranges': 274, 'prunes': 561, 'poires': 137} »> print stock {'oranges': 274, 'poires': 137} >>> print invent {'oranges': 274, 'poires': 137} 10.4.4 Parcours d'un dictionnaire Vous pouvez utiliser une boucle for pour traiter successivement tous les elements contenus dans un dictionnaire, mais attention : • Au cours de l'iteration, ce sont les cles utilisees dans le dictionnaire qui seront successivement affectees a la variable de travail, et non les valeurs. • L'ordre dans lequel les elements seront extraits est imprevisible (puisqu'un dictionnaire n'est pas une sequence). Exemple : »> invent ={ "oranges" : 274, "poires" : 137 , "bananes" : 312 } >>> for clef in invent : . . . print clef poires bananes oranges Si vous souhaitez effectuer un traitement sur les valeurs, il vous suffit alors de recuperer chacune d'elles a partir de la cle correspondante : for clef in invent : print clef, invent [clef] poires 137 bananes 312 oranges 274 146. Gerard Swinnen : Apprendre a programmer avec Python Cette maniere de proceder n'est cependant pas ideale, ni en termes de performances ni meme du point de vue de la lisibilite. II est recommande de plutot faire appel a la methode items() decrite a la section precedente : for clef, valeur in invent . items ( ) : print clef, valeur poires 137 bananes 312 oranges 274 10.4.5 Les cles ne sont pas necessairement des chafnes de caracteres Jusqu'a present nous avons decrit des dictionnaires dont les cles etaient a chaque fois des valeurs de type string. En fait nous pouvons utiliser en guise de cles n'importe quel type de donnee non modifiable : des entiers, des reels, des chaines de caracteres, et meme des tuples. Considerons par exemple que nous voulions repertorier les arbres remarquables situes dans un grand terrain rectangulaire. Nous pouvons pour cela utiliser un dictionnaire, dont les cles seront des tuples indiquant les coordonnees x,y de chaque arbre : >» arb = { } »> arb[(l,2)] = 'Peuplier' »> arb[(3,4)] = 'Platane' >» arb [6, 5] = 'Palmier' »> arb [5,1] = 'Cycas' »> arb[7,3] = ' Sapin ' »> print arb {(3, 4): 'Platane', (6, 5): 'Palmier', (5, 'Cycas', (1, 2): 'Peuplier', (7, 3): 'Sapin'} »> print arb[(6,5)] palmier Vous pouvez remarquer que nous avons allege l'ecriture a partir de la troisieme ligne, en profitant du fait que les parentheses delimitant les tuples sont facultatives (a utiliser avec prudence !). Dans ce genre de construction, il faut garder a l'esprit que le dictionnaire contient des elements seulement pour certains couples de coordonnees. Ailleurs, il n'y a rien. Par consequent, si nous voulons interroger le dictionnaire pour savoir ce qui se trouve la ou il n'y a rien, comme par exemple aux coordonnees (2,1), nous allons provoquer une erreur : »> print arb [1,2] Peuplier »> print arb [2,1] ***** Erreur : KeyError: (2, 1) ***** Pour resoudre ce petit probleme, nous pouvons utiliser la methode get() : »> print arb. get ( (1, 2) , 'neant ' ) Peuplier »> print arb. get ( (2, 1) , 'neant ' ) neant Le premier argument transmis a cette methode est la cle de recherche, le second argument est la valeur que nous voulons obtenir en retour si la cle n'existe pas dans le dictionnaire. Gerard Swinnen : Apprendre a programmer avec Python 147. 10.4.6 Les dictionnaires ne sont pas des sequences Comme vous l'avez vu plus haut, les elements d'un dictionnaire ne sont pas disposes dans un ordre particulier. Des operations comme la concatenation et l'extraction (d'un groupe d'elements contigus) ne peuvent done tout simplement pas s'appliquer ici. Si vous essayez tout de meme, Python levera une erreur lors de l'execution du code : >» print arb[l:3] ***** Erreur : KeyError: slice (1, 3, None) ***** Vous avez vu egalement qu'il suffit d'affecter un nouvel indice (une nouvelle cle) pour ajouter une entree au dictionnaire. Cela ne marcherait pas avec les listes 46 : »> invent [' cerises ' ] = 987 >>> print invent {'oranges': 274, 'cerises': 987, 'poires': 137} »> liste =['jambon', 'salade', 'confiture', 'chocolat'] »> liste [ 4 ] = ' salami ' ***** IndexError: list assignment index out of range ***** Du fait qu'ils ne sont pas des sequences, les dictionnaires se revelent done particulierement precieux pour gerer des ensembles de donnees ou Ton est amene a effectuer frequemment des ajouts ou des suppressions, dans n'importe quel ordre. lis remplacent avantageusem*nt les listes lorsqu'il s'agit de traiter des ensembles de donnees numerotees, dont les numeros ne se suivent pas. Exemple : »> client = { } »> client [4317] = "Dupond" »> client [256] = "Durand" »> client [782] = "Schmidt" etc. Exercices : 10.45. Ecrivez un script qui cree un mini-systeme de base de donnees fonctionnant a l'aide d'un dictionnaire, et dans lequel vous memoriserez les noms d'une serie de copains, leur age et leur taille. Votre script devra comporter deux fonctions : la premiere pour le remplissage du dictionnaire, et la seconde pour sa consultation. Dans la fonction de remplissage, utilisez une boucle pour accepter les donnees entrees par l'utilisateur. Dans le dictionnaire, le nom de l'eleve servira de cle d'acces, et les valeurs seront constitutes de tuples (age, taille), dans lesquels l'age sera exprime en annees (donnee de type entier), et la taille en metres (donnee de type reel). La fonction de consultation comportera elle aussi une boucle, dans laquelle l'utilisateur pourra fournir un nom quelconque pour obtenir en retour le couple « age, taille » correspondant. Le resultat de la requete devra etre une ligne de texte bien formatee, telle par exemple : «Nom : Jean Dhoute - age : 15 ans - taille : 1.74 m ». Pour obtenir ce resultat, servez-vous du formatage des chaines de caracteres decrit a la page 130. 10.46. Ecrivez une fonction qui echange les cles et les valeurs d'un dictionnaire (ce qui permettra par exemple de transformer un dictionnaire anglais/francais en un dictionnaire francais/anglais). (On suppose que le dictionnaire ne contient pas plusieurs valeurs identiques). 46 Rappel : les methodes permettant d'ajouter des elements a une liste sont decrites page 135. 148. Gerard Swinnen : Apprendre a programmer avec Python 10.4.7 Construction d'un histogramme a I'aide d'un dictionnaire Les dictionnaires constituent un outil tres elegant pour construire des histogrammes. Supposons par exemple que nous voulions etablir rhistogramme qui represente la frequence d'utilisation de chacune des lettres de l'alphabet dans un texte donne. L'algorithme permettant de realiser ce travail est extraordinairement simple si on le construit sur base d'un dictionnaire : »> texte ="les saucisses et saucissons sees sont dans le saloir" >» lettres ={} »> for c in texte: lettres [c] = lettres . get (c, 0) + 1 »> print lettres {'t': 2, 'u' : 2, 'r': 1, 's': 14, 'n': 3, 'o': 3, '1': 3, 'i': 3, 'd': 1, 'e': 5, 'c' : 3, ' ' : 8, 'a' : 4} Nous commencons par creer un dictionnaire vide : lettres. Ensuite, nous allons remplir ce dictionnaire en utilisant les caracteres de l'alphabet en guise de cles. Les valeurs que nous memoriserons pour chacune de ces cles seront les frequences des caracteres correspondants dans le texte. Afin de calculer celles-ci, nous effectuons un parcours de la chaine de caracteres texte. Pour chacun de ces caracteres, nous interrogeons le dictionnaire a I'aide de la methode get(), en utilisant le caractere en guise de cle, afin d'y lire la frequence deja memorisee pour ce caractere. Si cette valeur n'existe pas encore, la methode get() doit renvoyer une valeur nulle. Dans tous les cas, nous incrementons la valeur trouvee, et nous la memorisons dans le dictionnaire a l'emplacement qui correspond a la cle (e'est-a-dire au caractere en cours de traitement). Pour fignoler notre travail, nous pouvons encore souhaiter afficher l'histogramme dans l'ordre alphabetique. Pour ce faire, nous pensons immediatement a la methode sort(), mais celle-ci ne peut s'appliquer qu'aux listes. Qu'a cela ne tienne ! Nous avons vu plus haut comment nous pouvions convertir un dictionnaire en une liste de tuples : »> lettres_triees = lettres . items () »> lettres_triees . sort () »> print lettres_triees [(' ', 8), ('a', 4), ('c\ 3), ('d', 1), Ce', 5), ('i', 3), ('1', 3), ('n', 3), ('o', 3), ('r', 1), ('s\ 14), ('t', 2), ('u', 2)] Exercices : 10.47. Vous avez a votre disposition un fichier texte quelconque (pas trop gros). Ecrivez un script qui compte les occurrences de chacune des lettres de l'alphabet dans ce texte (on ne tiendra pas compte du probleme des lettres accentuees).. 10.48. Modifiez le script ci-dessus afin qu'il etablisse une table des occurrences de chaque mot dans le texte. Conseil : dans un texte quelconque, les mots ne sont pas seulement separes par des espaces, mais egalement par divers signes de ponctuation. Pour simplifier le probleme, vous pouvez commencer par remplacer tous les caracteres non-alphabetiques par des espaces, et convertir la chaine resultante en une liste de mots a I'aide de la methode split(). 10.49. Vous avez a votre disposition un fichier texte quelconque (pas trop gros). Ecrivez un script qui analyse ce texte, et memorise dans un dictionnaire l'emplacement exact de chacun des mots (compte en nombre de caracteres a partir du debut). Lorsqu'un meme mot apparait plusieurs fois, tous ses emplacements doivent etre memorises : chaque valeur de votre dictionnaire doit done etre une liste d' emplacements. Gerard Swinnen : Apprendre a programmer avec Python 149. 10.4.8 Controle du flux d'executlon a I'aide d'un dictionnaire II arrive frequemment que Ton ait a diriger l'execution d'un programme dans differentes directions, en fonction de la valeur prise par une variable. Vous pouvez bien evidemment traiter ce probleme a I'aide d'une serie destructions if - elif - else , mais cela peut devenir assez lourd et inelegant si vous avez affaire a un grand nombre de possibilites. Exemple : materiau = raw_input ( "Choisissez le materiau : ") if materiau == 'fer': fonctionA() elif materiau == 'bois': fonctionC () elif materiau == ' cuivre ' : f onctionB ( ) elif materiau == 'pierre': fonctionD () elif . . . etc . . . Les langages de programmation proposent souvent des instructions specifiques pour traiter ce genre de probleme, telles les instructions switch ou case du C ou du Pascal. Python n'en propose aucune, mais vous pouvez vous tirer d'affaire dans bien des cas a I'aide d'une liste (nous en donnons un exemple detaille page 225), ou mieux encore a I'aide d'un dictionnaire. Exemple : materiau = raw_input ( "Choisissez le materiau : ") dico = { ' f er ' : f onctionA, 'bois : fonctionC, ' cuivre ' : f onctionB, 'pierre' : fonctionD, . . . etc . . . } dico [materiau] () Les deux instructions ci-dessus pourraient etre condensees en une seule, mais nous les laissons separees pour bien detailler le mecanisme : • La premiere instruction definit un dictionnaire dico dans lequel les cles sont les differentes possibilites pour la variable materiau, et les valeurs, les noms des fonctions a invoquer en correspondance. Notez bien qu'il s'agit seulement des noms de ces fonctions, qu'il ne faut surtout pas faire suivre de parentheses dans ce cas (Sinon Python executerait chacune de ces fonctions au moment de la creation du dictionnaire). • La seconde instruction invoque la fonction correspondant au choix opere a I'aide de la variable materiau. Le nom de la fonction est extrait du dictionnaire a I'aide de la cle, puis associe a une paire de parentheses. Python reconnait alors un appel de fonction tout a fait classique et l'execute. Vous pouvez encore ameliorer la technique ci-dessus en remplacant cette instruction par sa variante ci-dessous, qui fait appel a la methode get() afin de prevoir le cas ou la cle demandee n'existerait pas dans le dictionnaire (vous obtenez de cette facon l'equivalent d'une instruction else terminant une longue serie de elif) : dico . get (materiau, f onct Autre) () (Lorsque la la valeur de la variable materiau ne correspond a aucune cle du dictionnaire, c'est la fonction fonctAutre() qui est invoquee). 150. Gerard Swinnen : Apprendre a programmer avec Python Exercices : 10.50. Completez l'exercice 10.45 (mini-systeme de base de donnees) en lui ajoutant deux fonctions : l'une pour enregistrer le dictionnaire resultant dans un fichier texte, et l'autre pour reconstituer ce dictionnaire a partir du fichier correspondant. Chaque ligne de votre fichier texte correspondra a un element du dictionnaire. Elle sera formatee de maniere a bien separer : - la cle et la valeur (c'est-a-dire le nom de la personne, dune part, et l'ensemble : « age + taille », d'autre part. - dans l'ensemble « age + taille », ces deux donnees numeriques. Vous utiliserez done deux caracteres separateurs differents, par exemple « @ » pour separer la cle et la valeur, et « # » pour separer les donnees constituant cette valeur : Juliette@18#1.67 Jean-Pierre@17#l .78 Delphine@19#1.71 Anne-Marie@17#l . 63 etc . 10.51. Ameliorez encore le script de l'exercice precedent, en utilisant un dictionnaire pour diriger le flux d' execution du programme au niveau du menu principal. Votre programme affichera par exemple : Choisissez : (R) ecuperer un dictionnaire preexistant sauvegarde dans un fichier (A) jouter des donnees au dictionnaire courant (C) onsulter le dictionnaire courant (S) auvegarder le dictionnaire courant dans un fichier (T) erminer : Suivant le choix opere par l'utilisateur, vous effectuerez alors l'appel de la fonction correspondante en la selectionnant dans un dictionnaire de fonctions. Gerard Swinnen : Apprendre a programmer avec Python 151. Chapitre 11 : Classes, objets, attributs Les chapitres precedents vous ont deja mis en contact a plusieurs reprises avec la notion d'objet. Vous savez done deja qu'un objet est une entite que Ton construit par instanciation a partir d'une classe (e'est-a-dire en quelque sorte une « categorie » ou un « type » d'objet). Par exemple, on peut trouver dans la bibliotheque Tkinter, une classe Button() a partir de laquelle on peut creer dans une fenetre un nombre quelconque de boutons. Nous allons a present examiner comment vous pouvez vous-memes definir de nouvelles classes d'objets. II s'agit la d'un sujet relativement ardu, mais vous l'aborderez de maniere tres progressive, en commencant par definir des classes d'objets tres simples, que vous perfectionnerez ensuite. Attendez-vous cependant a rencontrer des objets de plus en plus complexes par apres. Comme les objets de la vie courante, les objets informatiques peuvent etre tres simples ou tres compliques. lis peuvent etre composes de differentes parties, qui soient elles-memes des objets, ceux-ci etant faits a leur tour d'autres objets plus simples, etc. 11.1 Utilite des classes Les classes sont les principaux outils de la programmation orientee objet (Object Oriented Programming ou OOP). Ce type de programmation permet de structurer les logiciels complexes en les organisant comme des ensembles d'objets qui interagissent, entre eux et avec le monde exterieur. Le premier benefice de cette approche de la programmation consiste dans le fait que les differents objets utilises peuvent etre construits independamment les uns des autres (par exemple par des programmeurs differents) sans qu'il n'y ait de risque d' interference. Ce resultat est obtenu grace au concept class Point : "Definition d'un point mathematique" Les definitions de classes peuvent etre situees n'importe ou dans un programme, mais on les placera en general au debut (ou bien dans un module a importer). L'exemple ci-dessus est probablement le plus simple qui se puisse concevoir. Une seule ligne nous a suffi pour definir le nouveau type d'objet Point(). Remarquons d'emblee que : ♦ L'instruction class est un nouvel exemple ^instruction composee. N'oubliez pas le double point obligatoire a la fin de la ligne, et l'indentation du bloc d'instructions qui suit. Ce bloc doit contenir au mo ins une ligne. Dans notre exemple ultra-simplifie, cette ligne n'est rien d' autre qu'un simple commentaire. (Par convention, si la premiere ligne suivant l'instruction class est une chaine de caracteres, celle-ci sera consideree comme un commentaire et incorporee automatiquement dans un dispositif de documentation des classes qui fait partie integrante de Python. Prenez done l'habitude de toujours placer une chaine decrivant la classe a cet endroit). ♦ Rappelez-vous aussi la convention qui consiste a toujours donner aux classes des noms qui commencent par une majuscule. Dans la suite de ce texte, nous respecterons encore une autre convention qui consiste a associer a chaque nom de classe une paire de parentheses, comme nous le faisons deja pour les noms de fonctions. Nous venons de definir une classe Point(). Nous pouvons des a present nous en servir pour creer des objets de ce type, par instantiation. Creons par exemple un nouvel objet p9 47 : »> p9 = Point () Apres cette instruction, la variable p9 contient la reference d'un nouvel objet Point(). Nous pouvons dire egalement que p9 est une nouvelle instance de la classe Point(). Attention : comme les fonctions, les classes auxquelles on fait appel dans une instruction doivent toujours etre accompagnees de parentheses (meme si aucun argument n'est transmis). Nous verrons un peu plus loin que les classes peuvent etre appelees avec des arguments. Remarquez bien cependant que la definition d'une classe ne necessite pas de parentheses (contrairement a ce qui de regie lors de la definition des fonctions), sauf si nous souhaitons que la classe en cours de definition derive d'une autre classe preexistante (ceci sera explique plus loin). Nous pouvons des a present effectuer quelques manipulations elementaires avec notre nouvel 47 Sous Python, on peut done instancier un objet a l'aide d'une simple instruction d'affectation. D'autres langages imposent l'emploi d'une instruction speciale, souvent appelee new pour bien montrer que Ton cree un nouvel objet a partir d'un moule. Exemple : p9 = new PointQ Gerard Swinnen : Apprendre a programmer avec Python 153. objet p9. Exemple : »> print p9. doc Definition d'un point mathematique (Comme nous vous l'avons explique pour les fonctions (voir page 73), les chaines de documentation de divers objets Python sont associees a l'attribut predefini doc ) >» print p9 < main .Point instance at 0x403ela8c> Le message renvoye par Python indique, comme vous l'aurez certainement bien compris tout de suite, que p9 est une instance de la classe Point(), qui est defrnie elle-meme au niveau principal du programme. Elle est situee dans un emplacement bien determine de la memoire vive, dont l'adresse apparait ici en notation hexadecimale (Veuillez consulter votre cours d'informatique generale si vous souhaitez des explications complementaires a ce sujet). 11.3 Attributs (ou variables) d'instance P 9 3.0 L'objet que nous venons de creer est une coquille vide. Nous pouvons ajouter des composants a cet objet par simple assignation, en utilisant le systeme de qualification des noms par points 48 : »> p9 .x = 3.0 »> p9 .y = 4.0 Les variables ainsi definies sont des attributs de l'objet p9, ou encore des variables d'instance. Elles sont incorporees, ou plutot encapsulees dans l'objet. Le diagramme d'etat ci-contre montre le resultat de ces affectations : la variable p9 contient la reference indiquant l'emplacement memoire du nouvel objet, qui contient lui- meme les deux attributs x et y. On peut utiliser les attributs d'un objet dans n'importe quelle expression, comme toutes les variables ordinaires : >>> print p9.x 3.0 »> print p9.x**2 + p9.y**2 25.0 y-> 4.0 Du fait de leur encapsulation dans l'objet, les attributs sont des variables distinctes d'autres variables qui pourraient porter le meme nom. Par exemple, l'instruction x = p9.x signifie : « extraire de l'objet reference par p9 la valeur de son attribut x, et assigner cette valeur a la variable x ». II n'y a pas de conflit entre la variable x et l'attribut x de l'objet p9. L'objet p9 contient en effet son propre espace de noms, independant de l'espace de nom principal ou se trouve la variable x. 48 Ce systeme de notation est similaire a celui que nous utilisons pour designer les variables d'un module, comme par exemple math.pi ou string.uppercase. Nous aurons l'occasion d'y revenir plus tard, mais sachez des a present que les modules peuvent en effet contenir des fonctions, mais aussi des classes et des variables. Essayez par exemple : »> import string »> print string.uppercase »> print string. lowercase »> print string. hexdigits 154. Gerard Swinnen : Apprendre a programmer avec Python Remarque importante : Nous venons de voir qu'il est tres aise d'aj outer un attribut a un objet en utilisant une simple instruction d'assignation telle que p9.x = 3.0 On peut se permettre cela sous Python (c'est une consequence de l'assignation dynamique des variables), mais cela n'est pas vraiment recommandable, comme vous le comprendrez plus loin. Nous n'utiliserons done cette facon de faire que de maniere occasionnelle, et uniquement dans le but de simplifier nos explications concernant les attributs d' instances. La bonne maniere de proceder sera developpee dans le chapitre suivant. 1 1.4 Passage d'objets comme arguments tors de I'appel d'une fonction Les fonctions peuvent utiliser des objets comme parametres (elles peuvent egalement fournir un objet comme valeur de re tour). Par exemple, vous pouvez definir une fonction telle que celle-ci : »> def af f iche_point (p) : print "coord, horizontale =", p.x, "coord, verticale =", p.y Le parametre p utilise par cette fonction doit etre un objet de type Point(), puisque l'instruction qui suit utilise les variables d'instance p.x et p.y. Lorsqu'on appelle cette fonction, il faut done lui fournir un objet de type Point() comme argument. Essayons avec l'objet p9 : »> af fiche_point (p9) coord, horizontale = 3.0 coord, verticale = 4.0 Exercice : (11) Ecrivez une fonction distance() qui permette de calculer la distance entre deux points. Cette fonction attendra evidemment deux objets Point() comme arguments. 11.5 Similitude et unicite Dans la langue parlee, les memes mots peuvent avoir des significations fort differentes suivant le contexte dans lequel on les utilise. La consequence en est que certaines expressions utilisant ces mots peuvent etre comprises de plusieurs manieres differentes (expressions ambigues). Le mot « meme », par exemple, a des significations differentes dans les phrases : « Charles et moi avons la meme voiture » et « Charles et moi avons la meme mere ». Dans la premiere, ce que je veux dire est que la voiture de Charles et la mienne sont du meme modele. II s'agit pourtant de deux voitures distinctes. Dans la seconde, j'indique que la mere de Charles et la mienne constituent en fait une seule et unique personne. Lorsque nous traitons d'objets logiciels, nous pouvons rencontrer la meme ambigui'te. Par exemple, si nous parlons de l'egalite de deux objets Point(), cela signifie-t-il que ces deux objets contiennent les memes donnees (leurs attributs), ou bien cela signifie-t-il que nous parlons de deux references a un meme et unique objet ? Considerez par exemple les instructions suivantes : »> pi = Point () »> pl.x = 3 »> pl.y = 4 »> p2 = Point () »> p2.x = 3 »> p2.y = 4 »> print (pi == p2) 0 Gerard Swinnen : Apprendre a programmer avec Python 155. Ces instructions creent deux objets pi et p2 qui restent distincts, meme s'ils ont des contenus similaires. La derniere instruction teste l'egalite de ces deux objets (double signe egale), et le resultat est zero (ce qui signifie que l'expression entre parentheses est fausse : il n'y a done pas egalite). On peut confirmer cela d'une autre maniere encore : >>> print pi < main .Point instance at 00C2CBEO »> print p2 < main .Point instance at 00C50F9O L'information est claire : les deux variables pi et p2 referencent bien des objets differents. Essayons autre chose, a present : »> p2 = pi »> print (pi == p2) 1 Par l'instruction p2 = pi, nous assignons le contenu de pi a p2. Cela signifie que desormais ces deux variables referencent le meme objet. Les variables pi et p2 sont des alias 49 l'une de l'autre. Le test d'egalite dans l'instruction suivante renvoie cette fois la valeur 1, ce qui signifie que l'expression entre parentheses est vraie : pi et p2 designent bien toutes deux un seul et unique objet, comme on peut s'en convaincre en essayant encore : »> pl.x = 7 >>> print p2.x 7 >>> print pi < main .Point instance at 00C2CBEO >» print p2 < main .Point instance at 00C2CBEO 11.6 Objets composes d'objets Supposons maintenant que nous voulions definir une classe pour representer des rectangles. Pour simplifier, nous allons considerer que ces rectangles seront toujours orientes horizontalement ou verticalement, et jamais en oblique. De quelles informations avons-nous besoin pour definir de tels rectangles ? II existe plusieurs possibilites. Nous pourrions par exemple specifier la position du centre du rectangle (deux coordonnees) et preciser sa taille (largeur et hauteur). Nous pourrions aussi specifier les positions du coin superieur gauche et du coin inferieur droit. Ou encore la position du coin superieur gauche et la taille. Admettons ce soit cette derniere methode qui soit retenue. Definissons done notre nouvelle classe : »> class Rectangle: "definition d'une classe de rectangles" ... et servons nous-en tout de suite pour creer une instance : »> boite = Rectangle () »> boite . largeur = 50.0 >» boite . hauteur = 35.0 49 Concernant ce phenomene d'aliasing, voir egalement page 138 : copie d'une liste 156. Gerard Swinnen : Apprendre a programmer avec Python Nous creons ainsi un nouvel objet Rectangle() et deux attributs. Pour specifier le coin superieur gauche, nous allons utiliser une instance de la classe Point() que nous avons definie precedemment. Ainsi nous allons creer un objet a l'interieur d'un autre objet ! »> boite. coin = Point () »> boite.coin.x = 12.0 »> boite.coin.y = 27.0 Pour acceder a un objet qui se trouve a l'interieur d'un autre objet, on utilise la qualification des noms hierarchisee (a l'aide de points) que nous avons deja rencontree a plusieurs reprises. Ainsi l'expression boite.coin.y signifie « Aller a l'objet reference dans la variable boite. Dans cet objet, reperer l'attribut coin, puis aller a l'objet reference dans cet attribut. Une fois cet autre objet trouve, selectionner son attribut y. » Vous pourrez peut-etre mieux vous representer a l'avenir les objets composites, a l'aide de diagrammes similaires a celui que nous reproduisons ci-dessous : Espaces de noms boite largeur hauteur coin x y Valeurs > 50.0 > 35.0 -> |12.0 * 1 27.0 Le nom « boite » se trouve dans l'espace de noms principal. II reference un autre espace de noms reserve a l'objet correspondant, dans lequel sont memorises les noms « largeur », « hauteur » et « coin ». Ceux-ci referencent a leur tour, soit d'autres espaces de noms (cas du nom « coin »), soit des valeurs bien determinees. Python reserve des espaces de noms differents pour chaque module, chaque classe, chaque instance, chaque fonction. Vous pouvez tirer parti de tous ces espaces bien compartimentes afin de realiser des programmes robustes, c'est-a-dire des programmes dont les differents composants ne peuvent pas facilement interferes 11.7 Objets comme valeurs de retour d'une fonction Nous avons vu plus haut que les fonctions peuvent utiliser des objets comme parametres. Elles peuvent egalement transmettre une instance comme valeur de retour. Par exemple, la fonction trouveCentre() ci-dessous doit etre appelee avec un argument de type Rectangle() et elle renvoie un objet Point(), lequel contiendra les coordonnees du centre du rectangle. »> def trouveCentre (box) : p = Point () p.x = box. coin. x + box . largeur/2 . 0 p.y = box. coin. y + box . hauteur/2 . 0 return p Pour appeler cette fonction, vous pouvez utiliser l'objet boite comme argument : »> centre = trouveCentre (boite) »> print centre. x, centre. y 37.0 44.5 Gerard Swinnen : Apprendre a programmer avec Python 157. 11.8 Les objets sont modifiables Nous pouvons changer les proprietes d'un objet en assignant de nouvelles valeurs a ses attributs. Par exemple, nous pouvons modifier la taille d'un rectangle (sans modifier sa position), en reassignant ses attributs hauteur et largeur : »> boite . hauteur = boite .hauteur + 20 »> boite . largeur = boite . largeur - 5 Nous pouvons faire cela sous Python, parce que dans ce langage les proprietes des objets sont toujours publiques (du moins dans la version actuelle 2.0). D'autres langages etablissent une distinction nette entre attributs publics (accessibles de l'exterieur de l'objet) et attributs prives (qui sont accessibles seulement aux algorithmes inclus dans l'objet lui-meme). Comme nous l'avons deja signale plus haut (a propos de la definition des attributs par assignation simple, depuis l'exterieur de l'objet), modifier de cette fa*gon les attributs d'une instance n'est pas une pratique recommandable, parce qu'elle contredit l'un des objectifs fondamentaux de la programmation orientee objet, qui vise a etablir une separation stricte entre la fonctionnalite d'un objet (telle qu'elle a ete declaree au monde exterieur) et la maniere dont cette fonctionnalite est reellement implementee dans l'objet (et que le monde exterieur n'a pas a connaitre). Plus concretement, nous devrons veiller desormais a ce que les objets que nous creons ne soient modifiables en principe que par l'intermediaire de methodes mises en place specifiquement dans ce but, comme nous allons l'expliquer dans le chapitre suivant. 158. Gerard Swinnen : Apprendre a programmer avec Python Chapitre 12 : Classes, methodes, heritage Les classes que nous avons definies dans le chapitre precedent ne sont finalement rien d'autre que des espaces de noms particuliers, dans lesquels nous n'avons place jusqu'ici que des variables (les attributs d'instance). II nous faut a present doter ces classes d'une fonctionnalite. L'idee de base de la programmation orientee objet consiste en effet a regrouper dans un meme ensemble (l'objet) a la fois un certain nombre de donnees (ce sont les attributs d'instance) et les algorithmes destines a effectuer divers traitements sur ces donnees (ce sont les methodes, c'est-a-dire des fonctions encapsulees). Objet = [ attributs + methodes ] Cette facon d'associer dans une meme « capsule » les proprietes d'un objet et les fonctions qui permettent d'agir sur elles, correspond chez les concepteurs de programmes a une volonte de construire des entites informatiques dont le comportement se rapproche du comportement des objets du monde reel qui nous entoure. Considerons par exemple un widget « bouton ». II nous parait raisonnable de souhaiter que l'objet informatique que nous appelons ainsi ait un comportement qui ressemble a celui d'un bouton d'appareil quelconque dans le monde reel. Or la fonctionnalite d'un bouton reel (sa capacite de fermer ou d'ouvrir un circuit electrique) est bien integree dans l'objet lui-meme (au meme titre que d'autres proprietes telles que sa taille, sa couleur, etc.) De la meme maniere, nous souhaiterons que les differentes caracteristiques de notre bouton logiciel (sa taille, son emplacement, sa couleur, le texte qu'il supporte), mais aussi la definition de ce qui se passe lorsque Ton effectue differentes actions de la souris sur ce bouton, soient regroupes dans une entite bien precise a l'interieur du programme, de maniere telle qu'il n'y ait pas de confusion avec un autre bouton ou d'autres entites. 12.1 Definition d'une methode Pour illustrer notre propos, nous allons definir une nouvelle classe Time, qui nous permettra d'effectuer toute une serie d'operations sur des instants, des durees, etc. : >» class Time: "Definition d'une classe temporelle" Creons a present un objet de ce type, et ajoutons-lui des variables d'instance pour memoriser les heures, minutes et secondes : »> instant = Time ( ) »> instant . heure = 11 »> instant .minute = 34 »> instant . seconde = 25 A titre d'exercice, ecrivez maintenant vous-meme une fonction affiche_heure() , qui serve a visualiser le contenu d'un objet de classe Time() sous la forme conventionnelle « heure:minute:seconde ». Appliquee a l'objet instant cree ci-dessus, cette fonction devrait done afficher 11:34:25 : »> print affiche_heure (instant) 11:34 : 25 Gerard Swinnen : Apprendre a programmer avec Python 159. Votre fonction ressemblera probablement a ceci : >>> def af f iche_heure (t ) : print str(t.heure) + ":" + str (t .minute) + ":" + str (t . seconde) (Notez au passage ['utilisation de la fonction str() pour convertir les donnees numeriques en chaines de caracteres). Si par la suite vous utilisez frequemment des objets de la classe Time(), il y a gros a parier que cette fonction d'affichage vous sera frequemment utile. II serait done probablement fort judicieux d'encapsuler cette fonction affiche_heure() dans la classe Time() elle-meme, de maniere a s'assurer qu'elle soit toujours automatiquement disponible chaque fois que Ton doit manipuler des objets de la classe Time(). Une fonction qui est ainsi encapsulee dans une classe s'appelle une methode. Vous avez deja rencontre des methodes a de nombreuses reprises (et vous savez done deja qu'une methode est bien une fonction associee a une classe d'objets). Definition concrete d'une methode : On definit une methode comme on definit une fonction, avec cependant deux differences : ♦ La definition d'une methode est toujours placee a Vinterieur de la definition d'une classe, de maniere a ce que la relation qui lie la methode a la classe soit clairement etablie. ♦ Le premier parametre utilise par une methode doit toujours etre une reference d'instance. Vous pourriez en principe utiliser un nom de variable quelconque pour ce parametre, mais il est vivement conseille de respecter la convention qui consiste a toujours lui donner le nom : self. Le parametre self designe done l'instance a laquelle la methode sera associee, dans les instructions faisant partie de la definition. (De ce fait, la definition d'une methode comporte toujours au moins un parametre, alors que la definition d'une fonction peut n'en comporter aucun). Voyons comment cela se passe en pratique : Pour re-ecrire la fonction affiche_heure() comme une methode de la classe Time(), il nous suffit de deplacer sa definition a l'interieur de celle de la classe, et de changer le nom de son parametre : >>> class Time: "Nouvelle classe temporelle" def af f iche_heure (self ) : print str (self .heure) + ":" + str (self .minute) \ + ":" + str (self .seconde) La definition de la methode fait maintenant partie du bloc destructions indentees apres l'instruction class. Notez bien l'utilisation du mot reserve self , qui se refere done a toute instance susceptible d'etre creee a partir de cette classe. (Note : Le code \ permet de continuer une instruction trop longue sur la ligne suivante). 160. Gerard Swinnen : Apprendre a programmer avec Python Essai de la methode dans une instance Nous pouvons des a present instancier un objet de notre nouvelle classe Time() : »> maintenant = Time ( ) Si nous essayons d'utiliser un peu trop vite notre nouvelle methode, ca ne marche pas : »> maintenant . af f iche_heure ( ) AttributeError : ' Time ' instance has no attribute ' heure ' C'est normal : nous n'avons pas encore cree les attributs d'instance. II faudrait faire par exemple : »> maintenant . heure = 13 »> maintenant .minute = 34 >» maintenant . seconde = 21 »> maintenant . af f iche_heure ( ) 13:34 :21 Nous avons cependant deja signale a plusieurs reprises qu'il n'est pas recommandable de creer ainsi les attributs d'instance en dehors de l'objet lui-meme, ce qui conduit (entre autres desagrements) a des erreurs comme celle que nous venons de rencontrer, par exemple. Voyons done a present comment nous pouvons mieux faire. 12.2 La methode « constructeur » L'erreur que nous avons rencontree au paragraphe precedent est-elle evitable ?. Elle ne se produirait effectivement pas, si nous nous etions arranges pour que la methode affiche_heure() puisse toujours afficher quelque chose, sans qu'il ne soit necessaire d'effectuer au prealable aucune manipulation sur l'objet nouvellement cree. En d'autres termes, il serait judicieux que les variables d'instance soient predefinies elles aussi a I'interieur de la classe, avec pour chacune d'elles une valeur « par defaut ». Pour obtenir cela, nous allons faire appel a une methode particuliere, que Ton appelle un constructeur. Une methode constructeur est une methode qui est executee automatiquement lorsque Ton instancie un nouvel objet a partir de la classe. On peut y placer tout ce qui semble necessaire pour initialiser automatiquement l'objet que Ton cree. Sous Python, la methode constructeur doit obligatoirement s'appeler init (deux caracteres « souligne », le mot init, puis encore deux caracteres « souligne »). Exemple : »> class Time: "Encore une nouvelle classe temporelle" def init (self) : self. heure =0 self. minute =0 self. seconde =0 def af f iche_heure (self ) : print str (self . heure) + ":" + str (self .minute) \ + ":" + str (self .seconde) »> tstart = Time() »> tstart . af f iche_heure ( ) 0:0:0 Gerard Swinnen : Apprendre a programmer avec Python 161. L'interet de cette technique apparaitra plus clairement si nous ajoutons encore quelque chose. Comme toute methode qui se respecte, la methode init () peut etre dotee de parametres. Ceux- ci vont jouer un role important, parce qu'ils vont permettre d'instancier un objet et d'initialiser certaines de ses variables d'instance, en une seule operation. Dans l'exemple ci-dessus, veuillez done modifier la definition de la methode init () comme suit : def init (self, hh =0, mm =0, ss =0) : self.heure = hh self .minute = mm self . seconde = ss La methode init () comporte a present 3 parametres, avec pour chacun une valeur par defaut. Pour lui transmettre les arguments correspondants, il suffit de placer ceux-ci dans les parentheses qui accompagnent le nom de la classe, lorsque Ton ecrit l'instruction d'instanciation du nouvel objet. Voici par exemple la creation et l'initialisation simultanees d'un nouvel objet Time() : »> recreation = Time (10, 15, 18) >>> recreation . af f iche_heure ( ) 10:15:18 Puisque les variables d'instance possedent maintenant des valeurs par defaut, nous pouvons aussi bien creer de tels objets Time() en omettant un ou plusieurs arguments : »> rentree = Time (10, 30) >>> rentree . affiche_heure () 10:30:0 (12) Exercices : 12.1. Definissez une classe Domino() qui permette d'instancier des objets simulant les pieces d'un jeu de dominos. Le constructeur de cette classe initialisera les valeurs des points presents sur les deux faces A et B du domino (valeurs par defaut = 0). Deux autres methodes seront definies : une methode affiche_points() qui affiche les points presents sur les deux faces une methode valeur() qui renvoie la somme des points presents sur les 2 faces. Exemples d'utilisation de cette classe : »> dl = Domino (2, 6) »> 62 = Domino (4,3) »> dl . af f iche_points ( ) face A : 2 face B : 6 »> d2 . af f iche_points ( ) face A : 4 face B : 3 »> print "total des points :", dl. valeur () + d2. valeur () 15 »> liste_dominos = [] »> for i in range (7) : liste_dominos . append (Domino (6 , i) ) »> print liste_dominos etc. , etc. 162. Gerard Swinnen : Apprendre a programmer avec Python 12.2. Definissez une classe CompteBancaire(), qui permette d'instancier des objets tels que comptel, compte2, etc. Le constructeur de cette classe initialisera deux attributs d'instance nom et solde, avec les valeurs par defaut 'Dupont' et 1000. Trois autres methodes seront definies : - depot(somme) permettra d'aj outer une certaine somme au solde - retrait(somme) permettra de retirer une certaine somme du solde - affiche() permettra d'afficher le nom du titulaire et le solde de son compte. Exemples d'utilisation de cette classe : »> comptel = CompteBancaire ( ' Duchmol ' , 800) »> comptel. depot (350) »> comptel. retrait (200) »> comptel . af f iche ( ) Le solde du compte bancaire de Duchmol est de 950 euros . »> compte2 = CompteBancaire () »> compte2 .depot (25) »> compte2 . af f iche ( ) Le solde du compte bancaire de Dupont est de 1025 euros. 12.3. Definissez une classe Voiture() qui permette d'instancier des objets reproduisant le comportement de voitures automobiles. Le constructeur de cette classe initialisera les attributs d'instance suivants, avec les valeurs par defaut indiquees : marque = 'Ford', couleur = 'rouge', pilote = 'personne', vitesse = 0. Lorsque Ton instanciera un nouvel objet Voiture(), on pourra choisir sa marque et sa couleur, mais pas sa vitesse, ni le nom de son conducteur. Les methodes suivantes seront definies : - choix_conducteur(nom) permettra de designer (ou changer) le nom du conducteur - accelerer(taux, duree) permettra de faire varier la vitesse de la voiture. La variation de vitesse obtenue sera egale au produit : taux x duree. Par exemple, si la voiture accelere au taux de 1,3 m/s 2 pendant 20 secondes, son gain de vitesse doit etre egal a 26 m/s. Des taux negatifs seront acceptes (ce qui permettra de decelerer). La variation de vitesse ne sera pas autorisee si le conducteur est 'personne'. - affiche_tout() permettra de faire apparaitre les proprietes presentes de la voiture, c'est-a- dire sa marque, sa couleur, le nom de son conducteur, sa vitesse. Exemples d'utilisation de cette classe : »> al = Voiture (' Peugeot ' , 'bleue') »> a2 = Voiture (couleur = 'verte') »> a3 = Voiture ( 'Mercedes ' ) »> al . choix_conducteur ( ' Romeo ' ) »> a2 . choix_conducteur ( ' Juliette ' ) »> a2.accelerer(1.8, 12) »> a3.accelerer(1.9, 11) Cette voiture n'a pas de conducteur ! »> a2.affiche_tout() Ford verte pilotee par Juliette, vitesse = 21.6 m/s. »> a3.affiche_tout () Mercedes rouge pilotee par personne, vitesse = 0 m/s. Gerard Swinnen : Apprendre a programmer avec Python 163. 12.4. Definissez une classe Satellite() qui permette d'instancier des objets simulant des satellites artificiels lances dans l'espace, autour de la terre. Le constructeur de cette classe initialisera les attributs d'instance suivants, avec les valeurs par defaut indiquees : masse = 100, vitesse = 0. Lorsque Ton instanciera un nouvel objet Satellite(), on pourra choisir son nom, sa masse et sa vitesse. Les methodes suivantes seront definies : - impulsion(force, duree) permettra de faire varier la vitesse du satellite. Pour savoir comment, rappelez-vous votre cours de physique : la variation de vitesse A v subie par un FXt objet de masse m soumis a Taction dune force F pendant un temps t vaut A v— . Par exemple : un satellite de 300 kg qui sub it une force de 600 Newtons pendant 10 secondes voit sa vitesse augmenter (ou diminuer) de 20 m/s. - affiche_vitesse() affichera le nom du satellite et sa vitesse courante. - energie() renverra au programme appelant la valeur de l'energie cinetique du satellite. mXv 2 Rappel : l'energie cinetique se calcule a l'aide de la formule E c — — - — Exemples d'utilisation de cette classe : »> si = Satellite (' Zoe ' , masse =250, vitesse =10) »> si . impulsion (500 , 15) »> si . af f iche_vitesse ( ) vitesse du satellite Zoe = 40 m/s. »> print sl.energie() 200000 »> si . impulsion (500 , 15) »> si . af f iche_vitesse ( ) vitesse du satellite Zoe = 70 m/s. »> print sl.energie() 612500 164. Gerard Swinnen : Apprendre a programmer avec Python 12.3 Espaces de noms des classes et instances Vous avez appris precedemment (voir page 68) que les variables definies a l'interieur d'une fonction sont des variables locales, inaccessibles aux instructions qui se trouvent a l'exterieur de la fonction. Cela vous permet d'utiliser les memes noms de variables dans differentes parties d'un programme, sans risque d' interference. Pour decrire la meme chose en d'autres termes, nous pouvons dire que chaque fonction possede son propre espace de noms, independant de l'espace de noms principal. Vous avez appris egalement que les instructions se trouvant a l'interieur d'une fonction peuvent acceder aux variables definies au niveau principal, mais en lecture seulement : elles peuvent utiliser les valeurs de ces variables, mais pas les modifier (a moins de faire appel a l'instruction global). II existe done une sorte de hierarchie entre les espaces de noms. Nous allons constater la meme chose a propos des classes et des objets. En effet : ♦ Chaque classe possede son propre espace de noms. Les variables qui en font partie sont appelees les attributs de la classe. ♦ Chaque objet instance (cree a partir d'une classe) obtient son propre espace de noms. Les variables qui en font partie sont appelees variables d' instance ou attributs d' instance. ♦ Les classes peuvent utiliser (mais pas modifier) les variables definies au niveau principal. ♦ Les instances peuvent utiliser (mais pas modifier) les variables definies au niveau de la classe et les variables definies au niveau principal. Considerons par exemple la classe Time() definie precedemment. A la page 162, nous avons instancie deux objets de cette classe : recreation et rentree. Chacun a ete initialise avec des valeurs differentes, independantes. Nous pouvons modifier et reafficher ces valeurs a volonte dans chacun de ces deux objets, sans que l'autre n'en soit affecte : »> recreation . heure = 12 »> rentree . af f iche_heure ( ) 10:30:0 »> recreation . af f iche_heure ( ) 12:15:18 Veuillez a present encoder et tester l'exemple ci-dessous : »> class Espaces : # 1 aa = 33 #2 def affiche(self) : # 3 print aa, Espaces. aa, self.aa # 4 »> aa = 12 #5 »> essai = Espaces () # 6 »> essai. aa =67 #7 »> essai .affiche () # 8 12 33 67 »> print aa, Espaces. aa, essai. aa # 9 12 33 67 Dans cet exemple, le meme nom aa est utilise pour definir trois variables differentes : une dans l'espace de noms de la classe (a la ligne 2), une autre dans l'espace de noms principal (a la ligne 5), et enfin une derniere dans l'espace de nom de l'instance (a la ligne 7). La ligne 4 et la ligne 9 montrent comment vous pouvez acceder a ces trois espaces de noms (de l'interieur d'une classe, ou au niveau principal), en utilisant la qualification par points. Notez encore une fois l'utilisation de self pour designer l'instance. Gerard Swinnen : Apprendre a programmer avec Python 165. 12.4 Heritage Les classes constituent le principal outil de la programmation orientee objet (Object Oriented Programming ou OOP), qui est consideree de nos jours comme la technique de programmation la plus performante. L'un des principaux atouts de ce type de programmation reside dans le fait que Ton peut toujours se servir d'une classe preexistante pour en creer une nouvelle qui possedera quelques fonctionnalites differentes ou supplementaires. Le procede s'appelle derivation. II permet de creer toute une hierarchic de classes allant du general au particulier. Nous pouvons par exemple definir une classe Mammifere(), qui contiendra un ensemble de caracteristiques propres a ce type d'animal. A partir de cette classe, nous pourrons alors deriver une classe Primate(), une classe Rongeur(), une classe Carnivore(), etc., qui heriteront toutes les caracteristiques de la classe Mammifere(), en y ajoutant leurs specificites. Au depart de la classe Carnivore(), nous pourrons ensuite deriver une classe Belette(), une classe Loup(), une classe Chien(), etc., qui heriteront encore une fois toutes les caracteristiques de la classe parente avant d'y ajouter les leurs. Exemple : >>> class Mammif ere : caractl = "il allaite ses petit* ; " >>> class Carnivore (Mammif ere) : caract2 = "il se nourrit de la chair de ses proies ; " >>> class Chien (Carnivore) : caract3 = "son cri s'appelle aboiement ; " >>> mirza = Chien () >>> print mirza . caractl , mirza . caract2 , mirza . caract3 il allaite ses petit* ; il se nourrit de la chair de ses proies ; son cri s ' appelle aboiement ; Dans cet exemple, nous voyons que l'objet mirza , qui est une instance de la classe Chien(), herite non seulement l'attribut defini pour cette classe, mais egalement des attributs definis pour les classes parentes. Vous voyez egalement dans cet exemple comment il faut proceder pour deriver une classe a partir d'une classe parente : On utilise l'instruction class , suivie comme d'habitude du nom que Ton veut attribuer a la nouvelle classe, et on place entre parentheses le nom de la classe parente. Notez bien que les attributs utilises dans cet exemple sont des attributs des classes (et non des attributs d'instances). L'instance mirza peut acceder a ces attributs, mais pas les modifier : >>> mirza . caract2 = "son corps est couvert de poils" # 1 >>> print mirza . caract2 # 2 son corps est couvert de poils # 3 »> fido = Chien () # 4 >>> print fido . caract2 # 5 il se nourrit de la chair de ses proies ; # 6 Dans ce nouvel exemple, la ligne 1 ne modifie pas l'attribut caract2 de la classe Carnivore(), contrairement a ce que Ton pourrait penser au vu de la ligne 3. Nous pouvons le verifier en creant une nouvelle instance fido (lignes 4 a 6) . Si vous avez bien assimile les paragraphes precedents, vous aurez compris que l'instruction de la ligne 1 cree une nouvelle variable d'instance associee seulement a l'objet mirza. II existe done des ce moment deux variables avec le meme nom caract2 : l'une dans l'espace de noms de l'objet mirza, et l'autre dans l'espace de noms de la classe Carnivore(). 166. Gerard Swinnen : Apprendre a programmer avec Python Comment faut-il alors interpreter ce qui s'est passe aux lignes 2 et 3 ? Comme nous l'avons vu plus haut, l'instance mirza peut acceder aux variables situees dans son propre espace de noms, mais aussi a celles qui sont situees dans les espaces de noms de toutes les classes parentes. S'il existe des variables aux noms identiques dans plusieurs de ces espaces, laquelle sera-t-elle selectionnee lors de 1' execution d'une instruction comme celle de la ligne 2 ? Pour resoudre ce conflit, Python respecte une regie de priorite fort simple. Lorsqu'on lui demande d'utiliser la valeur d'une variable nommee alpha, par exemple, il commence par rechercher ce nom dans l'espace local (le plus « interne », en quelque sorte). Si une variable alpha est trouvee dans l'espace local, c'est celle-la qui est utilisee, et la recherche s'arrete. Sinon, Python examine l'espace de noms de la structure parente, puis celui de la structure grand-parente, et ainsi de suite jusqu'au niveau principal du programme. A la ligne 2 de notre exemple, c'est done la variable d'instance qui sera utilisee. A la ligne 5, par contre, c'est seulement au niveau de la classe grand-parente qu'une variable repondant au nom caract2 peut etre trouvee. C'est done celle-la qui est affichee. 12.5 Heritage et polymorph isme Analysez soigneusem*nt le script de la page suivante. II met en oeuvre plusieurs concepts decrits precedemment, en particulier le concept d'heritage. Pour bien comprendre ce script, il faut cependant d'abord vous rappeler quelques notions elementaires de chimie. Dans votre cours de chimie, vous avez certainement du apprendre que les atomes sont des entries constitutes d'un certain nombre de protons (particules chargees d'electricite positive), ^electrons (charges negativement) et de neutrons (neutres). Le type d'atome (ou element) est determine par le nombre de protons, que Ton appelle egalement numero atomique. Dans son etat fondamental, un atome contient autant d'electrons que de protons, et par consequent il est electriquement neutre. II possede egalement un nombre variable de neutrons, mais ceux-ci n'influencent en aucune maniere la charge electrique globale. Dans certaines circonstances, un atome peut gagner ou perdre des electrons. II acquiert de ce fait une charge electrique globale, et devient alors un ion (il s'agit d'un ion negatif si l'atome a gagne un ou plusieurs electrons, et d'un ion positif s'il en a perdu). La charge electrique d'un ion est egale a la difference entre le nombre de protons et le nombre d'electrons qu'il contient. Le script reproduit a la page suivante genere des objets « atome » et des objets « ion ». Nous avons rappele ci-dessus qu'un ion est simplement un atome modifie. Dans notre programmation, la classe qui definit les objets « ion » sera done une classe derivee de la classe « atome » : elle heritera d'elle tous ses attributs et toutes ses methodes, en y ajoutant les siennes propres. L'une de ces methodes ajoutees (la methode affiche()) remplace une methode de meme nom heritee de la classe « atome ». Les classes « atome » et « ion » possedent done chacune une methode de meme nom, mais qui effectuent un travail different. On parle dans ce cas de polymorphisme. On pourra dire egalement que la methode affiche() a ete surcharges II sera evidemment possible d'instancier un nombre quelconque d'atomes et d'ions a partir de ces deux classes. Or l'une d'entre elles (la classe « atome ») doit contenir une version simplifiee du tableau periodique des elements (tableau de Mendeleev), de facon a pouvoir attribuer un nom d'element chimique, ainsi qu'un nombre de neutrons, a chaque objet genere. Comme il n'est pas souhaitable de recopier tout ce tableau dans chacune des instances, nous le placerons dans un attribut de classe. Ainsi ce tableau n'existera qu'en un seul endroit en memoire, tout en restant accessible a tous les objets qui seront produits a partir de cette classe. Gerard Swinnen : Apprendre a programmer avec Python 167. Voyons concretement comment toutes ces idees s'articulent : class Atome: """atomes simplifies, choisis parmi les 10 premiers elements du TP""" table =[None, ( ' hydrogene ' , 0 ) , ( 'helium' , 2) , (' lithium' , 4) , ( 'beryllium' , 5) , ('bore ',6), ( ' carbone ' , 6) , ('azote ',7), ( 'oxygene' , 8) , ( ' f luor ' , 10) , ('neon ',10)] def init (self, nat) : "le n° atomique determine le n. de protons, d' electrons et de neutrons" self.np, self.ne = nat, nat # nat = numero atomique self.nn = Atome . table [nat ] [1] # nb. de neutrons trouves dans table def af f iche (self ) : print print "Nom de 1 ' element :", Atome . table [self . np] [0] print "%s protons, %s electrons, %s neutrons" % \ (self.np, self.ne, self.nn) class Ion (Atome): """les ions sont des atomes qui ont gagne ou perdu des electrons""" def init (self, nat, charge) : "le n° atomique et la charge electrique determinent l'ion" Atome. init (self, nat) self.ne = self.ne - charge self. charge = charge def aff iche (self ) : "cette methode remplace celle heritee de la classe parente" Atome . aff iche (self ) # ... tout en l'utilisant elle-meme ! print "Particule electrisee. Charge =", self. charge ### Programme principal : ### al = Atome (5) a2 = Ion (3, 1) a3 = Ion (8, -2) al . aff iche () a2 .aff iche () a3 .aff iche () L'execution de ce script provoque l'affichage suivant : Nom de 1 ' element : bore 5 protons, 5 electrons, 6 neutrons Nom de 1 ' element : lithium 3 protons, 2 electrons, 4 neutrons Particule electrisee. Charge = 1 Nom de 1 ' element : oxygene 8 protons, 10 electrons, 8 neutrons Particule electrisee. Charge = -2 Au niveau du programme principal, vous pouvez constater que Ton instancie les objets Atome() en fournissant leur numero atomique (lequel doit etre compris entre 1 et 10). Pour instancier des objets Ion(), par contre, on doit fournir un numero atomique et une charge electrique globale (positive ou negative). La meme methode affiche() fait apparaitre les proprietes de ces objets, qu'il s'agisse d'atomes ou d'ions, avec dans le cas de l'ion une ligne supplemental (polymorphisme). 168. Gerard Swinnen : Apprendre a programmer avec Python Commentaires : La definition de la classe Atome() commence par l'assignation de la variable table. Une variable definie a cet endroit fait partie de l'espace de noms de la classe. C'est done un attribut de classe, dans lequel nous placons une liste d'informations concernant les 10 premiers elements du tableau periodique de Mendeleev. Pour chacun de ces elements, la liste contient un tuple : (nom de l'element, nombre de neutrons), a l'indice qui correspond au numero atomique. Comme il n'existe pas d'element de numero atomique zero, nous avons place a l'indice zero dans la liste, l'objet special None. (A priori, nous aurions pu placer a cet endroit n'importe quelle autre valeur, puisque cet indice ne sera pas utilise. L'objet None de Python nous semble cependant particulierement explicite). Viennent ensuite les definitions de deux methodes : • Le constructeur init 0 sert essentiellement ici a generer trois attributs d'instance, destines a memoriser respectivement les nombres de protons, d'electrons et de neutrons pour chaque objet atome construit a partir de cette classe (Les attributs d'instance sont des variables liees a self). Notez bien la technique utilisee pour obtenir le nombre de neutrons a partir de l'attribut de classe, en mentionnant le nom de la classe elle-meme dans une qualification par points. • La methode affiche() utilise a la fois les attributs d'instance, pour retrouver les nombres de protons, d'electrons et de neutrons de l'objet courant, et l'attribut de classe (lequel est commun a tous les objets) pour en extraire le nom d'element correspondant. Veuillez aussi remarquer au passage 1'utilisation de la technique de formatage des chaines (cfr. page 130). La definition de la classe Ion() comporte des parentheses. II s'agit done d'une classe derivee, sa classe parente etant bien entendu la classe Atome() qui precede. Les methodes de cette classe sont des variantes de celles de la classe atome. Elles devront done vraisemblablement faire appel a celles-ci. Cette remarque est importante : Comment peut-on, a I'interieur de la definition d'une classe, faire appel a une methode definie dans une autre classe ? II ne faut pas perdre de vue, en effet, qu'une methode se rattache toujours a l'instance qui sera generee a partir de la classe (instance representee par self dans la definition). Si une methode doit faire appel a une autre methode definie dans une autre classe, il faut pouvoir lui transmettre la reference de l'instance a laquelle elle doit s'associer. Comment faire ? C'est tres simple : Lorsque dans la definition d'une classe, on souhaite faire appel a une methode definie dans une autre classe, on doit lui transmettre la reference de l'instance comme premier argument. C'est ainsi que dans notre script, par exemple, la methode affiche() de la classe Ion() peut faire appel a la methode affiche() de la classe Atome() : les informations affichees seront bien celles de l'objet-ion courant, puisque sa reference a ete transmise dans l'instruction d'appel : Atome . af f iche (self) (dans cette instruction, self est bien entendu la reference de l'instance courante). De la meme maniere (vous en verrez de nombreux autres exemples plus loin), la methode constructeur de la classe Ion() fait appel a la methode constructeur de sa classe parente, dans : Atome. init (self, nat) Gerard Swinnen : Apprendre a programmer avec Python 169. r Resume : Definition et utilisation d'une classe #################################### # Programme Python type # # auteur : G.Swinnen, Liege, 2003 # # licence : GPL # #################################### class Point: """point mathematique""" def init (self, x, y) : self .x = x self .y = y class Rectangle: """rectangle" " " def init (self, ang, lar, hau) : self .ang = ang self . lar = lar self .hau = hau def trouveCentre (self) : xc = self .ang. x + self. lar /2 yc = self .ang. y + self. hau /2 return Point (xc, yc) class Carre (Rectangle) : """carre = rectangle particulier" " " def init (self, coin, cote) : Rectangle. init (self, coin, cote, cote) self .cote = cote def surface (self) : return self .cote**2 ########################### ## Programme principal : ## # coord, de 2 coins sup. gaudies : csgR = Point (40, 30) csgC = Point (10, 25) # "boites" rectangulaire et carree : boiteR = Rectangle (csgR, 100, 50) boiteC = Carre (csgC, 40) # Coordonnees du centre pour chacune cR = boiteR. trouveCentre () cC = boiteC . trouveCentre ( ) print "centre du rect. print "centre du carre print "surf, du carre : print boiteC. surface () cR.x, cR.y cC.x, cC.y La classe est un moule servant d produire des objets. Chacun d'euxsera une instance de la classe consideree. Les instances de la classe Point() seront des objets tres simples qui possederont seulement un attribut 'x' et un attribut 'y' ; Us ne seront dotes d'aucune methode. Le parametre self designe toutes les instances qui seront produites par cette classe Les instances de la classe RectangleQ possederont 3 attributs : le premier ( 'ang' ) doit etre lui-meme un objet de classe Point(). II servira a memoriser les coordonnees de V angle sup erieur gauche du rectangle. La classe RectangleQ comporte une methode, qui renverra un objet de classe PointQ au programme appelant. CarreQ est une classe derivee, qui herite les attributs et methodes de la classe RectangleQ. Son constructeur doit /aire appel au constructeur de la classe parente, en lui transmettant la reference de I'instance (self) comme premier argument. % La classe CarreQ comporte une methode de plus que sa classe parente. Pour creer (ou instancier) un objet, il suffit d'affecter une classe a une variable. Les instructions ci-contre creent done deux objets de la classe PointQ... ... et celles-ci, encore deux autres objets. Note : par convention, le nom d'une classe commence par une lettre majuscule La methode trouveCentreQ fonctionne pour les objets des deux types, puisque la classe CarreQ a herite de classe RectangleQ. Par contre, la methode surfaceQ ne peut etre invoquee que pour les objets carres. 170. Gerard Swinnen : Apprendre a programmer avec Python 12.6 Modules contenant des bibliotheques de classes Vous connaissez deja depuis longtemps l'utilite des modules Python. Vous savez qu'ils servent a regrouper des bibliotheques de classes et de fonctions. A titre d'exercice de revision, vous allez creer vous-meme un nouveau module de classes, en encodant les lignes d'instruction ci-dessous dans un fichier que vous nommerez formes.py : class Rectangle : "Classe de rectangles" def init (self, longueur =30, largeur =15) : self.L = longueur self.l = largeur self.nom ="rectangle" def perimetre (self ) : return " (%s + %s) * 2 = %s" % (self.L, self.l, (self.L + self.l) *2) def surface (self ) : return "%s * %s = %s" % (self.L, self.l, self .L*self . 1) def mesures (self ) : print "Un %s de %s sur %s" % (self.nom, self.L, self.l) print "a une surface de %s" % (self . surf ace (), ) print "et un perimetre de %s\n" % (self .perimetre (), ) class Carre (Rectangle) : "Classe de carres" def init (self, cote =10) : Rectangle. init (self, cote, cote) self.nom ="carre" if name == " main " : rl = Rectangle (15, 30) rl .mesures () cl = Carre (13) cl .mesures () Une fois ce module enregistre, vous pouvez l'utiliser de deux manieres : Soit vous en lancez l'execution comme celle d'un programme ordinaire, soit vous l'importez dans un script quelconque ou depuis la ligne de commande, pour en utiliser les classes : »> import formes »> fl = formes .Rectangle (27, 12) »> f 1. mesures () Un rectangle de 27 sur 12 a une surface de 27 * 12 = 324 et un perimetre de (27 +12) * 2 = 78 »> f2 = formes .Carre (13) »> f 2. mesures () Un car re de 13 sur 13 a une surface de 13 * 13 = 169 et un perimetre de (13 +13) * 2 = 52 Gerard Swinnen : Apprendre a programmer avec Python 111. On voit dans ce script que la classe Carre() est construite par derivation a partir de la classe Rectangle() dont elle herite toutes les caracteristiques. En d'autres termes, la classe Carre() est une classe fille de la classe Rectangle(). Vous pouvez remarquer encore une fois que le constructeur de la classe Carre() fait appel au constructeur de sa classe parente ( Rectangle. init () ), en lui transmettant la reference de l'instance (c'est-a-dire self) comme premier argument. Quant a l'instruction : if name == " main " : placee a la fin du module, elle sert a determiner si le module est « lance » en tant que programme (auquel cas les instructions qui suivent doivent etre executees), ou au contraire utilise comme une bibliotheque de classes importee ailleurs. Dans ce cas cette partie du code est sans effet. Exercices : 12.5. Definissez une classe Cercle(). Les objets construits a partir de cette classe seront des cercles de tailles variees. En plus de la methode constructeur (qui utilisera done un parametre rayon), vous definirez une methode surface(), qui devra renvoyer la surface du cercle. Definissez ensuite une classe Cylindre() derivee de la precedente. Le constructeur de cette nouvelle classe comportera les deux parametres rayon et hauteur. Vous y ajouterez une methode volume() qui devra renvoyer le volume du cylindre. (Rappel : Volume d'un cylindre = surface de section x hauteur). Exemple d'utilisation de cette classe : »> cyl = Cylindre (5, 7) »> print cyl . surf ace () 78.54 >>> print cyl . volume ( ) 549.78 12.6. Completez l'exercice precedent en lui ajoutant encore une classe Cone(), qui devra deriver cette fois de la classe Cylindre(), et dont le constructeur comportera lui aussi les deux parametres rayon et hauteur. Cette nouvelle classe possedera sa propre methode volume(), laquelle devra renvoyer le volume du cone. (Rappel : Volume d'un cone = volume du cylindre correspondant divise par 3). Exemple d'utilisation de cette classe : »> co = Cone (5, 7) >>> print co . volume ( ) 183.26 172. Gerard Swinnen : Apprendre a programmer avec Python 12.7. Definissez une classe JeuDeCartes() permettant d'instancier des objets «jeu de cartes » dont le comportement soit similaire a celui dun vrai jeu de cartes. La classe devra comporter au moins les trois methodes suivantes : - methode constructeur : creation et remplissage d'une liste de 52 elements, qui sont eux- memes des tuples de 2 elements contenant les caracteristiques de chacune des 52 cartes. Pour chacune d'elles, il faut en effet memoriser separement un nombre entier indiquant la valeur (2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, les 4 dernieres valeurs etant celles des valet, dame, roi et as), et un autre nombre entier indiquant la couleur de la carte (c'est-a-dire 3,2,1,0 pour Cceur, Carreau, Trefle & Pique). Dans une telle liste, l'element (1 1,2) designe done le valet de Trefle, et la liste terminee doit etredutype: [(2, 0), (3,0), (3,0), (4,0), (12,3), (13,3), (14,3)] - methode nom_carte() : cette methode renvoie sous la forme d'une chaine l'identite d'une carte quelconque, dont on lui a fourni le tuple descripteur en argument. Par exemple, l'instruction : print jeu.nom_carte( (14, 3)) doit provoquer l'affichage de : As de pique - methode battre() : comme chacun sait, battre les cartes consiste a les melanger. Cette methode sert done a melanger les elements de la liste contenant les cartes, quel qu'en soit le nombre. - methode tirer() : lorsque cette methode est invoquee, une carte est retiree du jeu. Le tuple contenant sa valeur et sa couleur est renvoye au programme appelant. On retire toujours la premiere carte de la liste. Si cette methode est invoquee alors qu'il ne reste plus aucune carte dans la liste, il faut alors renvoyer l'objet special None au programme appelant. Exemple d'utilisation de la classe JeuDeCartes() : jeu = JeuDeCartes ( ) jeu. battre () for n in range (53) : c = jeu.tirer() if c == None : print ' Termine ! ' else : print j eu . nom_car te ( c ) # instanciation d'un objet # melange des cartes # tirage des 52 cartes : # il ne reste plus aucune carte # dans la liste # valeur et couleur de la carte 12.8. Complement de l'exercice precedent : Definir deux joueurs A et B. Instancier deux jeux de cartes (un pour chaque joueur) et les melanger. Ensuite, a l'aide d'une boucle, tirer 52 fois une carte de chacun des deux jeux et comparer leurs valeurs. Si e'est la premiere des 2 qui a la valeur la plus elevee, on ajoute un point au joueur A. Si la situation contraire se presente, on ajoute un point au joueur B. Si les deux valeurs sont egales, on passe au tirage suivant. Au terme de la boucle, comparer les comptes de A et B pour determiner le gagnant. Gerard Swinnen : Apprendre a programmer avec Python 173. Chapitre 13 : Classes & Interfaces graphiques La programmation orientee objet convient particulierement bien au developpement d'applications avec interface graphique. Des bibliotheques de classes comme Tkinter ou wxPython fournissent une base de widgets tres etoffee, que nous pouvons adapter a nos besoins par derivation. Dans ce chapitre, nous allons utiliser a nouveau la bibliotheque Tkinter, mais en appliquant les concepts decrits dans les pages precedentes, et en nous efforcant de mettre en evidence les avantages qu'apporte Vorientation objet dans nos programmes. 13.1 « Code des couleurs » : un petit projet bien encapsule Nous allons commencer par un petit projet qui nous a ete inspire par le cours d' initiation a l'electronique. L'application que nous decrivons ci-apres permet de retrouver rapidement le code de trois couleurs qui correspond a une resistance electrique de valeur bien determines Pour rappel, la fonction des resistances electriques consiste a s'opposer (a resister) plus ou moins bien au passage du courant. Les resistances se presentent concretement sous la forme de petites pieces tubulaires cerclees de bandes de couleur (en general 3). Ces bandes de couleur indiquent la valeur numerique de la resistance, en fonction du code suivant : Chaque couleur correspond conventionnellement a I'un des chiffres de zero a neuf : Noir = 0 ; Brun = 1 ; Rouge = 2 ; Orange = 3 ; Jaune = 4 ; Vert = 5 ; Bleu = 6 ; Violet = 7 ; Gris = 8 ; Blanc = 9. On oriente la resistance de maniere telle que les bandes colorees soient placees a gauche. La valeur de la resistance - exprim.ee en ohms (£l ) - s'obtient en lis ant ces bandes colorees egalement a partir de la gauche : les deux premieres bandes indiquent les deux premiers chiffres de la valeur numerique ; il faut ensuite accoler a ces deux chiffres un nombre de zeros egal a Vindication fournie par la troisieme bande. Exemple concret : Supposons qu'd partir de la gauche, les bandes colorees soient jaune, violette et verte. La valeur de cette resistance est 4700000 Q. , ou 4700 k£l , ou encore 4, 7 MCI. Ce systeme ne permet evidemment de preciser une valeur numerique qu'avec deux chiffres significatifs seulement. LI est toutefois considere comme largement suffisant pour la plupart des applications electroniques « ordinaires » (radio, TV, etc.) a) Cahier des charges de notre programme : Notre application doit faire apparaitre une fenetre comportant un dessin de la resistance, ainsi qu'un champ d'entree dans lequel l'utilisateur peut encoder une valeur numerique. Un bouton « Montrer » declenche la modification du dessin de la resistance, de telle facon que les trois bandes de couleur se mettent en accord avec la valeur numerique introduite. Contrainte : Le programme doit accepter toute entree numerique fournie sous forme entiere ou reelle, dans les limites de 10 a 10 11 Q,. Par exemple, une valeur telle que 4.78e6 doit etre acceptee et arrondie correctement, c'est-a-dire convertie en 4800000 Q.. Code des couleurs Montrer Entrez la valeur de la resistance, en | 47000 ohms : Quitter 174. Gerard Swinnen : Apprendre a programmer avec Python b) Mise en oeuvre concrete Nous construisons cette application simple sous la forme d'une classe. Sa seule utilite presente consiste a nous fournir un espace de noms commun dans lequel nous pouvons encapsuler nos variables et nos fonctions, ce qui nous permet de nous passer de variables globales. En effet : • Les variables auxquelles nous souhaitons pouvoir acceder de partout sont declarees comme des attributs d'instance (nous attachons chacune d'elles a l'instance a l'aide de self). • Les fonctions sont declarees comme des methodes, et done attachees elles aussi a self. Au niveau principal du programme, nous nous contentons d'instancier un objet de la classe ainsi construite (aucune methode de cet objet n'est activee de l'exterieur). 1. class Application: 2. def init (self): 3. " " "Constructeur de la fenetre principale" " " 4. self. root =Tk() 5. self . root .title (' Code des couleurs ' ) 6. self . dessineResistance () 7. Label (self .root, 8. text ="Entrez la valeur de la resistance, en ohms :") .grid(row =2) 9. Button (self .root, text ='Montrer', 10. command =self . changeCouleurs) . grid (row =3, sticky = W) 11. Button (self .root, text =' Quitter 1 , 12. command =self . root . quit) . grid (row =3, sticky = E) 13. self. entree = Entry (self . root , width =14) 14. self . entree . grid (row =3) 15 . # Code des couleurs pour les valeurs de zero a neuf : 16. self.ee = [ 'black ', 'brown ',' red' ,' orange ', 1 yellow' , 17 . ' green ' , ' blue ' , ' purple 1 , ' grey ' , ' white ' ] 18. self . root .mainloop () 19. 20. def dessineResistance (self ) : 21. """Canevas avec un modele de resistance a trois lignes colorees""" 22. self. can = Canvas (self .root, width=250, height =100, bg =' ivory 1 ) 23. self . can . grid (row =1, pady =5, padx =5) 24. self .can.create_line(10, 50, 240, 50, width =5) # fils 25. self .can.create_rectangle(65, 30, 185, 70, fill =' light grey', width =2) 26. # Dessin des trois lignes colorees (noires au depart) : 27. self.ligne =[] # on memorisera les trois lignes dans 1 liste 28. for x in range (85, 150, 24) : 29 . self . ligne . append (self . can . create_rectangle (x, 30, x+12 ,70, 30. ~ fill= ' black ' ,width=0) ) 31. 32. def changeCouleurs (self ) : 33. """Affichage des couleurs correspondant a la valeur entree""" 34. self.vlch = self . entree . get ( ) # la methode get() renvoie une chaine 35. try: 36. v = float (self . vlch) # conversion en valeur numerique 37 . except : 38 . err =1 # erreur : entree non numerique 39. else: 40. err =0 41. if err ==1 or v < 10 or v > lell : 42. self . signaleErreur ( ) # entree incorrecte ou hors limites 43. else: 44. li =[0]*3 # liste des 3 codes a afficher 45. logv = int (loglO (v) ) # partie entiere du logarithme 46. ordgr = 10**logv # ordre de grandeur 47 . # extraction du premier chif f re signif icatif : 48. li [0] = int(v/ordgr) # partie entiere 49. decim = v/ordgr - li[0] # partie decimale 50 . # extraction du second chif f re signif icatif : 51. li[l] = int (decim* 10 +.5) # +.5 pour arrondir correctement 52 . # nombre de zeros a accoler aux 2 chif f res signif icatif s : 53. li[2] = logv -1 54 . # Coloration des 3 lignes : 55. for n in range (3) : Gerard Swinnen : Apprendre a programmer avec Python 175. 56. 57. 58. 59. 60. 61. 62. 63. 64. 65. 66. 67. 68. 69. self . can . itemconf igure (self . ligne [n] , fill =self . cc [li [n] ] ) def signaleErreur (self ) : self . entree . configure (bg = ' red' ) self . root . after ( 1000 , self . videEntree) def videEntree (self ) : self . entree . configure (bg = 1 white ' ) self . entree . delete (0, len (self . vlch) ) # Programme principal from Tkinter import * from math import loglO f = Application () # colorer le fond du champ # apres 1 seconde, ef facer # retablir le fond blanc # enlever les car. presents # logarithmes en base 10 # instanciation de l'objet application Commentaires : • Ligne 1 : La classe est definie sans reference a une classe parente (pas de parentheses). II s'agira done d'une nouvelle classe independante. • Lignes 2 a 14 : Le constructeur de la classe instancie les widgets necessaires : pour ameliorer la lisibilite du programme, on a place l'instanciation du canevas (avec le dessin de la resistance) dans une methode separee dessineResistance(). Les boutons et le libelle ne sont pas memorises dans des variables, parce que Ton ne souhaite pas y faire reference ailleurs dans le programme. Le positionnement des widgets dans la fenetre utilise la methode grid(), decrite a la page 96. • Lignes 15-17 : Le code des couleurs est memorise dans une simple liste. • Ligne 18 : La derniere instruction du constructeur demarre l'application. • Lignes 20 a 30 : Le dessin de la resistance se compose d'une ligne et dun premier rectangle gris clair, pour le corps de la resistance et ses deux fils. Trois autres rectangles figureront les bandes colorees que le programme devra modifier en fonction des entrees de l'utilisateur. Ces bandes sont noires au depart ; elles sont referencees dans la liste self.ligne. • Lignes 32 a 53 : Ces lignes contiennent l'essentiel de la fonctionnalite du programme. L'entree brute fournie par l'utilisateur est acceptee sous la forme d'une chaine de caracteres. A la ligne 36, on essaie de convertir cette chaine en une valeur numerique de type float. Si la conversion echoue, on memorise l'erreur. Si Ton dispose bien d'une valeur numerique, on verifie ensuite qu'elle se situe effectivement dans l'intervalle autorise (de 10 Q. a 10 n £2). Si une erreur est detectee, on signale a l'utilisateur que son entree est incorrecte en colorant de rouge le fond du champ d' entree, qui est ensuite vide de son contenu (lignes 55 a 61). • Lignes 45-46 : Les mathematiques viennent a notre secours pour extraire de la valeur numerique son ordre de grandeur (e'est-a-dire l'exposant de 10 le plus proche). Veuillez consulter votre cours de mathematiques pour de plus amples explications concernant les logarithmes. • Lignes 47-48 : Une fois connu l'ordre de grandeur, il devient relativement facile d'extraire du nombre traite ses deux premiers chiffres significatifs. Exemple : Supposons que la valeur entree soit 31687. Le logarithme de ce nombre est 4,50088... dont la partie entiere (4) nous donne l'ordre de grandeur de la valeur entree (soit 10 4 ). Pour extraire de celle-ci son premier chiffre significatif, il suffit de la diviser par 10 4 , soit 10000, et de conserver seulement la partie entiere du resultat (3). 176. Gerard Swinnen : Apprendre a programmer avec Python • Lignes 49 a 51 : Le resultat de la division effectuee dans le paragraphe precedent est 3,1687. Nous recuperons la partie decimale de ce nombre a la ligne 49, soit 0,1687 dans notre exemple. Si nous le multiplions par dix, ce nouveau resultat comporte une partie entiere qui n'est rien d' autre que notre second chiffre significatif (1 dans notre exemple). Nous pourrions facilement extraire ce dernier chiffre, mais puisque c'est le dernier, nous souhaitons encore qu'il soit correctement arrondi. Pour ce faire, il suffit d'ajouter une demi unite au produit de la multiplication par dix, avant d'en extraire la valeur entiere. Dans notre exemple, en effet, ce calcul donnera done 1,687 + 0,5 =2,187 , dont la partie entiere (2) est bien la valeur arrondie recherchee. • Ligne 53 : Le nombre de zeros a accoler aux deux chiffres significatifs correspond au calcul de l'ordre de grandeur. II suffit de retirer une unite au logarithme. • Ligne 56 : Pour attribuer une nouvelle couleur a un objet deja dessine dans un canevas, on utilise la methode itemconfigure(). Nous utilisons done cette methode pour modifier l'option fill de chacune des bandes colorees, en utilisant les noms de couleur extraits de la liste self.ee grace a aux trois indices li[l], li[2] et li[3] qui contiennent les 3 chiffres correspondants. (13) Exercices : 13.1. Modifiez le script ci-dessus de telle maniere que le fond d'image devienne bleu clair (light blue'), que le corps de la resistance devienne beige ('beige'), que le fil de cette resistance soit plus fin, et que les bandes colorees indiquant la valeur soient plus larges. 13.2. Modifiez le script ci-dessus de telle maniere que l'image dessinee soit deux fois plus grande. 13.3. Modifiez le script ci-dessus de telle maniere qu'il devienne possible d'entrer aussi des valeurs de resistances comprises entre 1 et 10 £2. Pour ces valeurs, le premier anneau colore devra rester noir, les deux autres indiqueront la valeur en Q. et dixiemes d £1. 13.4. Modifiez le script ci-dessus de telle facon que le bouton « Montrer » ne soit plus necessaire. Dans votre script modifie, il suffira de frapper apres avoir entre la valeur de la resistance, pour que l'affichage s'active. 13.5. Modifiez le script ci-dessus de telle maniere que les trois bandes colorees redeviennent noires dans les cas ou l'utilisateur fournit une entree inacceptable. Gerard Swinnen : Apprendre a programmer avec Python 111. 13.2 « Petit train » : heritage, echange d'informations entre classes Dans l'exercice precedent, nous n'avons exploite qu'une seule caracteristique des classes : {'encapsulation. Celle-ci nous a permis d'ecrire un programme dans lequel les differentes fonctions (qui sont done devenues des methodes) peuvent chacune acceder a un meme pool de variables : toutes celles qui sont definies comme etant attachees a self. Toutes ces variables peuvent etre considerees en quelque sorte comme des variables globales a l'interieur de l'objet. Comprenez bien toutefois qu'il ne s'agit pas de veritables variables globales. Elles restent en effet strictement confinees a l'interieur de l'objet, et il est deconseille de vouloir y acceder de l'exterieur 50 . D'autre part, tous les objets que vous instancierez a partir d'une meme classe possederont chacun leur propre jeu de ces variables, qui sont done bel et bien encapsulees dans ces objets. On les appelle pour cette raison des attributs ({'instance. Nous allons a present passer a la vitesse superieure et realiser une petite application sur la base de plusieurs classes, afin d' examiner comment differents objets peuvent s'echanger des informations par I'intermediaire de leurs methodes. Nous allons egalement profiter de cet exercice pour vous montrer comment vous pouvez definir la classe principale de votre application graphique par derivation d'une classe Tkinter preexistante, mettant ainsi a profit le mecanisme 6! heritage. \ 1. . ■■ *fti —J oj xl BUM □□□ ©in oooooooo Train | Hello Le projet developpe ici tres simple, mais il pourrait constituer une premiere etape dans la realisation dun logiciel de jeu : nous en fournissons d'ailleurs des exemples plus loin (voir page 227). II s'agit d'une fenetre contenant un canevas et deux boutons. Lorsque Ton actionne le premier de ces deux boutons, un petit train apparait dans le canevas. Lorsque Ton actionne le second bouton, quelques petit* personnages apparaissent a certaines fenetres des wagons. a) Cahier des charges : L'application comportera deux classes : • La classe Application() sera obtenue par derivation d'une des classes de base de Tkinter : elle mettra en place la fenetre principale, son canevas et ses deux boutons. • Une classe Wagon(), independante, permettra d'instancier dans le canevas 4 objets-wagons similaires, dotes chacun d'une methode perso(). Celle-ci sera destinee a provoquer l'apparition d'un petit personnage a l'une quelconque des trois fenetres du wagon. L'application principale invoquera cette methode differemment pour differents objets-wagons, afin de faire apparaitre un choix de quelques personnages. 50 Comme nous l'avons deja signale precedemment, Python vous permet d'acceder aux attributs d'instance en utilisant la qualification des noms par points. D'autres langages de programmation l'interdisent, ou bien ne l'autorisent que moyennant une declaration particuliere de ces attributs (distinction entre attributs prives et publics). Sachez en tous cas que ce n'est pas recommande : le bon usage de la programmation orientee objet stipule en effet que vous ne devez pouvoir acceder aux attributs des objets que par I'intermediaire de methodes specifiques. 178. Gerard Swinnen : Apprendre a programmer avec Python b) Implementation : I . from Tkinter import * 2. 3. def cercle (can, x, y, r) : 4. "dessin d'un cercle de rayon en dans le canevas " 5. can . create_oval (x-r, y-r, x+r, y+r) 6. 7. class Application (Tk) : 8. def init (self): 9. Tk. init (self) # constructeur de la classe parente 10. self. can =Canvas (self , width =475, height =130, bg ="white") II. self .can. pack (side =TOP, padx =5, pady =5) 12. Button(self, text ="Train", command =self . dessine) . pack (side =LEFT) 13. Button(self, text ="Hello", command =self. coucou) .pack (side =LEFT) 14. 15. def dessine (self ) : 16. "instanciation de 4 wagons dans le canevas" 17. self.wl = Wagon (self . can, 10, 30) 18. self.w2 = Wagon (self. can, 130, 30) 19. self.w3 = Wagon (self . can, 250, 30) 20. self.w4 = Wagon (self. can, 370, 30) 21. 22. def coucou (self ) : 23. "apparition de personnages dans certaines fenetres" 24. self .wl .perso (3) # ler wagon, 3e fenetre 25. self .w3 .perso (1) # 3e wagon, le fenetre 26. self .w3 .perso (2) # 3e wagon, 2e fenetre 27. self . w4 . perso ( 1 ) # 4e wagon, le fenetre 28. 29. class Wagon: 30. def init (self, canev, x, y) : 31. "dessin d'un petit wagon en dans le canevas " 32. # memorisation des parametres dans des variables d' instance : 33. self. canev, self.x, self.y = canev, x, y 34 . # rectangle de base : 95x60 pixels : 35. canev. create_rectangle (x, y, x+95, y+60) 36. #3 fenetres de 25x40 pixels, ecartees de 5 pixels : 37. for xf in range(x+5, x+90, 30): 38. canev . create_rectangle (xf, y+5, xf+25, y+40) 39. #2 roues de rayon egal a 12 pixels : 40. cercle (canev, x+18, y+73, 12) 41. cercle (canev, x+77, y+73, 12) 42. 43. def perso (self, fen) : 44. "apparition d'un petit personnage a la fenetre " 45. # calcul des coordonnees du centre de chaque fenetre : 46. xf = self.x + fen*30 -12 47. yf = self.y + 25 48. cercle (self . canev, xf, yf, 10) # visage 49. cercle (self . canev, xf-5, yf-3, 2) # oeil gauche 50. cercle (self .canev, xf+5, yf-3, 2) # oeil droit 51. cercle (self . canev, xf, yf+5, 3) # bouche 52. 53. app = Application () 54. app.mainloopO Commentaires : • Lignes 3 a 5 : Nous projetons de dessiner une serie de petit* cercles. Cette petite fonction nous facilitera le travail en nous permettant de definir ces cercles a partir de leur centre et leur rayon. • Lignes 7 a 13 : La classe principale de notre application est construite par derivation de la classe de fenetres Tk() importee du module Tkinter. 51 Comme nous l'avons explique au chapitre 51 Nous verrons plus loin que Tkinter autorise egalement de construire la fenetre principale d'une application par Gerard Swinnen : Apprendre a programmer avec Python 179. precedent, le constructeur d'une classe derivee doit activer lui-meme le constructeur de la classe parente, en lui transmettant la reference de l'instance comme premier argument. Les lignes 10 a 13 servent a mettre en place le canevas et les boutons. • Lignes 15 a 20: Ces lignes instancient les 4 objets-wagons, produits a partir de la classe correspondante. Ceci pourrait etre programme plus elegamment a l'aide d'une boucle et d'une liste, mais nous le laissons ainsi afin de ne pas alourdir inutilement les explications qui suivent. Nous voulons placer nos objets-wagons dans le canevas, a des emplacements bien precis : il nous faut done transmettre quelques informations au constructeur de ces objets : au mo ins la reference du canevas, ainsi que les coordonnees souhaitees. Ces considerations nous font egalement entrevoir, que lorsque nous definirons la classe Wagon() un peu plus loin, nous devrons associer a sa methode constructeur un nombre egal de parametres pour receptionner ces arguments. • Lignes 22 a 27 : Cette methode est invoquee lorsque Ton actionne le second bouton. Elle invoque elle-meme la methode perso() de certains objets-wagons, avec des arguments differents, afin de faire apparaitre les personnages aux fenetres indiquees. Ces quelques lignes de code vous montrent done comment un objet peut communiquer avec un autre en faisant appel a l'une ou l'autre de ses methodes. II s'agit la du mecanisme central de la programmation par objets : Les objets sont des entites programmers qui s'echangent des messages et interagissent par Vintermediaire de leurs methodes. Idealement, la methode coucou() devrait comporter quelques instructions complementaires, lesquelles verifieraient d'abord si les objets-wagons concernes existent bel et bien, avant d'autoriser l'activation d'une de leurs methodes. Nous n'avons pas inclus ce genre de garde-fou afin que l'exemple reste aussi simple que possible, mais cela entraine la consequence que vous ne pouvez pas actionner le second bouton avant le premier. (Pouvez-vous ajouter un correctif ?) • Lignes 29-30 : La classe Wagon() ne derive d'aucune autre classe preexistante. Cependant, etant donne qu'il s'agit d'une classe d'objets graphiques, nous devons munir sa methode constructeur de parametres, afin de recevoir la reference du canevas auquel les dessins sont destines, ainsi que les coordonnees de depart de ces dessins. Dans vos experimentations eventuelles autour de cet exercice, vous pourriez bien evidemment ajouter encore d'autres parametres : taille du dessin, orientation, couleur, vitesse, etc. • Lignes 31 a 51 : Ces instructions ne necessitent guere de commentaires. La methode perso() est dotee d'un parametre qui indique celle des 3 fenetres ou il faut faire apparaitre un petit personnage. Ici aussi nous n'avons pas prevu de garde-fou : vous pouvez invoquer cette methode avec un argument egal a 4 ou 5, par exemple, ce qui produira des effets incorrects. • Lignes 53-54 : Pour demarrer l'application, il ne suffit pas d'instancier un objet de la classe Application() comme dans l'exemple de la rubrique precedente. II faut egalement invoquer la methode mainloop() qu'elle a herite de sa classe parente. Vous pourriez cependant condenser ces deux instructions en une seule, laquelle serait alors : Application ( ) . mainloop ( ) Exercice : 13.6. Perfectionnez le script decrit ci-dessus, en ajoutant un parametre couleur au constructeur de la classe Wagon(), lequel determinera la couleur de la cabine du wagon. Arrangez-vous egalement pour que les fenetres soient noires au depart, et les roues grises (pour realiser ce dernier objectif, ajoutez aussi un parametre couleur a la fonction cercle()). A cette meme classe Wagon(), ajoutez encore une methode allumer(), qui servira a derivation d'une classe de widget (le plus souvent, il s'agira d'un widget Frame()). La fenetre englobant ce widget sera automatiquement ajoutee. (Voir page 191). 180. Gerard Swinnen : Apprendre a programmer avec Python changer la couleur des 3 fenetres (initialement no ires) en jaune, afin de simuler l'allumage d'un eclairage interieur. Ajoutez un bouton a la fenetre principale, qui puisse declencher cet allumage. Profitez de l'amelioration de la fonction cercle() pour teinter le visage des petit* personnages en rose (pink), leurs yeux et leurs bouches en noir, et instanciez les objets-wagons avec des couleurs differentes. 13.3 « OscilloGraphe » : un widget personnalise Le projet qui suit va nous entrainer encore un petit peu plus loin. Nous allons y construire une nouvelle classe de widget, qu'il sera possible d'integrer dans nos projets futurs comme n'importe quel widget standard. Comme la classe principale de l'exercice precedent, cette nouvelle classe sera construite par derivation d'une classe Tkinter preexistante. Le sujet concret de cette application nous est inspire par le cours de physique. Pour rappel : Un mouvement vibratoire harmonique se definit comme etant la projection d'un mouvement circulaire uniforme sur une droite. Les positions successives d'un mobile qui effectue ce type de mouvement sont traditionnellement reperees par rapport a une position centrale : on les appelle alors des elongations. L'equation qui decrit revolution de V elongation d'un tel mobile au cours du temps est toujour s de la forme e = Asm(2nft + cp) , dans laquelle e represente Velongation du mobile a tout instant t . Les constantes A, f et (p designent respectivement I'amplitude, la frequence et la phase du mouvement vibratoire. Le but du present projet est de fournir un instrument de visualisation simple de ces differents concepts, a savoir un systeme d'affichage automatique de graphiques elongation/temps. L'utilisateur pourra choisir librement les valeurs des parametres A, f et cp , et observer les courbes qui en resultent. Le widget que nous allons construire d'abord s'occupera de l'affichage proprement dit. Nous construirons ensuite d'autres widgets pour faciliter l'entree des parametres A, f et cp . Veuillez done encoder le script ci-dessous et le sauvegarder dans un fichier, auquel vous donnerez le nom oscillo.py . Vous realiserez ainsi un veritable module contenant une classe (vous pourrez par la suite ajouter d'autres classes dans ce meme module, si le coeur vous en dit). Gerard Swinnen : Apprendre a programmer avec Python 181. 1 . from Tkinter import * 2. from math import sin, pi 3. 4. class OscilloGraphe (Canvas) : 5. "Canevas specialise, pour dessiner des courbes elongation/temps" 6. def init (self, boss =None, larg=200, haut=150) : 7. "Constructeur du graphique : axes et echelle horiz." 8 . # construction du widget parent : 9. Canvas. init (self) # appel au constructeur 10. self . configure (width=larg, height=haut) # de la classe parente 11. self.larg, self.haut = larg, haut # memorisation 12 . # trace des axes de reference : 13. self . create_line (10, haut/2, larg, haut/2, arrow=LAST) # axe X 14. self . create_line (10, haut-5, 10, 5, arrow=LAST) # axe Y 15. # trace d'une echelle avec 8 graduations : 16. pas = (larg-25)/8. # intervalles de 1' echelle horizontale 17. for t in range(l, 9): 18. stx = 10 + t*pas # +10 pour partir de l'origine 19. self .create_line (stx, haut/2-4, stx, haut/2+4) 20. 21. def traceCourbe (self , freq=l, phase=0, ampl=10, coul='red'): 22. "trace d'un graphique elongation/temps sur 1 seconde" 23. curve =[] # liste des coordonnees 24. pas = (self . larg-25) /1000 . # 1' echelle X correspond a 1 seconde 25. for t in range (0 , 1001 , 5) : # que 1 ' on divise en 1000 ms . 26. e = ampl*sin (2*pi*freq*t/1000 - phase) 27. x = 10 + t*pas 28. y = self. haut/2 - e*self . haut/25 29. curve . append ( (x, y) ) 30. n = self . create_line (curve, fill=coul, smooth=l) 31. return n # n = numero d'ordre du trace 32. 33. #### Code pour tester la classe : #### 34. 35. if name == 1 main ' : 36. root = Tk() 37. gra = OscilloGraphe (root , 250, 180) 38. gra. pack () 39. gra . configure (bg ='ivory', bd =2, relief = SUNKEN) 40. gra. traceCourbe (2, 1.2, 10, 'purple') 41. root .mainloop () Le niveau principal du script est constitute par les lignes 35 a 41. Comme nous l'avons deja explique a la page 172, les lignes de code situees apres l'instruction if name == ' main ': ne sont pas executees si le script est importe en tant que module. Si on lance le script comme application principale, par contre, ces instructions sont executees. Nous disposons ainsi d'un mecanisme interessant, qui nous permet d'integrer des instructions de test a l'interieur des modules, meme si ceux-ci sont destines a etre importes dans d'autres scripts. Lancez done l'execution du script de la maniere habituelle. Vous devriez obtenir un affichage similaire a celui qui est reproduit a la page precedente. Experimentation : Nous commenterons les lignes importantes du script un peu plus loin dans ce texte. Mais commencons d'abord par experimenter quelque peu la classe que nous venons de construire. 182. Gerard Swinnen : Apprendre a programmer avec Python Ouvrez une fenetre de terminal (« Python shell »), et entrez les instructions ci-dessous directement a la ligne de commande : >>> from oscillo import * »> gl = OscilloGraphe () »> gl.pack() Apres importation des classes du module oscillo, nous instancions un premier objet gl , de la classe OscilloGraphe(). Puisque nous ne fournissons aucun argument, l'objet possede les dimensions par defaut, definies dans le constructeur de la classe. Remarquons au passage que nous n'avons meme pas pris la peine de definir d'abord une fenetre maitre pour y placer ensuite notre widget. Tkinter nous pardonne cet oubli et nous en fournit une automatiquement ! »> g2 = OscilloGraphe (haut=2 00, larg=250) »> g2.pack() »> g2 .traceCourbe () Par ces instructions, nous creons un second widget de la meme classe, en precisant cette fois ses dimensions (hauteur et largeur, dans n'importe quel ordre). Ensuite, nous activons la methode traceCourbe() associee a ce widget. Etant donne que nous ne lui fournissons aucun argument, la sinusoide qui apparait correspond aux valeurs prevues par defaut pour les parametres A, f et cp . »> g3 = OscilloGraphe (larg=220) »> g3 . configure (bg= 1 white ' , bd=3 , relief =SUNKEN) >>> g3 .pack (padx=5, pady=5) »> g3 . traceCourbe (phase=l . 57 , coul= 1 purple ' ) >>> g3 . traceCourbe (phase=3 . 14 , coul= ' dark green') Pour comprendre la configuration de ce troisieme widget, il faut nous rappeler que la classe OscilloGraphe() a ete construite par derivation de la classe Canvas(). Elle herite done de toutes les proprietes de celle-ci, ce qui nous permet de choisir la couleur de fond, la bordure, etc., en utilisant les memes arguments que ceux qui sont a notre disposition lorsque nous configurons un canevas. Nous faisons ensuite apparaitre deux traces successifs, en faisant appel deux fois a la methode traceCourbe(), a laquelle nous fournissons des arguments pour la phase et la couleur. Exercice : 13.7. Creez un quatrieme widget, de taille 400 x 300, couleur de fond jaune, et faites-y apparaitre plusieurs courbes correspondant a des frequences et des amplitudes differentes. Gerard Swinnen : Apprendre a programmer avec Python 183. II est temps a present que nous analysions la structure de la classe qui nous a permis d'instancier tous ces widgets. Nous avons enregistre cette classe dans le module oscillo.py (voir page 182). a) Cahier des charges : Nous souhaitons definir une nouvelle classe de widget, capable d'afficher automatiquement les graphiques elongation/temps correspondant a divers mouvements vibratoires harmoniques. Ce widget doit pouvoir etre dimensionne a volonte au moment de son instanciation. II fait apparaitre deux axes cartesiens X et Y munis de fleches. L'axe X represente l'ecoulement du temps pendant une seconde au total, et il est muni d'une echelle comportant 8 intervalles. Une methode traceCourbe() est associee a ce widget. Elle provoque le trace du graphique elongation/temps pour un mouvement vibratoire dont on fournit la frequence (entre 0.25 et 10 Hz), la phase (entre 0 et 2 71 radians) et l'amplitude (entre 1 et 10 ; echelle arbitraire). b) Implementation : • Ligne 4 : La classe OscilloGraphe() est creee par derivation de la classe Canvas(). Elle herite done toutes les proprietes de celle-ci : on pourra configurer les objets de cette nouvelle classe en utilisant les nombreuses options deja disponibles pour la classe Canvas(). • Ligne 6 : La methode « constructeur » utilise 3 parametres, qui sont tous optionnels puisque chacun d' entre eux possede une valeur par defaut. Le parametre boss ne sert qu'a receptionner la reference d'une fenetre maitresse eventuelle (voir exemples suivants). Les parametres larg et haut (largeur et hauteur) servent a assigner des valeurs aux options width et height du canevas parent, au moment de l'instanciation. • Lignes 9 & 10 : La premiere operation que doit accomplir le constructeur d'une classe derivee, e'est activer le constructeur de sa classe parente. En effet : nous ne pouvons heriter toute la fonctionnalite de la classe parente, que si cette fonctionnalite a ete effectivement mise en place. Nous activons done le constructeur de la classe Canvas() a la ligne 9 , et nous ajustons deux de ses options a la ligne 10. Notez au passage que nous pourrions condenser ces deux lignes en une seule, qui deviendrait en l'occurrence : Canvas. init (self, width=larg, height=haut) Rappel : comme cela a ete explique a la page 169, nous devons transmettre a ce constructeur la reference de l'instance presente (self) comme premier argument. • Ligne 11 : II est necessaire de memoriser les parametres larg et haut dans des variables d'instance, parce que nous devrons pouvoir y acceder aussi dans la methode traceCourbe(). • Lignes 13 & 14 : Pour tracer les axes X et Y, nous utilisons les parametres larg et haut, ainsi ces axes sont automatiquement mis a dimension. L'option arrow=LAST permet de faire apparaitre une petite fleche a l'extremite de chaque ligne. • Lignes 16 a 19 : Pour tracer l'echelle horizontale, on commence par reduire de 25 pixels la largeur disponible, de maniere a menager des espaces aux deux extremites. On divise ensuite en 8 intervalles, que Ton visualise sous la forme de 8 petit* traits verticaux. • Ligne 21 : La methode traceCourbe() pourra etre invoquee avec quatre arguments. Chacun d'entre eux pourra eventuellement etre omis, puisque chacun des parametres correspondants possede une valeur par defaut. II sera egalement possible de fournir les arguments dans n'importe quel ordre, comme nous l'avons deja explique a la page 78. 184. Gerard Swinnen : Apprendre a programmer avec Python • Lignes 23 a 3 1 : Pour le trace de la courbe, la variable t prend successivement toutes les valeurs de 0 a 1000, et on calcule a chaque fois l'elongation e correspondante, a l'aide de la formule theorique (ligne 26). Les couples de valeurs t & e ainsi trouvees sont mises a l'echelle et transformees en coordonnees x, y aux lignes 27 & 28, puis accumulees dans la liste curve. • Lignes 30 & 31 : La methode create_line() trace alors la courbe correspondante en une seule operation, et elle renvoie le numero d'ordre du nouvel objet ainsi instancie dans le canevas (ce numero d'ordre nous permettra d'y acceder encore par apres : pour l'effacer, par exemple). L'option smooth =1 ameliore l'aspect final, par lissage. Exercices : 13.8. Modifiez le script de maniere a ce que l'axe de reference vertical comporte lui aussi une echelle, avec 5 tirets de part et d'autre de l'origine. 13.9. Comme les widgets de la classe Canvas() dont il derive, votre widget peut integrer des indications textuelles. II suffit pour cela d'utiliser la methode create_text(). Cette methode attend au moins trois arguments : les coordonnees x et y de l'emplacement ou vous voulez faire apparaitre votre texte, et puis le texte lui-meme, bien entendu. D'autres arguments peuvent etre transmis sous forme d'options, pour preciser par exemple la police de caracteres et sa taille. Afin de voir comment cela fonctionne, ajoutez provisoirement la ligne suivante dans le constructeur de la classe OscilloGraphe(), puis relancez le script : self. create text (130, 30, text = "Essai", anchor =CENTER) Utilisez cette methode pour ajouter au widget les indications suivantes aux extremites des axes de reference : e (pour « elongation ») le long de l'axe vertical, et t (pour « temps ») le long de l'axe horizontal. Le resultat pourrait ressembler a ceci (figure de gauche) : 13.10. Vous pouvez completer encore votre widget, en y faisant apparaitre une grille de reference, plutot que de simples tirets le long des axes. Pour eviter que cette grille ne soit trop visible, vous pouvez colorer ses traits en gris (option fill = 'grey'), comme dans la figure de droite. 13.11. Completez encore votre widget en y faisant apparaitre des reperes numeriques. Gerard Swinnen : Apprendre a programmer avec Python 185. 13.4 « Curseurs » : un widget composite Dans l'exercice precedent, vous avez construit un nouveau type de widget que vous avez sauvegarde dans le module oscillo.py. Conservez soigneusem*nt ce module, car vous l'integrerez bientot dans un projet plus complexe. Pour l'instant, vous allez construire encore un autre widget, plus interactif cette fois. II s'agira d'une sorte de panneau de controle comportant trois curseurs de reglage et une case a cocher. Comme le precedent, ce widget est destine a etre reutilise dans une application de synthese. 13.4.1 Presentation du widget « Scale » tk ■ _n|x| Reglage : I JJ •25 0 25 50 75 100 125 Valeur actuelle = 0 Commencons d'abord par decouvrir un widget de base, que nous n'avions pas encore utilise jusqu'ici : Le widget Scale se presente comme un curseur qui coulisse devant une echelle. II permet a l'utilisateur de choisir rapidement la valeur d'un parametre quelconque, d'une maniere tres attrayante. Le petit script ci-dessous vous montre comment le parametrer et l'utiliser dans une fenetre : from Tkinter import * def updateLabel (x) : lab . configure (text= 'Valeur actuelle = ' + str(x)) root = Tk() Scale (root, length=250, orient=HORIZONTAL, label =' Reglage :', troughcolor ='dark grey', sliderlength =20, showvalue =0, from_=-25, to=125, tickinterval =25, command=updateLabel ) . pack ( ) lab = Label (root) lab . pack ( ) root . mainloop ( ) Ces lignes ne necessitent guere de commentaires. Vous pouvez creer des widgets Scale de n'importe quelle taille (option length), en orientation horizontale (comme dans notre exemple) ou verticale (option orient = VERTICAL). Les options from_ (attention : n'oubliez pas le caractere 'souligne' !) et to definissent la plage de reglage. L'intervalle entre les reperes numeriques est defini dans l'option tickinterval, etc. La fonction designee dans l'option command est appelee automatiquement chaque fois que le curseur est deplace, et la position actuelle du curseur par rapport a l'echelle lui est transmise en argument. II est done tres facile d'utiliser cette valeur pour effectuer un traitement quelconque. Considerez par exemple le parametre x de la fonction updateLabel(), dans notre exemple. Le widget Scale constitue une interface tres intuitive et attrayante pour proposer differents reglages aux utilisateurs de vos programmes. Nous allons a present l'incorporer en plusieurs exemplaires dans une nouvelle classe de widget : un panneau de controle destine a choisir la frequence, la phase et l'amplitude pour un mouvement vibratoire, dont nous afficherons ensuite le graphique elongation/temps a l'aide du widget oscilloGraphe construit dans les pages precedentes. 186. Gerard Swinnen : Apprendre a programmer avec Python 13.4.2 Construction d'un panneau de controle a trois curseurs Comme le precedent, le script que nous decrivons ci-dessous est destine a etre sauvegarde dans un module, que vous nommerez cette fois curseurs.py. Les classes que vous sauvegardez ainsi seront reutilisees (par importation) dans une application de synthese que nous decrirons un peu plus loin 52 . Nous attirons votre attention sur le fait que le code ci-dessous peut etre raccourci de differentes manieres (Nous y reviendrons). Nous ne l'avons pas optimise d'emblee, parce que cela necessiterait d'y incorporer un concept supplemental (les expressions lambda), ce que nous preferons eviter pour l'instant. Vous savez deja que les lignes de code placees a la fin du script permettent de tester son fonctionnement. Vous devriez obtenir une fenetre semblable a celle-ci : tk |_n|x| Frequence (Hz) : Phase (degres) : Amplitude : W Afficher | _|J I II I JJ 1.0 3.0 5.0 7.0 9.0 -180 -90 0 90 180 1 3 5 7 9 1 -3.0-3.14159265359-4.0 I . from Tkinter import * 2 . from math import pi 3. 4 . class ChoixVibra (Frame) : 5. """Curseurs pour choisir frequence, phase & amplitude d'une vibration""" 6. def init (self, boss =None, coul ='red'): 7. Frame. init (self) # constructeur de la classe parente 8. # Initialisation de quelques attributs d' instance 9. self.freq, self. phase, self.ampl, self. coul =0, 0, 0, coul 10. # Variable d'etat de la case a cocher : II. self.chk = IntVar() # ' ob jet -variable ' Tkinter 12. Checkbutton (self , text= 'Afficher 1 , variable=self . chk, 13. fg = self. coul, command = self . setCurve) .pack (side=LEFT) 14 . # Definition des 3 widgets curseurs 15. Scale (self, length=150, orient=HORIZONTAL, sliderlength =25, 16. label ='Frequence (Hz) :', f rom_=l . , to=9., tickinterval =2, 17. resolution =0.25, 18. showvalue =0, command = self . setFrequency) . pack (side=LEFT) 19. Scale (self, length=150, orient=HORIZONTAL, sliderlength =15, 20. label ='Phase (degres) :', from_=-180, to=180, tickinterval =90, 21. showvalue =0, command = self . setPhase) . pack (side=LEFT) 22. Scale (self, length=150, orient=HORIZONTAL, sliderlength =25, 23. label ='Amplitude :', from_=l, to=9, tickinterval =2, 24. showvalue =0, command = self . setAmplitude) . pack (side=LEFT) 25. 26. def setCurve (self ) : 27 . self . event_generate ( ' ' ) 28. 29. def setFrequency (self , f ) : 30. self.freq = float (f) 31 . self . event_generate ( ' ' ) 32. 33. def setPhase (self , p) : 34 . pp =f loat (p) 35. self. phase = pp*2*pi/360 # conversion degres -> radians 36 . self . event_generate ( ' ' ) 37. 38. def setAmplitude (self , a): 39. self.ampl = float (a) 40 . self . event_generate ( ' ' ) 41. 52 Vous pourriez bien evidemment aussi enregistrer plusieurs classes dans un meme module. Gerard Swinnen : Apprendre a programmer avec Python 187. 42. #### Code pour tester la classe : ### 43. 44. if name == ' main ' : 45. def afficherTout (event =None) : 46. lab . configure (text = ' %s - %s - %s - %s ' % 47. (fra.chk.get () , fra.freq, fra. phase, fra.ampl)) 48. root = Tk() 49. fra = ChoixVibra (root , 'navy') 50. fra. pack (side =TOP) 51. lab = Label (root, text ='test') 52. lab. pack () 53. root .bind (' ' , afficherTout) 54. root .mainloop () Ce panneau de controle permettra a vos utilisateurs de regler aisem*nt la valeur des parametres indiques (frequence, phase & amplitude), lesquels pourront alors servir a commander l'affichage de graphiques elongation/temps dans un widget de la classe OscilloGraphe() construite precedemment, comme nous le montrerons dans l'application de synthese. Commentaires : • Ligne 6 : La methode « constructeur » utilise un parametre optionnel coul. Ce parametre permettra de choisir une couleur pour le graphique soumis au controle du widget. Le parametre boss sert a receptionner la reference d'une fenetre maitresse eventuelle (voir plus loin). • Ligne 7 : Activation du constructeur de la classe parente (pour heriter sa fonctionnalite). • Ligne 9 : Declaration de quelques variables d'instance. Leurs vraies valeurs seront determinees par les methodes des lignes 29 a 40 (gestionnaires d'evenements). • Ligne 11 : Cette instruction instancie un objet de la classe IntVar(), laquelle fait partie du module Tkinter au meme titre que les classes similaires DoubleVar(), StringVar() et Boolean Var(). Toutes ces classes permettent de definir des « variables Tkinter », lesquels sont en fait des objets, mais qui se se comportent comme des variables a l'interieur des widgets Tkinter. Ainsi l'objet reference dans self.chk contient l'equivalent d'une variable de type entier, dans un format utilisable par Tkinter. Pour acceder a sa valeur depuis Python, il faut utiliser des methodes specifiques de cette classe d' objets : la methode set() permet de lui assigner une valeur, et la methode get() permet de la recuperer (ce que Ton met en pratique a la ligne 47). • Ligne 12 : L'option variable de l'objet checkbutton est associee a la variable Tkinter definie a la ligne precedente. (Nous ne pouvons pas referencer directement une variable ordinaire dans la definition d'un widget Tkinter, parce que Tkinter lui-meme est ecrit dans un langage qui n'utilise pas les memes conventions que Python pour formater ses variables. Les objets construits a partir des classes de variables Tkinter sont done necessaires pour assurer l'interface). • Ligne 13 : L'option command designe la methode que le systeme doit invoquer lorsque l'utilisateur effectue un clic de souris dans la case a cocher. • Lignes 14 a 24 : Ces lignes definissent les trois widgets curseurs, en trois instructions similaires. II serait plus elegant de programmer tout ceci en une seule instruction, repetee trois fois a l'aide d'une boucle. Cela necessiterait cependant de faire appel a un concept que nous n'avons pas encore explique (les fonctions/expressions lamdba), et la definition du gestionnaire d'evenements associe a ces widgets deviendrait elle aussi plus complexe. Conservons done pour cette fois des instructions separees : nous nous efforcerons d'ameliorer tout cela plus tard. 188. Gerard Swinnen : Apprendre a programmer avec Python • Lignes 26 a 40 : Les 4 widgets definis dans les lignes precedentes possedent chacun une option command. Pour chacun d'eux, la methode invoquee dans cette option command est differente : la case a cocher active la methode setCurve(), le premier curseur active la methode setFrequency(), le second curseur active la methode setPhase(), et le troisieme curseur active la methode setAmplitude(). Remarquez bien au passage que l'option command des widgets Scale transmet un argument a la methode associee (la position actuelle du curseur), alors que la meme option command ne transmet rien dans le cas du widget Checkbutton. Ces 4 methodes (qui sont done les gestionnaires des evenements produits par la case a cocher et les trois curseurs) provoquent elles-memes chacune remission d'un nouvel evenement 53 , en faisant appel a la methode event_generate(). Lorsque cette methode est invoquee, Python envoie au systeme d'exploitation exactement le meme message-evenement que celui qui se produirait si l'utilisateur enfoncait simultanement les touches , et de son clavier. Nous produisons ainsi un message-evenement bien particulier, qui peut etre detecte et traite par un gestionnaire d'evenement associe a un autre widget (voir page suivante). De cette maniere, nous mettons en place un veritable systeme de communication entre widgets : chaque fois que l'utilisateur exerce une action sur notre panneau de controle, celui-ci genere un evenement specifique, qui signale cette action a l'attention des autres widgets presents. Note : nous aurions pu choisir une autre combinaison de touches (ou meme carrement un autre type d'evenement). Notre choix s'est porte sur celle-ci parce qu'il y a vraiment tres peu de chances que l'utilisateur s'en serve alors qu'il examine notre programme. Nous pourrons cependant produire nous-memes un tel evenement au clavier a titre de test, lorsque le moment sera venu de verifier le gestionnaire de cet evenement, que nous mettrons en place par ailleurs. • Lignes 42 a 54 : Comme nous l'avions deja fait pour oscillo.py, nous completons ce nouveau module par quelques lignes de code au niveau principal. Ces lignes permettent de tester le bon fonctionnement de la classe : elles ne s'executent que si on lance le module directement, comme une application a part entiere. Veillez a utiliser vous-meme cette technique dans vos propres modules, car elle constitue une bonne pratique de programmation : l'utilisateur de modules construits ainsi peut en effet (re)decouvrir tres aisem*nt leur fonctionnalite (en les executant) et la maniere de s'en servir (en analysant ces quelques lignes de code). Dans ces lignes de test, nous construisons une fenetre principale root qui contient deux widgets : un widget de la nouvelle classe ChoixVibra() et un widget de la classe Label(). A la ligne 53, nous associons a la fenetre principale un gestionnaire d'evenement : tout evenement du type specifie declenche desormais un appel de la fonction afficherTout(). Cette fonction est done notre gestionnaire d'evenement specialise, qui est sollicite chaque fois qu'un evenement de type est detecte par le systeme d'exploitation. Comme nous l'avons deja explique plus haut, nous avons fait en sorte que de tels evenements soient produits par les objets de la classe ChoixVibra(), chaque fois que l'utilisateur modifie l'etat de l'un ou l'autre des trois curseurs, ou celui de la case a cocher. 53 En fait, on devrait plutot appeler cela un message (qui est lui-meme la notification d'un evenement). Veuillez relire a ce sujet les explications de la page 85 : Programmes pilotes par des evenements. Gerard Swinnen : Apprendre a programmer avec Python 189. Concue seulement pour effectuer un test, la fonction afficherTout() ne fait rien d'autre que provoquer l'affichage des valeurs des variables associees a chacun de nos quatre widgets, en (re) configurant l'option text dun widget de classe Label(). • Ligne 47, expression fra.chk.get() : nous avons vu plus haut que la variable memorisant l'etat de la case a cocher est un objet-variable Tkinter. Python ne peut pas lire directement le contenu d'une telle variable, qui est en realite un objet-interface. Pour en extraire la valeur, il faut done faire usage d'une methode specifique de cette classe d'objets : la methode get(). Propagation des evenements Le mecanisme de communication decrit ci-dessus respecte la hierarchie de classes des widgets. Vous aurez note que la methode qui declenche l'evenement est associee au widget dont nous sommes en train de definir la classe, par l'intermediaire de self. En general, un message-evenement est en effet associe a un widget particulier (par exemple, un clic de souris sur un bouton est associe a ce bouton), ce qui signifie que le systeme d'exploitation va d'abord examiner s'il existe un gestionnaire pour ce type d'evenement, qui soit lui aussi associe a ce widget. S'il en existe un, e'est celui-la qui est active, et la propagation du message s'arrete. Sinon, le message-evenement est « presente » successivement aux widgets maitres, dans l'ordre hierarchique, jusqu'a ce qu'un gestionnaire d'evenement soit trouve, ou bien jusqu'a ce que la fenetre principale soit atteinte. Les evenements correspondant a des frappes sur le clavier (telle la combinaison de touches utilisee dans notre exercice) sont cependant toujours expedies directement a la fenetre principale de l'application. Dans notre exemple, le gestionnaire de cet evenement doit done etre associe a la fenetre root. Exercices : 13.12. Votre nouveau widget herite des proprietes de la classe Frame(). Vous pouvez done modifier son aspect en modifiant les options par defaut de cette classe, a l'aide de la methode configure(). Essayez par exemple de faire en sorte que le panneau de controle soit entoure d'une bordure de 4 pixels ayant l'aspect dun sillon (bd = 4, relief = GROOVE). Si vous ne comprenez pas bien ce qu'il faut faire, inspirez-vous du script oscillo.py (ligne 10). 13.13. Si Ton assigne la valeur 1 a l'option showvalue des widgets Scale(), la position precise du curseur par rapport a l'echelle est affichee en permanence. Activez done cette fonctionnalite pour le curseur qui controle le parametre « phase ». 13.14. L'option troughcolor des widgets Scale() permet de definir la couleur de leur glissiere. Utilisez cette option pour faire en sorte que la couleur des glissieres des 3 curseurs soit celle qui est utilisee comme parametre lors de l'instanciation de votre nouveau widget. 13.15. Modifiez le script de telle maniere que les widgets curseurs soient ecartes davantage les uns des autres (options padx et pady de la methode pack()). 190. Gerard Swinnen : Apprendre a programmer avec Python 13.5 Integration de widgets composites dans une application synthese Dans les exercices precedents, nous avons construit deux nouvelles classes de widgets : le widget OscilloGraphe(), canevas specialise pour le dessin de sinusoides, et le widget ChoixVibra(), panneau de controle a trois curseurs permettant de choisir les parametres d'une vibration. Ces widgets sont desormais disponibles dans les modules oscillo.py et curseurs.py 54 Nous allons a present les utiliser dans une application synthese, qui pourrait illustrer votre cours de physique : un widget OscilloGrapheO y affiche un, deux, ou trois graphiques superposes, de couleurs differentes, chacun d'entre eux etant soumis au controle d'un widget ChoixVibraQ : Mouvements vibratoires haimoniques Frequence (Hz) : Phase (degres) : Amplitude : |7 Afficher | Jj I u 1.0 3.0 5.0 7.0 9.0 Frequence (Hz) : •180 -90 0 90 180 Phase (degres) : 1 3 5 7 9 Amplitude : |7 Afficher |JJ I II 1.0 3.0 5.0 7.0 9.0 Frequence (Hz) : -180 -90 0 90 180 Phase (degres) : 1 3 5 7 9 Amplitude : F Afficher | JJ I u 1.0 3.0 5.0 7.0 9.0 -180 -90 0 90 180 1 3 5 7 9 Le script correspondant est reproduit ci-apres. Nous attirons votre attention sur la technique mise en ceuvre pour provoquer un rafraichissem*nt de l'affichage dans le canevas par l'intermediaire d'un evenement, chaque fois que l'utilisateur effectue une action quelconque au niveau de l'un des panneaux de controle. Rappelez-vous que les applications destinees a fonctionner dans une interface graphique doivent etre concues comme des « programmes pilotes par les evenements » (voir page 85). 54 II va de soi que nous pourrions rassembler toutes les classes que nous construisons dans un seul module. Gerard Swinnen : Apprendre a programmer avec Python 191. En preparant cet exemple, nous avons arbitrairement decide que l'affichage des graphiques serait declenche par un evenement particulier, tout a fait similaire a ceux que genere le systeme d'exploitation lorsque l'utilisateur accomplit une action quelconque. Dans la gamme (tres etendue) d'evenements possibles, nous en avons choisi un qui ne risque guere d'etre utilise pour d'autres raisons, pendant que notre application fonctionne : la combinaison de touches . Lorsque nous avons construit la classe de widgets ChoixVibra(), nous y avons done incorpore les instructions necessaires pour que de tels evenements soient generes, chaque fois que l'utilisateur actionne l'un des curseurs ou modifie l'etat de la case a cocher. Nous allons a present definir le gestionnaire de cet evenement et l'inclure dans notre nouvelle classe : nous l'appellerons montreCourbes() et il se chargera de rafraichir l'affichage. Etant donne que l'evenement concerne est du type , nous devrons cependant le detecter au niveau de la fenetre principale de l'application. 1 . from oscillo import * 2 . from curseurs import * 3. 4. class ShowVibra (Frame) : 5. """Demonstration de mouvements vibratoires harmoniques" " " 6. def init (self, boss =None) : 7. Frame. init (self) # constructeur de la classe parente 8. self.couleur = ['dark green', 'red', 'purple'] 9. self. trace = [0]*3 # liste des traces (courbes a dessiner) 10. self . controle = [0]*3 # liste des panneaux de controle 11. 12 . # Instanciation du canevas avec axes X et Y : 13. self.gra = OscilloGraphe (self , larg =400, haut=200) 14. self .gra. configure (bg =' white', bd=2, relief =SOLID) 15. self .gra. pack (side =TOP, pady=5) 16. 17. # Instanciation de 3 panneaux de controle (curseurs) : 18. for i in range (3) : 19. self . controle [i] = ChoixVibra (self , self . couleur [i] ) 20. self . controle [i] .pack () 21. 22. # Designation de l'evenement qui declenche l'affichage des traces : 23. self .master .bind (' ' , self .montreCourbes) 24. self .master .title ( 'Mouvements vibratoires harmoniques') 25. self. pack () 26. 27. def montreCourbes (self , event): 28. """ (Re) Affichage des trois graphiques elongation/temps""" 29. for i in range (3) : 30. 31. # D'abord, ef facer le trace precedent (eventuel) : 32 . self . gra . delete (self . trace [i] ) 33. 34. # Ensuite, dessiner le nouveau trace : 35. if self .controle [i] .chk. get () : 36. self . trace [i] = self .gra. traceCourbe ( 37. coul = self . couleur [i] , 38. freq = self . controle [i] . freq, 39. phase = self . controle [i] .phase, 40. ampl = self . controle [i] . ampl) 41. 42. #### Code pour tester la classe : ### 43. 44. if name == ' main ' : 45. ShowVibra () .mainloop ( ) 192. Gerard Swinnen : Apprendre a programmer avec Python Commentaires : • Lignes 1-2 : Nous pouvons nous passer d' importer le module Tkinter : chacun de ces deux modules s'en charge deja. • Ligne 4 : Puisque nous commencons a connaitre les bonnes techniques, nous decidons de construire l'application elle-meme sous la forme d'une classe, derivee de la classe Frame() : ainsi nous pourrons plus tard l'integrer toute entiere dans d'autres projets, si le coeur nous en dit. • Lignes 8-10 : Definition de quelques variables d'instance (3 listes) : les trois courbes tracees seront des objets graphiques, dont les couleurs sont pre-definies dans la liste self.couleur ; nous devons preparer egalement une liste self.trace pour memoriser les references de ces objets graphiques, et enfin une liste self.controle pour memoriser les references des trois panneaux de controle. • Lignes 13 a 15 : Instanciation du widget d'affichage. Etant donne que la classe OscilloGraphe() a ete obtenue par derivation de la classe Canvas(), il est toujours possible de configurer ce widget en redefinissant les options specifiques de cette classe (ligne 13). • Lignes 18 a 20 : Pour instancier les trois widgets « panneau de controle », on utilise une boucle. Leurs references sont memorisees dans la liste self.controle preparee a la ligne 10. Ces panneaux de controle sont instancies comme esclaves du present widget, par l'intermediaire du parametre self. Un second parametre leur transmet la couleur du trace a controler. • Lignes 23-24 : Au moment de son instanciation, chaque widget Tkinter recoit automatiquement un attribut master qui contient la reference de la fenetre principale de l'application. Cet attribut se revele particulierement utile si la fenetre principale a ete instanciee implicitement par Tkinter, comme c'est le cas ici. Rappelons en effet que lorsque nous demarrons une application en instanciant directement un widget tel que Frame, par exemple (c'est ce que nous avons fait a la ligne 4), Tkinter instancie automatiquement une fenetre maitresse pour ce widget (un objet de la classe Tk()). Comme cet objet a ete cree automatiquement, nous ne disposons d'aucune reference dans notre code pour y acceder, si ce n'est par l'intermediaire de l'attribut master que Tkinter associe automatiquement a chaque widget. Nous nous servons de cette reference pour redefinir le bandeau-titre de la fenetre principale (a la ligne 24), et pour y attacher un gestionnaire d'evenement (a la ligne 23). • Lignes 27 a 40 : La methode decrite ici est le gestionnaire des evenements specifiquement declenches par nos widgets ChoixVibra() (ou « panneaux de controle »), chaque fois que l'utilisateur exerce une action sur un curseur ou une case a cocher. Dans tous les cas, les graphiques eventuellement presents sont d'abord effaces (ligne 28) a l'aide de la methode delete 0 : le widget OscilloGraphe() a herite cette methode de sa classe parente CanvasO- Ensuite, de nouvelles courbes sont retracees, pour chacun des panneaux de controle dont on a coche la case « Afficher ». Chacun des objets ainsi dessines dans le canevas possede un numero de reference, renvoye par la methode traceCourbe() de notre widget OscilloGraphe(). Les numeros de reference de nos dessins sont memorises dans la liste self.trace. lis permettent d'effacer individuellement chacun d'entre eux (cfr. instruction de la ligne 28). • Lignes 38-40 : Les valeurs de frequence, phase & amplitude que Ton transmet a la methode traceCourbe() sont les attributs d'instance correspondants de chacun des trois panneaux de controle, eux-memes memorises dans la liste self.controle. Nous pouvons recuperer ces attributs en utilisant la qualification des noms par points. Gerard Swinnen : Apprendre a programmer avec Python 193. Exercices : 13.16. Modifiez le script, de maniere a obtenir l'aspect ci-dessous (ecran d'affichage avec grille de reference, panneaux de controle entoures d'un sillon) : Mouvements vibiatoires haimoniques BBS vm m rm m BBBB Frequence (Hz) : Phase (degres) : Amplitude : r |JJ I JJ I JJ 1.0 3.0 5.0 7.0 9.0 •180 -90 0 90 180 2 4 6 8 10 Frequence (Hz) : Phase (degres) : Amplitude : r I _u I JJ 1.0 3.0 5.0 7.0 9.0 ■180 -90 0 90 180 2 4 6 8 10 Frequence (Hz) : Phase (degres) : Amplitude : r I JJ I JJ LU 1.0 3.0 5.0 7.0 9.0 •180 -90 0 90 180 2 4 G 8 10 13.17. Modifiez le script, de maniere a faire apparaitre et controler 4 graphiques au lieu de trois. Pour la couleur du quatrieme graphique, choisissez par exemple : 'blue', 'navy', 'maroon', ... 13.18. Aux lignes 33-35, nous recuperons les valeurs des frequence, phase & amplitude choisies par l'utilisateur sur chacun des trois panneaux de controle, en accedant directement aux attributs d' instance correspondants. Python autorise ce raccourci - et c'est bien pratique - mais cette technique est dangereuse. Elle enfreint l'une des recommandations de la theorie generale de la « programmation orientee objet », qui preconise que I'acces aux proprietes des objets soit toujours pris en charge par des methodes specifiques. Pour respecter cette recommandation, ajoutez a la classe ChoixVibra() une methode supplemental que vous appellerez valeurs(), et qui renverra un tuple contenant les valeurs de la frequence, la phase et l'amplitude choisies. Les lignes 33 a 35 du present script pourront alors etre remplacees par quelque chose comme : freq, phase, ampl = self . control [i] . valeurs () 13.19. Ecrivez une petite application qui fait apparaitre une fenetre avec un canevas et un widget curseur (Scale). Dans le canevas, dessinez un cercle, dont l'utilisateur pourra faire varier la taille a l'aide du curseur. 13.20. Ecrivez un script qui creera deux classes : une classe "Application", derivee de Frame(), dont le constructeur instanciera un canevas de 400x400 pixels, ainsi que deux boutons. Dans le canevas, vous instancierez un objet de la classe "Visage" decrite ci-apres. 194. Gerard Swinnen : Apprendre a programmer avec Python La classe "Visage" servira a definir des objets graphiques censes representer des visages humains simplifies. Ces visages seront constitutes d'un cercle principal dans lequel trois ovales plus petit* representeront deux yeux et une bouche (ouverte). Une methode "fermer" permettra de remplacer l'ovale de la bouche par une ligne horizontale. Une methode "ouvrir" permettra de restituer la bouche de forme ovale. Les deux boutons definis dans la classe "Application" serviront respectivement a fermer et a ouvrir la bouche de l'objet Visage installe dans le canevas. (Vous pouvez vous inspirer de l'exemple de la page 90 pour composer une partie du code). 13.21. Exercice de synthese : elaboration d'un dictionnaire de couleurs. But : realiser un petit programme utilitaire, qui puisse vous aider a construire facilement et rapidement un nouveau dictionnaire de couleurs, lequel permettrait faeces technique a une couleur quelconque par fintermediaire de son nom usuel en francais. Contexte : En manipulant divers objets colores avec Tkinter, vous avez constate que cette bibliotheque graphique accepte qu'on lui designe les couleurs les plus fondamentales sous la forme de chaines de caracteres contenant leur nom en anglais : 'red', 'blue', etc. Vous savez cependant qu'un ordinateur ne peut traiter que des informations numerisees. Cela implique que la designation d'une couleur quelconque doit necessairement tot ou tard etre encodee sous la forme d'un nombre. II faut bien entendu adopter pour cela une une convention, et celle-ci peut varier d'un systeme a un autre. L'une de ces conventions, parmi les plus courantes, consiste a representer une couleur a l'aide de trois octets, qui indiqueront respectivement les intensites des trois composantes rouge, verte et bleue de cette couleur. Cette convention peut etre utilisee avec Tkinter pour acceder a n'importe quelle nuance coloree. Vous pouvez en effet lui indiquer la couleur d'un element graphique quelconque, a l'aide d'une chaine de 7 caracteres telle que '#00FA4E". Dans cette chaine, le premier caractere (#) signifie que ce qui suit est une valeur hexadecimale. Les six caracteres suivants represented les 3 valeurs hexadecimales des 3 composantes R, V & B. Pour visualiser concretement la correspondance entre une couleur quelconque et son code, vous pouvez essayer le petit programme utilitaire tkColorChooser.py (qui se trouve generalement dans le sous-repertoire /lib-tk de votre installation de Python). Etant donne qu'il n'est pas facile pour les humains que nous sommes de memoriser de tels codes hexadecimaux, Tkinter est egalement dote d'un dictionnaire de conversion, qui autorise l'utilisation de noms communs pour un certain nombre de couleurs parmi les plus courantes, mais cela ne marche que pour des noms de couleurs exprimes en anglais. Le but du present exercice est de realiser un logiciel qui facilitera la construction d'un dictionnaire equivalent en francais, lequel pourrait ensuite etre incorpore a l'un ou l'autre de vos propres programmes. Une fois construit, ce dictionnaire serait done de la forme : {'vert':'#00FF00', 'bleu':'#0000FF', ... etc ...}. Cahier des charges : L'application a realiser sera une application graphique, construite autour d'une classe. Elle comportera une fenetre avec un certain nombre de champs d' entree et de boutons, afin que l'utilisateur puisse aisem*nt encoder de nouvelles couleurs en indiquant a chaque fois son nom francais dans un champ, et son code hexadecimal dans un autre. Gerard Swinnen : Apprendre a programmer avec Python 195. Lorsque le dictionnaire contiendra deja un certain nombre de donnees, il devra etre possible de le tester, c'est-a-dire d'entrer un nom de couleur en francais et de retrouver le code hexadecimal correspondant a l'aide d'un bouton (avec affichage eventuel d'une zone coloree). Un bouton provoquera l'enregistrement du dictionnaire dans un fichier texte. Un autre permettra de reconstruire le dictionnaire a partir du fichier. 13.22. Le script ci-dessous correspond a une ebauche de projet dessinant des ensembles de des a jouer disposes a l'ecran de plusieurs manieres differentes (cette ebauche pourrait etre une premiere etape dans la realisation d'un logiciel de jeu). L'exercice consistera a analyser ce script et a le completer. Vous vous placerez ainsi dans la situation d'un programmeur charge de continuer le travail commence par quelqu'un d'autre, ou encore dans celle de l'informaticien prie de participer a un travail d'equipe. A) Commencez par analyser ce script, et ajoutez-y des commentaires, en particulier aux lignes marquees : #* ** , afin de montrer que vous comprenez ce que doit faire le programme a ces emplacements : from Tkinter import * class FaceDom: def init (self, can, val, pos, taille =70) : self. can =can # *** x, y, c = pos[0], pos[l], taille/2 can . create_rectangle (x -c, y-c, x+c, y+c, fill ='ivory', width =2) d = taille/3 # *** self.pList =[] # *** pDispo = [((0,0),), ((-d,d), (d,-d)), ((-d,-d), (0,0), (d,d))] disp = pDispo [val -1] # *** for p in disp: self .cercle(x +p[0], y +p[l], 5, 'red') def cercle(self, x, y, r, coul) : # *** self .pList . append (self . can . create_oval (x-r, y-r, x+r, y+r, fill=coul) ) def ef facer (self ) : # *** for p in self.pList: self . can . delete (p) class Projet (Frame) : def init (self, larg, haut) : Frame . init (self) self. larg, self. haut = larg, haut self. can = Canvas (self, bg='dark green', width =larg, height =haut) self . can. pack (padx =5, pady =5) # *** bList = [("A", self.boutA), ("B", self.boutB), ("C", self.boutC), ("D", self.boutD), ("Quitter", self .boutQuit) ] for b in bList: 196. Gerard Swinnen : Apprendre a programmer avec Python Button(self, text =b[0], command =b [1] ) .pack (side =LEFT) self .pack () def boutA(self) : self.d3 = FaceDom (self .can, 3, (100,100), 50) def boutB(self) : self.d2 = FaceDom (self .can, 2, (200,100), 80) def boutC(self) : self.dl = FaceDom (self .can, 1, (350,100), 110) def boutD(self) : # *** self . d3 . ef facer ( ) def boutQuit (self ) : self . master . destroy ( ) Projet(500, 300) .mainloop () B) Modifiez ensuite ce script, afin qu'il corresponde au cahier des charges suivant : • Le canevas devra etre plus grand : 600 x 600 pixels. • Les boutons de commande devront etre deplaces a droite et espaces davantage. • La taille des points sur une face de de devra varier proportionnellement a la taille de cette face Variante 1 : Ne conservez que les 2 boutons A et B. Chaque utilisation du bouton A fera apparaitre 3 nouveaux des (de meme taille, plutot petit*) disposes sur une colonne (verticale), les valeurs de ces des etant tirees au hasard entre 1 et 6. Chaque nouvelle colonne sera disposee a la droite de la precedente. Si l'un des tirages de 3 des correspond a 4, 2, 1 (dans n'importe quel ordre), un message « gagne » sera affiche dans la fenetre (ou dans le canevas). Le bouton B provoquera l'effacement complet (pas seulement les points !) de tous les des affiches. Variante 2 : Ne conservez que les 2 boutons A et B. Le bouton A fera apparaitre 5 des disposes en quinconce (c.a.d. comme les points dune face de valeur 5). Les valeurs de ces des seront tirees au hasard entre 1 et 6, mais il ne pourra pas y avoir de doublons. Le bouton B provoquera l'effacement complet (pas seulement les points !) de tous les des affiches. Variante 3 : Ne conservez que les 3 boutons A, B et C. Le bouton A fera apparaitre 13 des de meme taille disposes en cercle. Chaque utilisation du bouton B provoquera un changement de valeur du premier de, puis du deuxieme, du troisieme, etc. La nouvelle valeur d'un de sera a chaque fois egale a sa valeur precedente augmentee d'une unite, sauf dans le cas ou la valeur precedente etait 6 : dans ce cas la nouvelle valeur est 1, et ainsi de suite. Le bouton C provoquera l'effacement complet (pas seulement les points !) de tous les des affiches. Variante 4 : Ne conservez que les 3 boutons A, B et C. Le bouton A fera apparaitre 12 des de meme taille disposes sur deux lignes de 6. Les valeurs des des de la premiere ligne seront dans l'ordre 1, 2, 3, 4, 5, 6. Les valeurs des des de la seconde ligne seront tirees au hasard entre 1 et 6. Chaque utilisation du bouton B provoquera un changement de valeur aleatoire du premier de de la seconde ligne, tant que cette valeur restera differente de celle du de correspondant dans la premiere ligne. Lorsque le l er de de la 2 e ligne aura acquis la valeur de son correspondant, c'est la valeur du 2 e de de la seconde ligne qui sera changee au hasard, et ainsi de suite, jusqu'a ce que les 6 faces du bas soient identiques a celles du haut. Le bouton C provoquera l'effacement complet (pas seulement les points !) de tous les des affiches. Gerard Swinnen : Apprendre a programmer avec Python 197. Chapitre 14 : Et pour quelques widgets de plus . Les pages qui suivent contiennent des indications et et des exemples complementaires qui pourront vous etre utiles pour le developpement de vos projets personnels. II ne s'agit evidemment pas d'une documentation de reference complete sur Tkinter. Pour en savoir plus, vous devrez tot ou tard consulter des ouvrages specialises, comme par exemple l'excellent Python and Tkinter programming de John E. Grayson, dont vous trouverez la reference complete a la page 8. 14.1 Les « boutons radio » Les widgets « boutons radio » permettent de proposer a l'utilisateur un ensemble de choix mutuellement exclusifs. On les appelle ainsi par analogie avec les boutons de selection que Ton trouvait jadis sur les postes de radio. Ces boutons etaient concus de telle maniere qu'un seul a la fois pouvait etre enfonce : tous les autres ressortaient automatiquement. La caracteristique essentielle de ces widgets est qu'on les utilise toujours par groupes. Tous les boutons radio faisant partie d'un meme groupe sont associes a une seule et meme variable Tkinter, mais chacun d'entre eux se voit aussi attribuer une valeur particuliere. Lorsque l'utilisateur selectionne l'un des boutons, la valeur correspondant a ce bouton est affectee a la variable Tkinter commune. La programmation, c'est genial f Normal *~ Gras (* Italique <~ Gras/ltalique I . from Tkinter import * 2. 3. class RadioDemo (Frame) : 4. """Demo : utilisation de widgets 'boutons radio'""" 5. def init (self, boss =None) : 6. """Creation d'un champ d' entree avec 4 boutons radio""" 7. Frame. init (self) 8. self. pack () 9. # Champ d' entree contenant un petit texte : 10. self. texte = Entry (self, width =30, font ="Arial 14") II. self . texte . insert (END, "La programmation, c'est genial") 12. self .texte. pack (padx =8, pady =8) 13. # Nom francais et nom technique des quatre styles de police : 14. stylePoliceFr =[ "Normal", "Gras", "Italique", "Gras/ltalique"] 15. stylePoliceTk =[ "normal", "bold", "italic" , "bold italic"] 16. # Le style actuel est memorise dans un ' ob jet-variable ' Tkinter 17. self . choixPolice = StringVar() 18 . self . choixPolice . set (stylePoliceTk [0] ) 19. # Creation des quatre 'boutons radio' : 20. for n in range (4) : 21. bout = Radiobutton (self , 22. text = stylePoliceFr [n] , 23. variable = self . choixPolice, 24. value = stylePoliceTk [n] , 25. command = self . changePolice) 26. bout .pack (side =LEFT, padx =5) 27. 28. def changePolice (self ) : 29. """Remplacement du style de la police actuelle""" 30. police = "Arial 15 " + self . choixPolice . get ( ) 31. self .texte . configure (font =police) 32. 33. if name == ' main ' : 34. RadioDemo () .mainloop ( ) 198. Gerard Swinnen : Apprendre a programmer avec Python Commentaires : • Ligne 3 : Cette fois encore, nous preferons construire notre petite application comme une classe derivee de la classe Frame(), ce qui nous permettrait eventuellement de l'integrer sans difficulty dans une application plus importante. • Ligne 8 : En general, on applique les methodes de positionnement des widgets (pack(), grid(), ou place()) apres instanciation de ceux-ci, ce qui permet de choisir librement leur disposition a l'interieur des fenetres maitresses. Comme nous le montrons ici, il est cependant tout a fait possible de deja prevoir ce positionnement dans le constructeur du widget. • Ligne 1 1 : Les widgets de la classe Entry disposent de plusieurs methodes pour acceder a la chaine de caracteres affichee. La methode get() permet de recuperer la chaine entiere. La methode delete() permet d'en effacer tout ou partie (cfr. projet « Code des couleurs », page 174). La methode insert() permet d'inserer de nouveaux caracteres a un emplacement quelconque (c'est-a-dire au debut, a la fin, ou meme a l'interieur d'une chaine preexistante eventuelle). Cette methode s'utilise done avec deux arguments, le premier indiquant l'emplacement de l'insertion (utilisez 0 pour inserer au debut, END pour inserer a la fin, ou encore un indice numerique quelconque pour designer un caractere dans la chaine). • Lignes 14-15 : Pluto t que de les instancier dans des instructions separees, nous preferons creer nos quatre boutons a l'aide d'une boucle. Les options specifiques a chacun d'eux sont d'abord preparees dans les deux listes stylePoliceFr et stylePoliceTk : la premiere contient les petit* textes qui devront s'afficher en regard de chaque bouton, et la seconde les valeurs qui devront leur etre associees. • Lignes 17-18 : Comme explique a la page precedente, les quatre boutons forment un groupe autour d'une variable commune. Cette variable prendra la valeur associee au bouton radio que l'utilisateur decidera de choisir. Nous ne pouvons cependant pas utiliser une variable ordinaire pour remplir ce role, parce que les attributs internes des objets Tkinter ne sont accessibles qu'au travers de methodes specifiques. Une fois de plus, nous utilisons done ici un objet-variable Tkinter, de type 'chaine de caracteres', que nous instancions a partir de la classe StringVar()., et auquel nous donnons une valeur par defaut a la ligne 18. • Lignes 20 a 26 : Instanciation des quatre boutons radio. Chacun d'entre eux se voit attribuer une etiquette et une valeur differentes, mais tous sont associes a la meme variable Tkinter commune (self.choixPolice). Tous invoquent egalement la meme methode self.changePolice(), chaque fois que l'utilisateur effectue un clic de souris sur l'un ou l'autre. • Lignes 28 a 31 : Le changement de police s'obtient par re-configuration de l'option font du widget Entry. Cette option attend un tuple contenant le nom de la police, sa taille, et eventuellement son style. Si le nom de la police ne contient pas d'espaces, le tuple peut aussi etre remplace par une chaine de caracteres. Exemples : ('Arial', 12, 'italic') ('Helvetica', 10) ('Times New Roman', 12, 'bold italic') "Verdana 14 bold" "President 18 italic" Voir egalement les exemples de la page 221. Gerard Swinnen : Apprendre a programmer avec Python 199. 14.2 Utilisation des cadres (frames) pour la composition d'une fenetre Vous avez deja abondamment utilise la classe de widgets FrameO (« cadre », en francais), notamment pour creer de nouveaux widgets complexes par derivation. Le petit script ci-dessous vous montre l'utilite de cette meme classe pour regrouper des ensembles de widgets et les disposer d'une maniere determinee dans une fenetre. II vous demontre egalement l'utilisation de certaines options decoratives (bordures, relief, etc.). Pour composer la fenetre ci-contre, nous avons utilise deux cadres fl et f2, de maniere a realiser deux groupes de widgets bien distincts, l'un a gauche et l'autre a droite. Nous avons colore ces deux cadres pour bien les mettre en evidence, mais ce n'est evidemment pas indispensable. Le cadre fl contient lui-meme 6 autres cadres, qui contiennent chacun un widget de la classe Label(). Le cadre f2 contient un widget Canvas() et un widget Button(). Les couleurs et garnitures sont de simples options. I . from Tkinter import * 2. 3. fen = Tk() 4. fen. title ("Fenetre composee a 1 ' aide de frames") 5. fen. geometry ("300x300") 6. 7. fl = Frame (fen, bg = '#80c0c0') 8. fl. pack (side =LEFT, padx =5) 9. 10. fint = [0]*6 II. for (n, col, rel, txt) in [(0, 'grey50', RAISED, 'Relief sortant ' ) , 12. (1, 'grey60', SUNKEN, 'Relief rentrant ' ) , 13. (2, 'grey70', FLAT, 'Pas de relief), 14. (3, 'grey80', RIDGE, 'Crete'), 15. (4, 'grey90', GROOVE, 'Sillon'), 16. (5, 'greylOO', SOLID, 'Bordure')]: 17. fint[n] = Frame (fl, bd =2, relief =rel) 18. e = Label (fint [n] , text =txt, width =15, bg =col) 19. e. pack (side =LEFT, padx =5, pady =5) 20. fint [n] .pack (side =TOP, padx =10, pady =5) 21. 22. f2 = Frame (fen, bg ='#d0d0b0', bd =2, relief =GROOVE) 23. f 2. pack (side =RIGHT, padx =5) 24. 25. can = Canvas(f2, width =80, height =80, bg ='white', bd =2, relief =SOLID) 26. can. pack (padx =15, pady =15) 27. bou =Button(f2, text= ' Bouton ' ) 28. bou. pack () 29. 30. fen.mainloop () Fenetre composee a I'aide de frames [ Relief sortant Relief rentrant Pas de relief Bordure 200. Gerard Swinnen : Apprendre a programmer avec Python • Lignes 3 a 5 : Afin de simplifier au maximum la demonstration, nous ne programmons pas cet exemple comme une nouvelle classe. Remarquez a la ligne 5 l'utilite de la methode geometry() pour fixer les dimensions de la fenetre principale. • Ligne 7 : Instanciation du cadre de gauche. La couleur de fond (une variete de bleu cyan) est determined par l'argument bg {background). Cette chaine de caracteres contient en notation hexadecimale la description des trois composantes rouge, verte et bleue de la teinte que Ton souhaite obtenir : Apres le caractere # signalant que ce qui suit est une valeur numerique hexadecimale, on trouve trois groupes de deux symboles alphanumeriques. Chacun de ces groupes represente un nombre compris entre 1 et 255. Ainsi 80 correspond a 128, et cO correspond a 192 en notation decimale. Dans notre exemple, les composantes rouge, verte et bleue de la teinte a representer valent done respectivement 128, 192 & 192. En application de cette technique descriptive, le noir serait obtenu avec #000000, le blanc avec #ffffff, le rouge pur avec #ff0000, un bleu sombre avec #000050, etc. • Ligne 8 : Puisque nous lui appliquons la methode pack(), le cadre sera automatiquement dimensionne par son contenu. L'option side =LEFT le positionnera a gauche dans sa fenetre maitresse. L'option padx =5 menagera un espace de 5 pixels a sa gauche et a sa droite (nous pouvons traduire « padx » par « espacement horizontal »). • Ligne 10 : Dans le cadre fl que nous venons de preparer, nous avons l'intention de regrouper 6 autres cadres similaires contenant chacun une etiquette. Le code correspondant sera plus simple et plus efficient si nous instancions ces widgets dans une liste plutot que dans des variables independantes. Nous preparons done cette liste avec 6 elements que nous remplacerons plus loin. • Lignes 11 a 16 : Pour construire nos 6 cadres similaires, nous allons parcourir une liste de 6 tuples contenant les caracteristiques particulieres de chaque cadre. Chacun de ces tuples est constitue de 4 elements : un indice, une constante Tkinter definissant un type de relief, et deux chaines de caracteres decrivant respectivement la couleur et le texte de l'etiquette. La boucle for effectue 6 iterations pour parcourir les 6 elements de la liste. A chaque iteration, le contenu d'un des tuples est affecte aux variables n, col, rel et txt (et ensuite les instructions des lignes 17 a 20 sont executees). Le parcours dune liste de tuples a l'aide d'une boucle for constitue done une construction particulierement compacte, qui permet de realiser de nombreuses affectations avec un tres petit nombre destructions. • Ligne 17 : Les 6 cadres sont instancies comme des elements de la liste fint. Chacun d' entre eux est agremente d'une bordure decorative de 2 pixels de large, avec un certain effet de relief. • Lignes 18-20 : Les etiquettes ont toutes la meme taille, mais leurs textes et leurs couleurs de fond different. Du fait de l'utilisation de la methode pack(), e'est la dimension des etiquettes qui determine la taille des petit* cadres. Ceux-ci a leur tour determinent la taille du cadre qui les regroupe (le cadre fl). Les options padx et pady permettent de reserver un petit espace autour de chaque etiquette, et un autre autour de chaque petit cadre. L'option side =TOP positionne les 6 petit* cadres les uns en dessous des autres dans le cadre conteneur fl. • Lignes 22-23 : Preparation du cadre f2 (cadre de droite). Sa couleur sera une variete de jaune, et nous l'entourerons d'une bordure decorative ayant l'aspect d'un sillon. • Lignes 25 a 28 : Le cadre f2 contiendra un canevas et un bouton. Notez encore une fois l'utilisation des options padx et pady pour menager des espaces autour des widgets (Considerez par exemple le cas du bouton, pour lequel cette option n'a pas ete utilisee : de ce fait, il entre en contact avec la bordure du cadre qui l'entoure). Comme nous l'avons fait pour les cadres, nous avons place une bordure autour du canevas. Sachez que d'autres widgets acceptent egalement ce genre de decoration : boutons, champs d'entree, etc. Gerard Swinnen : Apprendre a programmer avec Python 201. 14.3 Comment deplacer des dessins a I'aide de la souris Le widget canevas est l'un des points forts de la bibliotheque graphique Tkinter. II integre en effet un grand nombre de dispositifs tres efficaces pour manipuler des dessins. Le script ci-apres est destine a vous montrer quelques techniques de base. Si vous voulez en savoir plus, notamment en ce qui concerne la manipulation de dessins composes de plusieurs parties, veuillez consulter l'un ou l'autre ouvrage de reference traitant de Tkinter. Au demarrage de notre petite application, une serie de dessins sont traces au hasard dans un canevas (il s'agit en l'occurrence de simples ellipses colorees). Vous pouvez deplacer n'importe lequel de ces dessins en les « saisissant » a I'aide de votre souris. Lorsqu'un dessin est deplace, il passe a l'avant-plan par rapport aux autres, et sa bordure apparait plus epaisse pendant toute la duree de sa manipulation. Pour bien comprendre la technique utilisee, vous devez vous rappeler qu'un logiciel utilisant une interface graphique est un logiciel « pilote par les evenements » (revoyez au besoin les explications de la page 85). Dans cette application, nous allons mettre en place un mecanisme qui reagit aux evenements : « enfoncement du bouton gauche de la souris », « deplacement de la souris, le bouton gauche restant enfonce », « relachement du bouton gauche ». Ces evenements sont generes par le systeme d'exploitation et pris en charge par l'interface Tkinter. Notre travail de programmation consistera done simplement a les associer a des gestionnaires differents (fonctions ou methodes). . □ x 202. Gerard Swinnen : Apprendre a programmer avec Python # Exemple montrant comment faire en sorte que les objets dessines dans un # canevas puissent etre manipules a 1 ' aide de la souris from Tkinter import * from random import randrange class Draw (Frame): "classe definissant la fenetre principale du programme" def init (self) : Frame . init (self) # mise en place du canevas - dessin de 15 ellipses colorees : self.c = Canvas (self, width =400, height =300, bg =' ivory') self . c .pack (padx =5, pady =3) for i in range (15): # tirage d'une couleur au hasard : coul = [ ' brown ' , ' red ' , ' orange ' , ' yellow ' , ' green ' , ' cyan ' , ' blue ' , ' violet ' , ' purple ' ] [randrange ( 9) ] # trace d'une ellipse avec coordonnees aleatoires : xl, yl = randrange (300) , randrange (200) x2, y2 = xl + randrange (10, 150), yl + randrange (10 , 150) self . c . create_oval (xl, yl, x2, y2, fill =coul) # liaison d'evenements au widget : self . c . bind ( "" , self .mouseDown) self . c . bind ( "" , self .mouseMove) self . c . bind ( " " , self . mouseUp) # mise en place d'un bouton de sortie : b_fin = Button(self, text ='Terminer', bg ='royal blue', fg ='white', font =(' Helvetica ' , 10, 'bold'), command =self.quit) b_f in .pack (pady =2) self .pack () def mouseDown (self , event) : "Op. a effectuer quand le bouton gauche de la souris est enfonce" self . currOb ject =None # event. x et event. y contiennent les coordonnees du clic effectue : self.xl, self.yl = event. x, event. y # renvoie la reference du dessin le plus proche : self . selOb ject = self . c . f ind_closest (self .xl, self.yl) # modification de l'epaisseur du contour du dessin : self . c . itemconfig (self . selOb ject , width =3) # fait passer le dessin a 1 ' avant-plan : self . c . lift (self . selOb ject) def mouseMove (self , event): "Op. a effectuer quand la souris se deplace, bouton gauche enfonce" x2, y2 = event. x, event. y dx, dy = x2 -self.xl, y2 -self.yl if self . selOb ject : self . c. move (self . selOb ject , dx, dy) self.xl, self.yl = x2, y2 def mouseUp (self , event): "Op. a effectuer quand le bouton gauche de la souris est relache" if self . selOb ject : self . c. itemconfig (self . selOb ject, width =1) self . selOb ject =None if name == ' main ' : Draw ( ) . mainloop() Gerard Swinnen : Apprendre a programmer avec Python 203. Commentaires : Le script contient essentiellement la definition d'une classe graphique derivee de Frame(). Comme c'est souvent le cas pour les programmes exploitant les classes d'objets, le corps principal du script se resume a une seule instruction composee, dans laquelle on realise deux operations consecutives : instanciation d'un objet de la classe definie precedemment, et activation de sa methode mainloop() ( laquelle demarre l'observateur d'evenements). Le constructeur de la classe Draw() presente une structure qui doit vous etre devenue familiere, a savoir : appel au constructeur de la classe parente, puis mise en place de divers widgets. Dans le widget canevas, nous instancions 15 dessins sans nous preoccuper de conserver leurs references dans des variables. Nous pouvons proceder ainsi parce que Tkinter conserve lui-meme une reference interne pour chacun de ces objets. (Si vous travaillez avec d'autres bibliotheques graphiques, vous devrez probablement prevoir une memorisation de ces references). Les dessins sont de simples ellipses colorees. Leur couleur est choisie au hasard dans une liste de 9 possibilites, l'indice de la couleur choisie etant determine par la fonction randrange() importee du module random. Le mecanisme d'interaction est installe ensuite : on associe les trois identificateurs d'evenements , et concernant le widget canevas, aux noms des trois methodes choisies comme gestionnaires d'evenements. Lorsque l'utilisateur enfonce le bouton gauche de sa souris, la methode mouseDown() est done activee, et le systeme d'exploitation lui transmet en argument un objet event, dont les attributs x et y contiennent les coordonnees du curseur souris dans le canevas, determinees au moment du clic. Nous memorisons directement ces coordonnees dans les variables d' instance self.xl et self.x2, car nous en aurons besoin par ailleurs. Ensuite, nous utilisons la methode find_closest() du widget canevas, qui nous renvoie la reference du dessin le plus proche. (Note : cette methode bien pratique renvoie toujours une reference, meme si le clic de souris n'a pas ete effectue a l'interieur du dessin). Le reste est facile : la reference du dessin selectionne est memorisee dans une variable d'instance, et nous pouvons faire appel a d'autres methodes du widget canevas pour modifier ses caracteristiques. En l'occurrence, nous utilisons les methodes itemconfig() et lift() pour epaissir son contour et le faire passer a l'avant-plan. Le « transport » du dessin est assure par la methode mouseMove(), invoquee a chaque fois que la souris se deplace alors que son bouton gauche est reste enfonce. L'objet event contient cette fois encore les coordonnees du curseur souris, au terme de ce deplacement. Nous nous en servons pour calculer les differences entre ces nouvelles coordonnees et les precedentes, afin de pouvoir les transmettre a la methode move() du widget canevas, qui effectuera le transport proprement dit. Nous ne pouvons cependant faire appel a cette methode que s'il existe effectivement un objet selectionne, et il nous faut veiller egalement a memoriser les nouvelles coordonnees acquises. La methode mouseUp() termine le travail. Lorsque le dessin transports est arrive a destination, il reste a annuler la selection et rendre au contour son epaisseur initiale. Ceci ne peut etre envisage que s'il existe effectivement une selection, bien entendu. 204. Gerard Swinnen : Apprendre a programmer avec Python 14.4 Python Mega Widgets Les modules Pmw constituent une extension interessante de Tkinter. Entierement ecrits en Python, ils contiennent toute une bibliotheque de widgets composites, construits a partir des classes de base de Tkinter. Dotes de fonctionnalites tres etendues, ces widgets peuvent se reveler fort precieux pour le developpement rapide d'applications complexes. Si vous souhaitez les utiliser, sachez cependant que les modules Pmw ne font pas partie de l'installation standard de Python : vous devrez done toujours verifier leur presence sur les machines cibles de vos programmes. II existe un grand nombre de ces mega-widgets. Nous n'en presenterons ici que quelques-uns parmi les plus utiles. Vous pouvez rapidement vous faire une idee plus complete de leurs multiples possibilites, en essayant les scripts de demonstration qui les accompagnent (lancez par exemple le script all.py , situe dans le repertoire .../Pmw/demos). 14.4.1 « Combo Box » Les mega-widgets s'utilisent aisem*nt. La petite application ci-apres vous montre comment mettre en ceuvre un widget de type ComboBox (boite de liste combinee a un champ d'entree). Nous l'avons configure de la maniere la plus habituelle (avec une boite de liste deroulante). mm Test cadet blue Lorsque l'utilisateur de notre petit programme choisit une couleur dans la liste deroulante (il peut aussi entrer un nom de couleur directement dans le champ d'entree), cette couleur devient _ automatiquement la couleur de fond pour la fenetre maitresse. Dans cette fenetre maitresse, nous avons ajoute un libelle et un bouton, afin de vous montrer comment vous pouvez acceder a la selection operee precedemment dans le ComboBox lui-meme (le bouton provoque l'affichage du nom de la derniere couleur choisie). I . from Tkinter import * 2 . import Pmw 3. 4. def changeCoul (col) : 5. fen . configure (background = col) 6. 7. def changeLabel () : 8 . lab . configure (text = combo . get ( ) ) 9. 10. couleur s = ('navy', 'royal blue', ' steelbluel ' , 'cadet blue', II. 'lawn green', 'forest green', 'dark red', 12. 'grey80 ' , 'grey60 ' , 'grey40', 'grey20') 13. 14. fen = Pmw . initialise ( ) 15. bou = Button (fen, text ="Test", command =changeLabel) 16. bou. grid (row =1, column =0, padx =8, pady =6) 17. lab = Label (fen, text ='neant', bg =' ivory') 18. lab. grid (row =1, column =1, padx =8) 19. 20. combo = Pmw. ComboBox (fen, labelpos = NW, 21. label_text = 'Choisissez la couleur : ', 22. scrolledlist_items = couleurs, 23. listheight = 150, 24. selectioncommand = changeCoul) 25. combo . grid (row =2, columnspan =2, padx =10, pady =10) 26. 27. f en . mainloop ( ) Choisissez la couleur : grey60 steelbluel cadet blue lawn green forest green dark red grey80 qrev'60 grey40 are'.'20 Gerard Swinnen : Apprendre a programmer avec Python 205. Commentaires : • Lignes 1 & 2 : On commence par importer les composants habituels de Tkinter, ainsi que le module Pmw. • Ligne 14 : Pour creer la fenetre maitresse, il faut utiliser de preference la methode Pmw.initialise(), plutot que d'instancier directement un objet de la classe Tk(). Cette methode veille en effet a mettre en place tout ce qui est necessaire afin que les widgets esclaves de cette fenetre puissent etre detruits correctement lorsque la fenetre elle-meme sera detruite. Cette methode installe egalement un meilleur gestionnaire des messages d'erreurs. • Ligne 12 : L'option labelpos determine l'emplacement du libelle qui accompagne le champ d'entree. Dans notre exemple, nous l'avons place au-dessus, mais vous pourriez preferer le placer ailleurs, a gauche par exemple (labelpos = W). Notez que cette option est indispensable si vous souhaitez un libelle (pas de valeur par defaut). • Ligne 14 : L'option selectioncommand transmet un argument a la fonction invoquee : l'item selectionne dans la boite de liste. Vous pourrez egalement retrouver cette selection a l'aide de la methode get(), comme nous le faisons a la ligne 8 pour actualiser le libelle. 14.4.2 Remarque concernant I'entree de caracteres accentues Nous vous avons deja signale precedemment que Python est tout a fait capable de prendre en charge les alphabets du monde entier (grec, cyrillique, arabe, japonais, etc. - voir notamment page 40). II en va de meme pour Tkinter. En tant que francophone, vous souhaiterez certainement que les utilisateurs de vos scripts puissent entrer des caracteres accentues dans les widgets Entry, Text et leurs derives (ComboBox, ScrolledText). Veuillez done prendre bonne note que lorsque vous entrez dans l'un de ces widgets une chaine contenant un ou plusieurs caracteres non-ASCII (tel qu'une lettre accentuee, par exemple), Tkinter encode cette chaine suivant la norme UTF-8. Si votre ordinateur utilise plutot le codage Latin- 1 par defaut (ce qui est tres souvent le cas), vous devrez convertir la chaine avant de pouvoir l'afficher. Cela peut se faire tres aisem*nt en utilisant la fonction integree encode(). Exemple : # -*- coding: Latin-1 -*- from Tkinter import * def imprimer ( ) : chl = e . get ( ) ch2 = chl. encode ("Latin-1") print ch2 f = Tk() e = Entry (f) e . pack ( ) Button(f, text ="afficher", command =imprimer) .pack () f . mainloop ( ) Essayez ce petit script en entrant des chaines avec caracteres accentues dans le champ d'entree. Essayez encore, mais en remplacant l'instruction « print ch2 » par « print chl ». Concluez. # le widget Entry renvoie une chaine utf8 # conversion utf8 -> Latin-1 206. Gerard Swinnen : Apprendre a programmer avec Python 14.4.3 « Scrolled Text » Ce mega-widget etend les possibilites du widget Text sandard, en lui associant un cadre, un libelle (titre) et des barres de defilement. Comme le demontrera le petit script ci-dessous, il sert fondamen- talement a afficher des textes, mais ceux-ci peuvent etre mis en forme et integrer des images. Vous pouvez egalement rendre « cliquables » les elements affiches (textes ou images), et vous en servir pour declencher toutes sortes de mecanismes. Petite demo du widget ScroltedText Le Corbeau et le Renard nd/'Jean d£ la Fontaine auteur francais Maitre Corbeau, sur un arbre perche, Tenait en son bee un fromage. x Maitre Renard, par I'odeur alleche, Lui tint a peu pres ce lanqaqe : Dans l'application qui genere la figure ci-dessus, par exemple, le fait de cliquer sur le nom « Jean de la Fontaine » provoque le defilement automatique du texte (scrolling), jusqu'a ce qu'une rubrique decrivant cet auteur devienne visible dans le widget (Voir page suivante le script correspondant). D'autres fonctionnalites sont presentes, mais nous ne presenterons ici que les plus fondamentales. Veuillez done consulter les demos et exemples accompagnant Pmw pour en savoir davantage. Gestion du texte affiche : Vous pouvez acceder a n'importe quelle portion du texte pris en charge par le widget grace a deux concepts complementaires, les index et les balises : • Chaque caractere du texte affiche est reference par un index, lequel doit etre une chaine de caracteres contenant deux valeurs numeriques reliees par un point (ex : "5.2"). Ces deux valeurs indiquent respectivement le numero de ligne et le numero de colonne ou se situe le caractere. • N'importe quelle portion du texte peut etre associee a une ou plusieurs balise(s), dont vous choisissez librement le nom et les proprietes. Celles-ci vous permettent de definir la police, les couleurs d'avant- et d'arriere-plan, les evenements associes, etc. Gerard Swinnen : Apprendre a programmer avec Python 207. Note : Pour la bonne comprehension du script ci-dessous, veuillez considerer que le texte de la fable traitee doit etre accessible, dans un fichier nomme « CorbRenard.txt ». I . from Tkinter import * 2 . import Pmw 3. 4. def action (event=None) : 5. """defilement du texte jusqu'a la balise """ 6 . index = st . tag_nextrange ( ' cible ' , '0.0', END) 7. st . see (index [0] ) 8. 9. # Instanciation d'une fenetre contenant un widget ScrolledText : 10. fen = Pmw. initialise () II. st = Pmw. ScrolledText (fen, 12. labelpos =N, 13. label_text ="Petite demo du widget ScrolledText", 14. label_font =' Times 14 bold italic', 15. label_fg = 'navy', label_pady =5, 16. text_font= ' Helvetica 11 normal', text_bg =' ivory', 17. text_padx =10, text_pady =10, text_wrap ='none', 18. borderframe =1, 19. borderf rame_borderwidth =3, 20. borderf rame_relief =SOLID, 21. usehullsize =1, 22. hull_width =370, hull_height =240) 23. st .pack (expand =YES, fill =BOTH, padx =8, pady =8) 24. 25. # Definition de balises, liaison d'un gestionnaire d'evenement au clic de souris 26. st . tag_conf igure ( ' titre ' , foreground =' brown', font =' Helvetica 11 bold italic') 27. st . tag_conf igure (' lien ' , foreground ='blue', font =' Helvetica 11 bold') 28. st . tag_conf igure (' cible ' , foreground =' forest green', font =' Times 11 bold') 29. st . tag_bind ( 1 lien ' , ' ' , action) 30. 31. titre ="""Le Corbeau et le Renard 32. par Jean de la Fontaine, auteur francais 33. \n""" 34. auteur =""" 35 . Jean de la Fontaine 36. ecrivain francais (1621-1695) 37. celebre pour ses Contes en vers, 38. et surtout ses Fables, publiees 39. de 1668 a 1694.""" 40. 41. # Remplissage du widget Text (2 techniques) 42 . st . importf ile ( ' CorbRenard. txt ' ) 43. st. insert ( '0.0' , titre, 'titre') 44. st . insert (END, auteur, 'cible') 45. # Insertion d'une image : 46. photo =PhotoImage (f ile= 'Penguin.gif') 47. st . image_create ( ' 6 . 14 ' , image =photo) 48. # Mise en oeuvre dynamigue d'une balise : 49. st . tag_add ( ' lien ' , '2.4', '2.23') 50. 51. f en . mainloop ( ) Commentaires : 208. Gerard Swinnen : Apprendre a programmer avec Python • Lignes 4-7 : Cette fonction est un gestionnaire d'evenement, qui est appele lorsque l'utilisateur effectue un clic de souris sur le nom de l'auteur (cfr. lignes 27 & 29). A la ligne 6, on utilise la methode tag_nextrange() du widget pour trouver les index de la portion de texte associee a la balise « cible ». La recherche de ces index est limitee au domaine defini par les 2 e & 3 e arguments (dans notre exemple, on recherche du debut a la fin du texte entier). La methode tag_nextrange() renvoie une liste de deux index (ceux des premier et dernier caracteres de la portion de texte associee a la balise « cible »). A la ligne 7, nous nous servons dun seul de ces index (le premier) pour activer la methode see(). Celle-ci provoque un defilement automatique du texte {scrolling), de telle maniere que le caractere correspondant a l'index transmis devienne visible dans le widget (avec en general un certain nombre des caracteres qui suivent). • Lignes 9 a 23 : Construction classique d'une fenetre destinee a afficher un seul widget. Dans le code d'instanciation du widget, nous avons inclus un certain nombre d'options destinees a vous montrer une petite partie des nombreuses possibilites de configuration. • Ligne 12 : L'option labelpos determine l'emplacement du libelle (titre) par rapport a la fenetre de texte. Les valeurs acceptees s'inspirent des lettres utilisees pour designer les points cardinaux (N, S, E, W, ou encore NE, NW, SE, SW). Si vous ne souhaitez pas afficher un libelle, il vous suffit tout simplement de ne pas utiliser cette option. • Lignes 13 a 15 : Le libelle n'est rien d' autre qu'un widget Label standard, integre dans le widget composite ScrolledText. On peut acceder a toutes ses options de configuration, en utilisant la syntaxe qui est presentee dans ces lignes : on y voit qu'il suffit d'associer le prefixe label_ au nom de l'option que Ton souhaite activer, pour definir aisem*nt les couleurs d'avant- et d'arriere- plans, la police, la taille, et meme l'espacement a reserver autour du widget (option pady). • Lignes 16-17 : En utilisant une technique similaire a celle qui est decrite ci-dessus pour le libelle, on peut acceder aux options de configuration du widget Text integre dans ScrolledText. II suffit cette fois d'associer aux noms d' option le prefixe text_. • Lignes 18 a 20 : II est prevu un cadre (un widget Frame) autour du widget Text. L'option borderframe = 1 permet de le faire apparaitre. On accede ensuite a ses options de configuration d'une maniere similaire a celle qui a ete decrite ci-dessus pour label_ et text_. • Lignes 21-22 : Ces options permettent de fixer globalement les dimensions du widget. Une autre possibility serait de definir plutot les dimensions de son composant Text (par exemple a l'aide d'options telles que text_width et text_height), mais alors les dimensions globales du widget risqueraient de changer en fonction du contenu (apparition/disparition automatique de barres de defilement). Note : le mot hull designe le contenant global, c.d.d. le mega-widget lui-meme. • Ligne 23 : Les options expand = YES et fill = BOTH de la methode pack() indiquent que le widget concerne pourra etre redimensionne a volonte, dans ses 2 dimensions horiz. et verticale. • Lignes 26 a 29 : Ces lignes definissent les trois balises « titre », « lien » et « cible » ainsi que le formatage du texte qui leur sera associe. La ligne 29 precise en outre que le texte associe a la balise « lien » sera « cliquable », avec indication du gestionnaire d'evenement correspondant. • Ligne 42 : Importation de texte a partir d'un fichier. Note : II est possible de preciser l'endroit exact ou devra se faire l'insertion, en fournissant un index comme second argument. • Lignes 43-44 : Ces instructions inserent des fragments de texte (respectivement au debut et a la fin du texte preexistant), en associant une balise a chacun d'eux. • Ligne 49 : L'association des balises au texte est dynamique. A tout moment, vous pouvez activer une nouvelle association (comme nous le faisons ici en rattachant la balise « lien » a une portion de texte preexistante). Note : pour « detacher » une balise, utilisez la methode tag deleteQ. Gerard Swinnen : Apprendre a programmer avec Python 209. 14.4.4 « Scrolled Canvas » Le script ci-apres vous montre comment vous pouvez exploiter le mega-widget ScrolledCanvas, lequel etend les possibilites du widget Canvas standard en lui associant des barres de defilement, un libelle et un cadre. Notre exemple constitue en fait un petit jeu d'adresse, dans lequel l'utilisateur doit reussir a cliquer sur un bouton qui s'esquive sans cesse. (Note : si vous eprouvez vraiment des difficultes pour Vattraper, commencez d'abord par dilater lafenetre). Cherchez en 1 07 1 60 Le widget Canvas est tres versatile : il vous permet de combiner a volonte des dessins, des images bitmap, des fragments de texte, et meme d'autres widgets, dans un espace parfaitement extensible. Si vous souhaitez developper l'un ou l'autre jeu graphique, c'est evidemment le widget qu'il vous faut apprendre a maitriser en priorite. Comprenez bien cependant que les indications que nous vous fournissons a ce sujet dans les presentes notes sont forcement tres incompletes. Leur objectif est seulement de vous aider a comprendre quelques concepts de base, afin que vous puissiez ensuite consulter les ouvrages de reference specialises dans de bonnes conditions. Notre petite application se presente comme une nouvelle classe FenPrinc(), obtenue par derivation a partir de la classe de mega-widgets Pmw.ScrolledCanvas(). Elle contient done un grand canevas muni de barres de defilement, dans lequel nous commencons par planter un decor constitue de 80 ellipses de couleur dont l'emplacement et les dimensions sont tires au hasard. Nous y ajoutons egalement un petit clin d'oeil sous la forme d'une image bitmap, destinee avant tout a vous rappeler comment vous pouvez gerer ce type de ressource. Nous y installons enfin un veritable widget : un simple bouton, en l'occurrence, mais la technique mise en oeuvre pourrait s'appliquer a n'importe quel autre type de widget, y compris un gros widget composite comme ceux que nous avons developpes precedemment. Cette grande souplesse dans le developpement d'applications complexes est l'un des principaux benefices apportes par le mode de programmation « orientee objet ». Le bouton s'anime des qu'on l'a enfonce une premiere fois. Dans votre analyse du script ci-apres, soyez attentifs aux methodes utilisees pour modifier les proprietes d'un objet existant. 210. Gerard Swinnen : Apprendre a programmer avec Python I . from Tkinter import * 2 . import Pmw 3 . from random import randrange 4. 5. Pmw . initialise ( ) 6 . coul = [ ' sienna ' , ' maroon ' , ' brown 1 , ' pink ' , ' tan ' , ' wheat ' , ' gold ' , ' orange ' , ' plum ' , 7 . ' red ' , ' khaki ' , ' Indian red ' , ' thistle ' , ' firebrick ' , ' salmon ' , ' coral ' ] 8. 9. class FenPrinc (Pmw. ScrolledCanvas) : 10. """Fenetre principale : canevas extensible avec barres de defilement""" II. def init (self): 12 . Pmw . ScrolledCanvas . init (self, 13. usehullsize =1, hull_width =500, hull_height =300, 14. canvas_bg ='grey40', canvasmargin =10, 15. labelpos =N, label_text ='Attrapez le bouton !', 16. borderframe =1, 17. borderf rame_borderwidth =3) 18. # Les options ci-dessous doivent etre precisees apres initialisation : 19. self . configure (vscrollmode =' dynamic', hscrollmode =' dynamic') 20. self .pack (padx =5, pady =5, expand =YES, fill =BOTH) 21. 22. self. can = self .interior () # acces au composant canevas 23. # Decor : trace d'une serie d' ellipses aleatoires 24. for r in range (80) : 25. xl, yl = randrange (-800, 800) , randrange (-800, 800) 26. x2, y2 = xl + randrange (40, 300) , yl + randrange (40, 300) 27. couleur = coul [randrange (0, 16) ] 28. self . can . create_oval (xl , yl, x2, y2, f ill=couleur , outline= 'black ' ) 29. # Ajout d'une petite image GIF : 30. self.img = Photolmage (f ile = ' linux2 . gif ' ) 31. self . can . create_image (50, 20, image =self.img) 32 . # Dessin du bouton a attraper : 33. self.x, self.y = 50, 100 34. self.bou = Button (self . can, text ="Start", command =self. start) 35. self.fb = self . can . create_window (self . x, self.y, window =self.bou) 36. self . resizescrollregion () 37. 38. def anim(self) : 39. if self. run ==0: 40. return 41. self.x += randrange (-60 , 61) 42. self.y += randrange (-60 , 61) 43. self . can . coords (self . fb, self.x, self.y) 44. self . configure (label_text = 'Cherchez en %s %s' % (self.x, self.y)) 45. self . resizescrollregion ( ) 46. self .after (250, self.anim) 47. 48 . def stop (self) : 49. self. run =0 50. self .bou . configure (text ="Restart", command =self. start) 51. 52 . def start (self) : 53. self .bou . configure (text ="Attrapez-moi !", command =self.stop) 54. self. run =1 55. self.anim() 56. 57. ##### Main Program ############## 58. 59. if name == ' main ' : 60. FenPrinc () .mainloop () Gerard Swinnen : Apprendre a programmer avec Python 211. Commentaires : • Ligne 6 : Tous ces noms de couleurs sont acceptes par Tkinter. Vous pourriez bien evidemment les remplacer par des descriptions hexadecimales, comme nous l'avons explique page 201. • Lignes 12 a 17 : Ces options sont tres similaires a celles que nous avons decrites plus haut pour le widget ScrolledText. Le present mega-widget integre un composant Frame, un composant Label, un composant Canvas et deux composants Scrollbar. On accede aux options de configuration de ces composants a l'aide d'une syntaxe qui relie le nom du composant et celui de l'option par l'intermediaire d'un caractere « souligne ». • Ligne 19 : Ces options definissent le mode d'apparition des barres de defilement. En mode « static », elles sont toujours presentes. En mode « dynamic », elles disparaissent si les dimensions du canevas deviennent inferieures a celles de la fenetre de visualisation. • Ligne 22 : La methode interior() renvoie la reference du composant Canvas integre dans le mega-widget ScrolledCanvas. Les instructions suivantes (lignes 23 a 35) installent ensuite toute une serie d'elements dans ce canevas : des dessins, une image et un bouton. • Lignes 25 a 27 : La fonction randrange() permet de tirer au hasard un nombre entier compris dans un certain intervalle (Veuillez vous referer aux explications de la page 142). • Ligne 35 : C'est la methode create_window() du widget Canvas qui permet d'y inserer n'importe quel autre widget (y compris un widget composite). Le widget a inserer doit cependant avoir ete defini lui-meme au prealable comme un esclave du canevas ou de sa fenetre maitresse. La methode create_window() attend trois arguments : les coordonnees X et Y du point ou Ton souhaite inserer le widget, et la reference de ce widget. • Ligne 36 : La methode resizescrollregion() reajuste la situation des barres de defilement de maniere a ce qu'elles soient en accord avec la portion du canevas actuellement affichee. • Lignes 38 a 46 : Cette methode est utilisee pour l'animation du bouton. Apres avoir repositionne le bouton au hasard a une certaine distance de sa position precedente, elle se re-appelle elle- meme apres une pause de 250 millisecondes. Ce bouclage s'effectue sans cesse, aussi longtemps que la variable selfrun contient une valeur non-nulle. • Lignes 48 a 55 : Ces deux gestionnaires d'evenement sont associes au bouton en alternance. lis servent evidemment a demarrer et a arreter l'animation. 212. Gerard Swinnen : Apprendre a programmer avec Python 14.4.5 Barres d'outils avec bulles d'aide - expressions lambda De nombreux programmes comportent une ou plusieurs « barres d'outils » (toolbar) constitutes de petit* boutons sur lesquels sont representes des pictogrammes (icones). Cette facon de faire permet de proposer a l'utilisateur un grand nombre de commandes specialisees, sans que celles-ci n'occupent une place excessive a l'ecran (un petit dessin vaut mieux qu'un long discours, dit-on). La signification de ces pictogrammes n'est cependant pas toujours evidente, surtout pour les utilisateurs neophytes. II est done vivement conseille de completer les barres d'outils a l'aide d'un systeme de bulles d'aide (tool tips), qui sont des petit* messages explicatifs apparaissant automatiquement lorsque la souris survole les boutons concernes. L'application decrite ci-apres comporte une barre d'outils et un canevas. Lorsque l'utilisateur clique sur l'un des boutons de la barre, le pictogramme qu'il porte est recopie dans le canevas, a un emplacement choisi au hasard : Dans notre exemple, chaque bouton apparait entoure d'un sillon. Vous pouvez aisem*nt obtenir d'autres aspects en choisissant judicieusem*nt les options relief et bd (bordure) dans l'instruction d'instanciation des boutons. En particulier, vous pouvez choisir relief = FLAT et bd =0 pour obtenir des petit* boutons « plats », sans aucun relief. La mise en place des bulles d'aide est un jeu d'enfant. II suffit d'instancier un seul objet Pmw.Balloon pour l'ensemble de l'application, puis d'associer un texte a chacun des widgets auxquels on souhaite associer une bulle d'aide, en faisant appel autant de fois que necessaire a la methode bind() de cet objet. 1 . from Tkinter import * 2 . import Pmw 3 . from random import randrange 4. 5. # noms des fichiers contenant les icones (format GIF) : 6 . images = ( ' f loppy_2 ' , ' papi2 ' , ' pion_l ' , ' pion_2 ' , ' help_4 ' ) 7. textes = ( ' sauvegarde ' , 'papillon ' , ' joueur l','joueur 2', 'Aide') 8. 9. class Application (Frame) : 10. def init (self): 11. Frame. init (self) 12. # Creation d'un objet (un seul suffit) : 13. tip = Pmw. Balloon (self ) 14. # Creation de la barre d'outils (e'est un simple cadre) 15. toolbar = Frame (self, bd =1) Gerard Swinnen : Apprendre a programmer avec Python 213. 16. toolbar. pack (expand =YES, fill =X) 17 . # Nombre de boutons a construire : 18. nBou = len (images) 19. # Les icones des boutons doivent etre placees dans des variables 20. # persistantes . Une liste fera 1' affaire : 21. self.photol =[None]*nBou 22. 23 . for b in range (nBou) : 24. # Creation de 1 ' icone (objet Photolmage Tkinter) : 25. self .photol [b] =PhotoImage (f ile = images [b] +'.gif) 26. 27. # Creation du bouton. : 28. # On utilise une expression "lambda" pour transmettre 29. # un argument a la methode invoquee comme commande : 30. bou = Button (toolbar, image =self .photol [b] , relief =GROOVE, 31. command = lambda arg =b: self . action (arg) ) 32. bou. pack (side =LEFT) 33. 34. # association du bouton avec un texte d'aide (bulle) : 35. tip. bind (bou, textes[b]) 36. 37. self.ca = Canvas (self, width =400, height =200, bg =' orange ' ) 38. self .ca. pack () 39. self. pack () 40. 41. def action (self, b) : 42. "1' icone du bouton b est recopiee dans le canevas" 43. x, y = randrange (25, 375) , randrange (25 , 175) 44. self . ca . create_image (x, y, image =self .photol [b] ) 45. 46. Application () .mainloop () Metaprogrammation. Expressions lambda : Vous savez qu'en regie generale, on associe a chaque bouton une commande, laquelle est une methode ou une fonction particuliere qui se charge d'effectuer le travail lorsque le bouton est active. Or dans l'application presente, tous les boutons doivent faire a peu pres la meme chose (recopier un dessin dans le canevas), la seule difference entre eux etant le dessin concerne. Pour simplifier notre code, nous voudrions done pouvoir associer l'option command de tous nos boutons avec une seule et meme methode (ce sera la methode action() ), mais en lui transmettant a chaque fois la reference du bouton particulier utilise, de maniere a ce que faction accomplie puisse etre differente pour chacun d'eux. Une difficulty se presente, cependant, parce que l'option command du widget Button accepte seulement une valeur ou une expression, et non une instruction. II est done permis de lui indiquer la reference d'une fonction, mais pas de l'invoquer veritablement en lui transmettant des arguments eventuels (e'est la raison pour laquelle on indique le nom de cette fonction sans lui adjoindre de parentheses). On peut resoudre cette difficulty de deux manieres : • Du fait de son caractere dynamique, Python accepte qu'un programme puisse se modifier lui- meme, par exemple en definissant de nouvelles fonctions au cours de son execution (e'est le concept de metaprogrammation). II est done possible de definir a la volee une fonction qui utilise des parametres, en indiquant pour chacun de ceux-ci une valeur par defaut, et ensuite d'invoquer cette meme fonction sans arguments la ou ceux-ci ne sont pas autorises. Puisque la fonction est definie en cours d'execution, les valeurs par defaut peuvent etre les contenus de variables, et le resultat de 1' operation est un veritable transfert d'arguments. 214. Gerard Swinnen : Apprendre a programmer avec Python Pour illustrer cette technique, remplacez les lignes 27 a 3 1 du script par les suivantes : # Creation du bouton. : # On definit a la volee une fonction avec un parametre, dont # la valeur par defaut est 1 ' argument a transmettre . # Cette fonction appelle la methode qui necessite un argument : def agir(arg = b) : self . action (arg) # La commande associee au bouton appelle la fonction ci-dessus : bou = Button (toolbar, image =self .photol [b] , relief =GROOVE, command = agir) • Tout ce qui precede peut etre simplifie en faisant appel a une expression lambda. Ce mot reserve Python designe une expression qui renvoie un objet fonction, similaire a ceux que vous creez avec l'instruction def, mais avec la difference que lambda etant une expression et non une instruction, on peut l'utiliser comme interface afin d'invoquer une fonction (avec passage d'arguments) la ou ce n'est normalement pas possible. Notez au passage qu'une telle fonction est anonyme (elle ne possede pas de nom). Par exemple, l'instruction : lambda arl=b, ar2=c : bidule (arl ,ar2) renvoie la reference d'une fonction anonyme qui aura elle-meme invoque la fonction bidule() en lui transmettant les arguments b et c , ceux-ci etant utilises comme valeurs par defaut dans la definition des parametres de la fonction. Cette technique utilise finalement le meme principe que la precedente, mais elle presente l'avantage d'etre plus concise, raison pour laquelle nous l'avons utilisee dans notre script. En revanche, elle est un peu plus difficile a comprendre : command = lambda arg =b: self . action (arg) Dans cette portion d'instruction, la commande associee au bouton se refere a une fonction anonyme dont le parametre arg possede une valeur par defaut : la valeur de l'argument b. Invoquee sans argument par la commande, cette fonction anonyme peut tout de meme utiliser son parametre (avec la valeur par defaut) pour faire appel a la methode cible self.action(), et Ton obtient ainsi un veritable transfert d'argument vers cette methode . Nous ne detaillerons pas davantage ici la question des expressions lambda, car elle deborde du cadre que nous nous sommes fixes pour cet ouvrage d'initiation. Si vous souhaitez en savoir plus, veuillez done consulter l'un ou l'autre des ouvrages de reference cites dans la bibliographie. Gerard Swinnen : Apprendre a programmer avec Python 215. 14.5 Fenetres avec menus Nous allons decrire a present la construction d'une fenetre d'application dotee de differents types de menus « deroulants », chacun de ces menus pouvant etre « detache » de l'application principale pour devenir lui-meme une petite fenetre independante, comme dans l'illustration ci-dessous. Cet exercice un peu plus long nous servira egalement de revision, et nous le realiserons par etapes, en appliquant une strategic de programmation que Ton appelle developpement incremental. Comme nous l'avons deja explique precedemment 55 , cette methode consiste a commencer l'ecriture d'un programme par une ebauche, qui ne comporte que quelques lignes seulement mais qui est deja fonctionnelle. On teste alors cette ebauche soigneusem*nt afin d'en eliminer les bugs eventuels. Lorsque l'ebauche fonctionne correctement, on y ajoute une fonctionnalite supplemental. On teste ce complement jusqu'a ce qu'il donne entiere satisfaction, puis on en ajoute un autre, et ainsi de suite... Cela ne signifie pas que vous pouvez commencer directement a programmer sans avoir au prealable effectue une analyse serieuse du projet, dont au mo ins les grandes lignes devront etre convenablement decrites dans un cahier des charges clairement redige. 77 reste egalement imperatif de commenter convenablement le code produit, au fur et a mesure de son elaboration. S'efforcer de rediger de bons commentaires est en effet necessaire, non seulement pour que votre code soit facile a lire (et done a maintenir plus tard, par d'autres ou par vous-meme), mais aussi pour que vous soyez forces d'exprimer ce que vous souhaitez vraiment que la machine fasse (Cfr. Erreurs semantiques, page 15) Cahier des charges de l'exercice : Notre application comportera simplement une barre de menus et un canevas. Les differentes rubriques et options des menus ne serviront qu'a faire apparaitre des fragments de texte dans le canevas ou a modifier des details de decoration, mais ce seront avant tout des exemples varies, destines a donner un apercu des nombreuses possibilites offertes par ce type de widget, accessoire indispensable de toute application moderne d'une certaine importance. Nous souhaitons egalement que le code produit dans cet exercice soit bien structure. Pour ce faire, nous ferons usage de deux classes : une classe pour l'application principale, et une autre pour la barre de menus. Nous voulons proceder ainsi afin de bien mettre en evidence la construction d'une application type incorporant plusieurs classes d'objets interactifs. 55 Voir page 16 : Recherche des erreurs et experimentation Exemples de menus ^JnJiU Fichier Musiciens Peintres Options Fic.HE Effacer Terminer xj Op...B0 2^ W. A. Mo. E. Delaa Nympheas a Giver Le moulin de la galette _ |n| x| 17e siecle Activer : * peintres v' musiciens Relief : * aucun sorti rentre rainure crete bordure 18e siecle Pei... HHQ classiques jomantiques jmpressionistes ► bH!WTTd1 x| Claude Monet Auguste Renoir Edgar Degas 216. Gerard Swinnen : Apprendre a programmer avec Python 14.5.1 Premiere ebauche du programme : Lorsque Ton construit l'ebauche d'un programme, il faut tacher d'y faire apparaitre le plus tot possible la structure d'ensemble, avec les relations entre les principaux blocs qui constitueront l'application definitive. C'est ce que nous nous sommes efforces de faire dans l'exemple ci-dessous : l . 2 . 3. 4 . 5. 6. 7 . 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. from Tkinter import * class MenuBar (Frame) : " " "Barre de menus deroulants" " " def init (self, boss =None) : Frame. init (self, borderwidth =2) ##### Menu ##### fileMenu = Menubutton (self , fileMenu. pack (side =LEFT) # Partie "deroulante" : mel = Menu (fileMenu) mel . addcommand ( label = command mel . addcommand ( label = command # Integration du menu : fileMenu . configure (menu = mel) text = ' Fichier ' ) Effacer', underline =0, = boss . effacer) Terminer', underline =0, = boss. quit) class Application (Frame) : ii ii "Application principale" " " def init (self, boss =None) : Frame . init (self) self .master. title ( 'Fenetre avec menus') mBar = MenuBar (self ) mBar . pack ( ) self. can = Canvas (self, bg=' light grey', width=250, borderwidth self . can . pack ( ) self .pack () height=190, =2) def effacer (self ) : self . can . delete (ALL) if name == ' main ' : app = Application () app . mainloop ( ) Fenelie avec menus Fichier Veuillez done encoder ces lignes et en tester l'execution. Vous devriez obtenir une fenetre avec un canevas gris clair surmonte d'une barre de menus. A ce stade, la barre de menus ne comporte encore que la seule rubrique « Fichier ». Cliquez sur la rubrique « fichier » pour faire apparaitre le menu correspondant : l'option « Effacer » n'est pas encore fonctionnelle (elle servira a effacer le contenu du canevas), mais l'option « Terminer » devrait deja vous permettre de fermer proprement l'application. ' Comme tous les menus geres par Tkinter, le menu que vous avez cree peut etre converti en menu « flottant » : il suffit de cliquer sur la ligne pointillee apparaissant en-tete de menu. Vous obtenez ainsi une petite fenetre satellite, que vous pouvez alors positionner ou bon vous semble sur le bureau. Gerard Swinnen : Apprendre a programmer avec Python 211. Analyse du script : La structure de ce petit programme devrait desormais vous apparaitre familiere : afin que les classes definies dans ce script puissent eventuellement etre (re)utilisees dans d'autres projets par importation, comme nous l'avons deja explique precedemment 56 , le corps principal du programme (lignes 35 a 37) comporte l'instruction classique : if name == ' main ': Les deux instructions qui suivent consistent seulement a instancier un objet app et a faire fonctionner sa methode mainloop(). Comme vous le savez certainement, nous aurions pu egalement condenser ces deux instructions en une seule. L'essentiel du du programme se trouve cependant dans les definitions de classes qui precedent : La classe MenuBar() contient la description de la barre de menus. Dans l'etat present du script, elle se resume a une ebauche de constructeur. • Ligne 5 : Le parametre boss receptionne la reference de la fenetre maitresse du widget au moment de son instanciation. Cette reference va nous permettre d'invoquer les methodes associees a cette fenetre maitresse, aux lignes 14 & 16. • Ligne 6 : Activation obligatoire du constructeur de la classe parente. • Ligne 9 : Instanciation d'un widget de la classe Menubutton(), defini comme un « esclave » de self (c'est-a-dire l'objet composite « barre de menus » dont nous sommes occupes a definir la classe). Comme l'indique son nom, ce type de widget se comporte un peu comme un bouton : une action se produit lorsque Ton clique dessus. • Ligne 12 : Afin que cette action consiste en l'apparition veritable d'un menu, il reste encore a definir celui-ci : ce sera encore un nouveau widget, de la classe Menu() cette fois, defini lui- meme comme un « esclave » du widget Menubutton instancie a la ligne 9. • Lignes 13 a 16 : On peut appliquer aux widgets de la classe Menu() un certain nombre de methodes specifiques, chacune d'elles acceptant de nombreuses options. Nous utilisons ici la methode add_command() pour installer dans le menu les deux items « Effacer » et « Terminer ». Nous y integrons tout de suite l'option underline, qui sert a definir un raccourci clavier : cette option indique en effet lequel des caracteres de l'item doit apparaitre souligne a l'ecran. L'utilisateur sait alors qu'il lui suffit de frapper ce caractere au clavier pour que faction correspondant a cet item soit activee (comme s'il avait clique dessus a l'aide de la souris). L'action a declencher lorsque l'utilisateur selectionne l'item est designee par l'option command. Dans notre script, les commandes invoquees sont toutes les deux des methodes de la fenetre maitresse, dont la reference aura ete transmise au present widget au moment de son instanciation par l'intermediaire du parametre boss. La methode effacer(), que nous definissons nous-meme plus loin, servira a vider le canevas. La methode predefinie quit() provoque la sortie de la boucle mainloopO et done l'arret du receptionnaire d'evenements associe a la fenetre d'application. • Ligne 18 : Lorsque les items du menu ont ete definis, il reste encore a reconfigurer le widget maitre Menubutton de maniere a ce que son option « menu » designe effectivement le Menu que nous venons de construire. En effet, nous ne pouvions pas deja preciser cette option lors de la definition initiale du widget Menubutton, puisqu'a ce stade le Menu n'existait pas encore. Nous ne pouvions pas non plus definir le widget Menu en premier lieu, puisque celui-ci doit etre defini comme un « esclave » du widget Menubutton. II faut done bien proceder en trois etapes comme nous l'avons fait, en faisant appel a la methode configure(). (Cette methode peut etre appliquee a n'importe quel widget preexistant pour en modifier l'une ou l'autre option). 56 Voir page 172: Modules contenant des bibliotheques de classes 218. Gerard Swinnen : Apprendre a programmer avec Python La classe Application() contient la description de la fenetre principale du programme ainsi que les methodes gestionnaires d'evenements qui lui sont associees. • Ligne 20 : Nous preferons faire deriver notre application de la classe Frame(), qui presente de nombreuses options, plutot que de la classe primordiale Tk(). De cette maniere, l'application toute entiere est encapsulee dans un widget, lequel pourra eventuellement etre integre par la suite dans une application plus importante. Rappelons que de toute maniere, Tkinter instanciera automatiquement une fenetre maitresse de type Tk() pour contenir de cette Frame. • Lignes 23-24 : Apres l'indispensable activation du constructeur de la classe parente, nous utilisons l'attribut master que Tkinter associe automatiquement a chaque widget, pour referencer la fenetre principale de l'application (la fenetre maitresse dont nous venons de parler au paragraphe precedent) et en redefinir le bandeau- titre. • Lignes 25 a 29 : Instanciation de deux widgets esclaves pour notre Frame principale. La « barre de menus » est evidemment le widget defini dans l'autre classe. • Ligne 30 : Comme n'importe quel autre widget, notre Frame principale doit etre mise en place. • Lignes 32-33 : La methode servant a effacer le canevas est definie dans la classe presente (puisque l'objet canevas en fait partie), mais elle est invoquee par l'option command dun widget esclave defini dans l'autre classe. Comme nous l'avons explique plus haut, ce widget esclave recoit la reference de son widget maitre par l'intermediaire du parametre boss. Toutes ces references sont hierarchisees a l'aide de la qualification des noms par points. 14.5.2 Ajout de la rubrique « Musiciens » Continuez le developpement de ce petit programme, en ajoutant les lignes suivantes dans le constructeur de la classe MenuBar() (apres la ligne 1 8) : ##### Menu ##### self.musi = Menubutton (self , text = 'Musiciens ' ) self .musi .pack (side =LEFT, padx ='3') # Partie "deroulante" du menu : mel = Menu (self .musi) mel. add command (label ='17e siecle', underline =1, foreground ='red', background =' yellow', font =( 'Comic Sans MS', 11), command = boss . showMusil7 ) mel. add command (label ='18e siecle', underline =1, f oreground= ' royal blue ' , background = ' white ' , font =( 'Comic Sans MS', 11, 'bold'), command = boss . showMusil8) # Integration du menu : self .musi . configure (menu = mel) ... ainsi que les definitions de methodes suivantes a la classe Application() (apres la ligne 33) : def showMusil7 (self) : self . can . create_text (10, 10, anchor =NW, text ='H. Purcell', f ont= (' Times ' , 20, 'bold'), f ill =' yellow ' ) def showMusil8 (self) : self .can. create_text (245, 40, anchor =NE, text ="W. A. Mozart", font =( 'Times', 20, 'italic'), fill ='dark green') Gerard Swinnen : Apprendre a programmer avec Python 219. Lorsque vous y aurez ajoute toutes ces lignes, sauvegardez le script et executez-le. Votre barre de menus comporte a present une rubrique supplemental : la rubrique « Musiciens ». Le menu correspondant propose deux items qui sont affiches avec des couleurs et des polices personnalisees. Vous pourrez vous inspirer de ces techniques decoratives pour vos projets personnels. A utiliser avec moderation ! Les commandes que nous avons associees a ces items sont evidemment simplifiees afin de ne pas alourdir l'exercice : elles provoquent l'affichage de petit* textes sur le canevas. Fenetre avec menus ^Jnjxj Fichier Musiciens I Fichier Ef facer Terminer W. A. Mozart |n| x| Musiciens | 17 e siecle 18e siecle Analyse du script Les seules nouveautes introduites dans ces lignes concernent l'utilisation de polices de caracteres bien determinees (option font), ainsi que de couleurs pour l'avant-plan (option foreground) et le fond (option background) des textes affiches. Veuillez noter encore une fois l'utilisation de l'option underline pour designer les caracteres correspondant a des raccourcis claviers (en n'oubliant pas que la numerotation des caracteres d'une chaine commence a partir de zero), et surtout que l'option command de ces widgets accede aux methodes de l'autre classe, par l'intermediaire de la reference memorisee dans l'attribut boss. La methode create_text() du canevas doit etre utilisee avec deux arguments numeriques, qui sont les coordonnees X et Y d'un point dans le canevas. Le texte transmis sera positionne par rapport a ce point, en fonction de la valeur choisie pour l'option anchor : Celle-ci determine comment le fragment de texte doit etre « ancre » au point choisi dans le canevas, par son centre, par son coin superieur gauche, etc., en fonction d'une syntaxe qui utilise l'analogie des points cardinaux geographiques (NW = angle superieur gauche, SE = angle inferieur droit, CENTER = centre, etc.) 220. Gerard Swinnen : Apprendre a programmer avec Python 14.5.3 Ajout de la rubrique « Peintres » : Cette nouvelle rubrique est construite d'une maniere assez semblable a la precedente, mais nous lui avons ajoute une fonctionnalite supplemental : des menus « en cascade ». Veuillez done aj outer les lignes suivantes dans le constructeur de la classe MenuBar() : ##### Menu ##### self.pein = Menubutton (self , text =' Peintres') self .pein .pack (side =LEFT, padx= ' 3 ' ) # Partie "deroulante" : mel = Menu (self .pein) mel. add command (label = ' classiques ' , state=DISABLED) mel . add_command (label = ' romantiques ' , underline =0, command = boss . showRomanti) # Sous-menu pour les peintres impressionistes : me 2 = Menu (mel) me2 . add_command (label =' Claude Monet', underline =7, command = boss . tabMonet ) me2 . add_command (label ='Auguste Renoir', underline =8, command = boss . tabRenoir) me2 . add_command (label =' Edgar Degas', underline =6, command = boss . tabDegas) # Integration du sous-menu : mel. add cascade (label =' impressionistes ' , underline=0, menu =me2) # Integration du menu : self .pein . configure (menu =mel) ... et les definitions suivantes dans la classe Application() : def showRomanti (self ) : self .can. create_text (245, 70, anchor =NE, text = "E. Delacroix", font =( 'Times', 20, 'bold italic'), fill ='blue') def tabMonet (self ) : self . can . create_text (10, 100, anchor =NW, text = 'Nympheas a Giverny', font =(' Technical ' , 20), fill ='red') def tabRenoir (self ) : self . can . create_text (10, 130, anchor =NW, text = 'Le moulin de la galette', font =('Dom Casual BT ' , 20), fill =' maroon') def tabDegas (self ) : self . can . create_text (10, 160, anchor =NW, text = 'Danseuses au repos ' , font =( 'President ' , 20), fill ='purple') Analyse du script : Vous pouvez realiser aisem*nt des menus en cascade, en enchainant des sous-menus les uns aux autres jusqu'a un niveau quelconque (il vous est cependant deconseille d'aller au-dela de 5 niveaux successifs : vos utilisateurs s'y perdraient). Un sous-menu est defini comme un menu « esclave » du menu de niveau precedent (dans notre exemple, me2 est defini comme un menu « esclave » de mel). reintegration est assuree ensuite a l'aide de la methode add_cascade(). L'un des items est desactive (option state = DISABLED). L'exemple suivant vous montrera comment vous pouvez activer ou desactiver a volonte des items, par programme. Gerard Swinnen : Apprendre a programmer avec Python 221. 14.5.4 Ajout de la rubrique « Options » : La definition de cette rubrique est un peu plus compliquee, parce que nous allons y integrer l'utilisation de variables internes a Tkinter. Les fonctionnalites de ce menu sont cependant beaucoup plus elaborees : les options ajoutees permettent en effet d'activer ou de desactiver a volonte les rubriques « Musiciens » et « Peintres », et vous pouvez egalement modifier a volonte l'aspect de la barre de menus elle-meme. Veuillez done aj outer les lignes suivantes dans le constructeur de la classe MenuBarQ : ■ JJ,' ,,.1'ih J ' .-ff^^M -inlxl Fichier M usiciens Peinttes Options Options PlIilQ Activer : musiciens peintres \k t KM LO £^ «0 r^" iNym pneas Le moulin de 1 Relief : aucun sorti rentre sillon crete bordure ##### Menu ##### optMenu = Menubutton (self , text =' Options') optMenu. pack (side =LEFT, padx ='3') # Variables Tkinter : self. relief = IntVar() self.actPein = IntVar() self.actMusi = IntVar() # Partie "deroulante" du menu : self .mo = Menu (optMenu) self .mo . addcommand (label = 'Activer :', foreground ='blue') self .mo . add_checkbutton (label = 'musiciens' , command = self . choixActif s, variable =self . actMusi) self .mo . add_checkbutton (label =' peintres' , command = self . choixActif s, variable =self . actPein) self . mo . add_separator ( ) self .mo . add_command (label = 'Relief :', foreground ='blue') for (v, lab) in [ (0, ' aucun ') , (1, 'sorti'), (2 ,' rentre ') , (3, 'sillon') , (4, 'crete'), (5 ,' bordure ')] : self .mo . add_radiobutton (label =lab, variable =self . relief , value =v, command =self . reliefBarre) # Integration du menu : optMenu . configure (menu = self .mo) ... ainsi que les definitions de methodes suivantes (toujours dans la classe MenuBar()) : def reliefBarre (self ) : choix = self .relief .get () self .configure (relief = [FLAT, RAISED, SUNKEN, GROOVE, RIDGE, SOLID] [choix] ) def choixActif s (self ) : p = self .actPein. get () m = self . actMusi . get () self .pein. configure (state = [DISABLED, NORMAL] [p] ) self .musi. configure (state = [DISABLED, NORMAL] [m] ) 222. Gerard Swinnen : Apprendre a programmer avec Python Analyse du script a) Menu avec « cases a cocher » Notre nouveau menu deroulant comporte deux parties. Afin de bien les mettre en evidence, nous avons insere une ligne de separation ainsi que deux « faux items » (« Activer : » et « Relief : ») qui servent simplement de titres. Nous faisons apparaitre ceux-ci en couleur pour que l'utilisateur ne les confonde pas avec de veritables commandes. Les items de la premiere partie sont dotees de « cases a cocher ». Lorsque l'utilisateur effectue un clic de souris sur l'un ou l'autre de ces items, les options correspondantes sont activees ou desactivees, et ces etats « actif / inactif » sont affiches sous la forme d'une coche. Les instructions qui servent a mettre en place ce type de rubrique sont assez explicites. Elles presentent en effet ces items comme des widgets de type « chekbutton » : self .mo . add_checkbutton (label = 'musiciens ' , command = choixActifs, variable = mbu.rael .music) II est important de comprendre ici que ce type de widget comporte necessairement une variable interne, destinee a memoriser l'etat « actif / inactif » du widget. Cette variable ne peut pas etre une variable Python ordinaire, parce que les classes de la bibliotheque Tkinter sont ecrites dans un autre langage. Et par consequent, on ne pourra acceder a une telle variable interne qu'a travers une interface. Cette interface, appelee « variable Tkinter », est en fait un objet, que Ton cree a partir d'une classe particuliere, qui fait partie du module Tkinter au meme titre que les classes de widgets. L'utilisation de ces « objets-variables » est relativement simple : • La classe IntVar() permet de creer des objets equivalents a des variables de type entier. On commence done par creer un ou plusieurs de ces objets-variables, que Ton memorise dans notre exemple comme de nouveaux attribiuts d'instance : self . actMusi =IntVar() Apres cette affectation, l'objet reference dans self.actMusi contient desormais l'equivalent d'une variable de type entier, dans un format specifique a Tkinter. • Ensuite, on associe l'option « variable » de l'objet checkbutton a la variable Tkinter ainsi definie : self .mo . add_checkbutton (label ='musiciens ' , variable =self . actMusi) • II est necessaire de proceder ainsi en deux etapes, parce que Tkinter ne peut pas directement assigner des valeurs aux variables Python. Pour une raison similaire, il n'est pas possible a Python de lire directement le contenu d'une variable Tkinter. II faut utiliser pour cela une methode specifique de cette classe d' objets : la methode get() 57 : m = self . actMusi . get () Dans cette instruction, nous affectons a m (variable ordinaire de Python) le contenu d'une variable Tkinter (laquelle est elle-meme associee a un widget bien determine). Tout ce qui precede peut vous paraitre un peu complique. Considerez simplement qu'il s'agit de votre premiere rencontre avec les problemes d'interfacage entre deux langages de programmation differents, utilises ensemble dans un projet composite. 57 Pour e'erire dans une variable Tkinter, il faudrait utiliser la methode set() . Exemple : self . actMusi . set (45) Gerard Swinnen : Apprendre a programmer avec Python 223. b) Menu avec choix exclusifs La deuxieme partie du menu « Options » pennet a l'utilisateur de choisir l'aspect que prendra la barre de menus, parmi six possibilites. II va de soi que Ton ne peut activer qu'une seule de ces possibilites a la fois. Pour mettre en place ce genre de fonctionnalite, on fait classiquement appel appel a des widgets de type « boutons radio ». La caracteristique essentielle de ces widgets est que plusieurs d'entre eux doivent etre associes a une seule et meme variable Tkinter. A chaque bouton radio correspond alors une valeur particuliere, et c'est cette valeur qui est affectee a la variable lorsque l'utilisateur selectionne le bouton. Ainsi, 1'instruction : self . mo . add_radiobutton (label ='sillon', variable =self . relief , value =3, command =self . relief Barre) configure un item du menu «Options» de telle maniere qu'il se comporte comme un bouton radio. Lorsque l'utilisateur selectionne cet item, la valeur 3 est affectee a la variable Tkinter self.relief (celle-ci etant designee a l'aide de l'option variable du widget), et un appel est lance en direction de la methode reliefBarre(). Celle-ci recupere alors la valeur memorisee dans la variable Tkinter pour effectuer son travail. Dans le contexte particulier de ce menu, nous souhaitons proposer 6 possibilites differentes a l'utilisateur. II nous faut done six « boutons radio », pour lesquels nous pourrions encoder six instructions similaires a celle que nous avons reproduite ci-dessus, chacune d'elles ne differant des cinq autres que par ses options value et label. Dans une situation de ce genre, la bonne pratique de programmation consiste a placer les valeurs de ces options dans une liste, et a parcourir ensuite cette liste a l'aide d'une boucle for, afin d'instancier les widgets avec une instruction commune : for (v, lab) in [ (0, ' aucun ' ) , (1,'sorti'), (2 , ' rentre ' ) , (3, 'sillon') , (4, 'crete'), (5, 'bordure ' ) ] : self .mo. add_radiobutton (label =lab, variable =self . relief , value =v, command =self . relief Barre) La liste utilisee est une liste de 6 tuples (valeur, libelle). A chacune des 6 iterations de la boucle, un nouvel item radiobutton est instancie, dont les options label et value sont extraites de la liste par l'intermediaire des variables lab et v. Dans vos projets personnels, il vous arrivera frequemment de constater que vous pouvez ainsi remplacer des suites d'instructions similaires, par une structure de programmation plus compacte (en general, la combinaison d'une liste et d'une boucle, comme dans l'exemple ci-dessus). Vous decouvrirez petit a petit encore d'autres techniques pour alleger votre code : nous en fournissons encore un exemple dans le paragraphe suivant. Tachez cependant de garder a l'esprit cette regie essentielle, qu'un bon programme doit avant tout rester lisible et commente. 224. Gerard Swinnen : Apprendre a programmer avec Python c) Controle du flux d'execution a l'aide d'une liste Veuillez a present considerer la definition de la methode reliefBarre() : A la premiere ligne, la methode get() nous permet de recuperer l'etat d'une variable Tkinter qui contient le numero du choix opere par l'utilisateur dans le sous-menu « Relief : ». A la seconde ligne, nous utilisons le contenu de la variable choix pour extraire d'une liste de six elements celui qui nous interesse. Par exemple, si choix contient la valeur 2, c'est l'option SUNKEN qui sera utilisee pour reconfigurer le widget. La variable choix est done utilisee ici comme un index, servant a designer un element de la liste. En lieu et place de cette construction compacte, nous aurions pu programmer une serie de tests conditionnels, comme par exemple : if choix ==0 : self . configure (relief =FLAT) elif choix ==1: self . configure (relief =RAISED) elif choix ==2 : self .configure (relief = SUNKEN) etc. D'un point de vue strictement fonctionnel, le resultat serait exactement le meme. Vous admettrez cependant que la construction que nous avons choisie est d'autant plus efficiente, que le nombre de possibilites de choix est eleve. Imaginez par exemple que l'un de vos programmes personnels doive effectuer une selection dans un tres grand nombre d'elements : avec une construction du type ci- dessus, vous seriez peut-etre amene a encoder plusieurs pages de « elif » ! Nous utilisons encore la meme technique dans la methode choixActifs(). Ainsi l'instruction : self .pein. configure (state = [DISABLED, NORMAL] [p] ) utilise le contenu de la variable p comme index pour designer lequel des deux etats DISABLED, NORMAL doit etre selectionne pour reconfigurer le menu « Peintres ». Lorsqu'elle est appelee, la methode choixActifs() reconfigure done les deux rubriques « Peintres » et « Musiciens » de la barre de menus, pour les faire apparaitre « normales » ou « desactivees » en fonction de l'etat des variables m et p, lesquelles sont elles-memes le reflet de variables Tkinter. Ces variables intermediaries m et p ne servent en fait qu'a clarifier le script. II serait en effet parfaitement possible de les eliminer, et de rendre le script encore plus compact, en utilisant la composition d'instructions. On pourrait par exemple remplacer les deux instructions : m = self .actMusi. get () self .musi . configure (state = [DISABLED, NORMAL] [m] ) par une seule, telle que : self .musi . configure (state = [DISABLED, NORMAL] [self . actMusi . get ()] ) Notez cependant que ce que Ton gagne en compacite se paie d'une certaine perte de lisibilite. Gerard Swinnen : Apprendre a programmer avec Python 225. d) Pre-selection d'une rubrique Pour terminer cet exercice, voyons encore comment vous pouvez determiner a l'avance certaines selections, ou bien les modifier par programme. Veuillez done ajouter l'instruction suivante dans le constructeur de la classe Application() (juste avant l'instruction self.pack() ,par exemple) : mBar . mo . invoke ( 2 ) Lorsque vous executez le script ainsi modifie, vous constatez qu'au depart la rubrique « Musiciens » de la barre de menus est active, alors que la rubrique « Peintres » ne Test pas. Programmees comme elles le sont, ces deux rubriques devraient etre actives toutes deux par defaut. Et e'est effectivement ce qui se passe si nous supprimons l'instruction : mBar . mo . invoke ( 2 ) Nous vous avons suggere d'ajouter cette instruction au script, pour vous montrer comment vous pouvez effectuer par programme la meme operation que celle que Ton obtient normalement avec un clic de souris. L'instruction ci-dessus invoque le widget mBar.mo en actionnant la commande associee au deuxieme item de ce widget. En consultant le listing, vous pouvez verifier que ce deuxieme item est bien l'objet de type checkbutton qui active/desactive le menu « Peintres » (Rappelons encore une fois que Ton numerate toujours a partir de zero). Au demarrage du programme, tout se passe done comme si l'utilisateur effectuait tout de suite un premier clic sur la rubrique « Peintres » du menu « Options », ce qui a pour effet de desactiver le menu correspondant. 226. Gerard Swinnen : Apprendre a programmer avec Python Chapitre 15 : Analyse de programmes concrets Dans ce chapitre, nous allons nous efforcer d'illustrer la demarche de conception d'un programme graphique, depuis ses premieres ebauches jusqu'a un stade de developpement relativement avance. Nous souhaitons montrer ainsi combien la programmation orientee objet peut faciliter et surtout securiser la strategic de developpement incremental que nous preconisons 58 . L'utilisation de classes s'impose, lorsque Ton constate qu'un projet en cours de realisation se revele nettement plus complexe que ce que Ton avait imagine au depart. Vous vivrez certainement vous-meme des cheminements similaires a celui que nous decrivons ci-dessous. 15. 1 Jeu des bombardes Ce projet de jeu 59 s'inspire d'un travail similaire realise par des eleves de terminale. II est vivement recommande de commencer l'ebauche d'un tel projet par une serie de petit* dessins et de schemas, dans lesquels seront decrits les differents elements graphiques a construire, ainsi qu'un maximum de cas d 'utilisations. Si vous rechignez a utiliser pour cela la bonne vieille technologie papier/crayon (laquelle a pourtant bien fait ses preuves), vous pouvez tirer profit d'un logiciel de dessin technique, tel l'utilitaire Draw de la suite bureautique OpenOffice.org 60 . C'est l'outil qui a ete utilise pour realiser le schema ci-dessous : Fenetre mattresse Espace de jeu (canevas) Canons : On doit pouvoir les orienter et les deplacer d volonte Indication du nom des joueurs Reglage de Tangle de tir Les obus doivent suivre une / trajectoire parabolique >>> Bourn ! Boun ! <<< Romeo ) (~12~L I 17 ] [ Juliette Fenetre fille : Choix des joueurs, Demarrage du jeu Fenetre fille : Documentation Fenetre fille : Niveau de difficulty, Ajoutd 'obstacles, etc. Chaque joueur tire d tour de role. Le nom de celui qui a la main apparaTt sur fond vert, I'autre sur fond rouge. Commande de tir Indication du score Bouton de sortie L'idee de depart est simple : deux joueurs s'affrontent au canon. Chacun doit ajuster son angle de tir pour tacher d'atteindre son adversaire, les obus decrivant une trajectoire balistique. 58 Voir page 16 : Recherche des erreurs et experimentation, et aussi page 216 : Fenetres avec menus 59 Nous n'hesitons pas a discuter ici le developpement d'un logiciel de jeu, parce qu'il s'agit d'un domaine directement accessible a tous, et dans lequel les objectifs concrets sont aisem*nt identifiables. II va de soi que les memes techniques de developpement peuvent s'appliquer a d'autres applications plus "serieuses". 60 II s'agit d'une suite bureautique complete, libre et gratuite, largement compatible avec MS-Office, disponible pour Linux, Windows, MacOS, Solaris ... Le present manuel a ete entierement redige avec son traitement de textes. Vous pouvez vous la procurer par telechargement depuis le site Web : http://www.openoffice.org Gerard Swinnen : Apprendre a programmer avec Python 227. L'emplacement des canons est defini au debut du jeu de maniere aleatoire (tout au mo ins en hauteur). Apres chaque tir, les canons se deplacent (afin d'accroitre l'interet du jeu, l'ajustement des tirs etant ainsi rendu plus difficile). Les coups au but sont comptabilises. Le dessin preliminaire que nous avons reproduit a la page precedente est l'une des formes que peut prendre votre travail ^analyse. Avant de commencer le developpement d'un projet de programmation, il vous faut en effet toujours vous efforcer d'etablir un cahier des charges detaille. Cette etude prealable est tres importante. La plupart des debutants commencent bien trop vite a ecrire de nombreuses lignes de code au depart d'une vague idee, en negligeant de rechercher la structure d' ensemble. Leur programmation risque alors de devenir chaotique, parce qu'ils devront de toute facon mettre en place cette structure tot ou tard. II s'apercevront alors bien souvent qu'il leur faut supprimer et re-ecrire des pans entiers d'un projet qu'ils ont concu d'une maniere trop monolithique et/ou malparametree. - Trop monolithique : cela signifie que Ton a neglige de decomposer un probleme complexe en plusieurs sous-problemes plus simples. Par exemple, on a imbrique plusieurs niveaux successifs d'instructions composees, au lieu de faire appel a des fonctions ou a des classes. • Mai parametree : cela signifie que Ton a traite seulement un cas particulier, au lieu d'envisager le cas general. Par exemple, on a donne a un objet graphique des dimensions fixes, au lieu de prevoir des variables pour permettre son redimensionnement. Vous devez done toujours commencer le developpement d'un projet par une phase d' analyse aussi fouillee que possible, et concretiser le resultat de cette analyse dans un ensemble de documents (schemas, plans, descriptions...) qui constitueront le cahier des charges. Pour les projets de grande envergure, il existe d'ailleurs des methodes d' analyse tres elaborees {UML, Merise...) que nous ne pouvons nous permettre de decrire ici car elles font l'objet de livres entiers. Cela etant dit, il faut malheureusem*nt admettre qu'il est tres difficile (et meme probablement impossible) de realiser des le depart l'analyse tout a fait complete d'un projet de programmation. C'est seulement lorsqu'il commence a fonctionner veritablement qu'un programme revele ses faiblesses. On constate alors qu'il reste des cas d'utilisation ou des contraintes qui n'avaient pas ete prevues au depart. D'autre part, un projet logiciel est pratiquement toujours destine a evoluer : il vous arrivera frequemment de devoir modifier le cahier des charges au cours du developpement lui- meme, pas necessairement parce que l'analyse initiale a ete mal faite, mais tout simplement parce que Ton souhaite encore ajouter des fonctionnalites supplementaires. En conclusion, tachez de toujours aborder un nouveau projet de programmation en respectant les deux consignes suivantes : • Decrivez votre projet en profondeur avant de commencer la redaction des premieres lignes de code, en vous efforcant de mettre en evidence les composants principaux et les relations qui les lient (pensez notamment a decrire les differents cas d'utilisation de votre programme) • Lorsque vous commencerez sa realisation effective, evitez de vous laisser entrainer a rediger de trop grands blocs d'instructions. Veillez au contraire a decouper votre application en un certain nombre de composants parametrables bien encapsules, de telle maniere que vous puissiez aisem*nt modifier l'un ou l'autre d'entre eux sans compromettre le fonctionnement des autres, et peut-etre meme les reutiliser dans differents contextes si le besoin s'en fait sentir. C'est pour satisfaire cette exigence que la programmation orientee objets est a ete inventee. Considerons par exemple l'ebauche dessinee a la page precedente. L'apprenti programmeur sera peut-etre tente de commencer la realisation de ce jeu en n'utilisant que la programmation procedurale seule (e'est-a-dire en omettant de definir de nouvelles classes). C'est d'ailleurs ainsi que nous avons procede nous-meme lors de notre premiere approche des interfaces graphiques, tout au long du chapitre 8. Cette fa?on de proceder ne se justifie cependant 228. Gerard Swinnen : Apprendre a programmer avec Python que pour de tout petit* programmes (des exercices ou des tests preliminaries). Lorsque Ton s'attaque a un projet d'une certaine importance, la complexity des problemes qui se presentent se revele rapidement trop importante, et il devient alors indispensable de fragmenter et de compartimenter. L'outil logiciel qui va permettre cette fragmentation est la classe. Nous pouvons peut-etre mieux comprendre son utilite en nous aidant d'une analogie : Tous les appareils electroniques sont constitutes d'un petit nombre de composants de base, a savoir des transistors, des diodes, des resistances, des condensateurs, etc. Les premiers ordinateurs ont ete construits directement a partir de ces composants. lis etaient volumineux, tres chers, et pourtant ils n'avaient que tres peu de fonctionnalites et tombaient frequemment en panne. On a alors developpe differentes techniques pour encapsuler dans un meme boitier un certain nombre de composants electroniques de base. Pour utiliser ces nouveaux circuits integres, il n'etait plus necessaire de connaitre leur contenu exact : seule importait leur fonction globale. Les premieres fonctions integrees etaient encore relativement simples : c'etaient par exemple des portes logiques, des bascules, etc. En combinant ces circuits entre eux, on obtenait des caracteristiques plus elaborees, telles que des registres ou des decodeurs, qui purent a leur tour etre integres, et ainsi de suite, jusqu'aux microprocesseurs actuels. Ceux-ci contiennent dorenavant plusieurs millions de composants, et pourtant leur fiabilite reste extremement elevee. En consequence, pour l'electronicien moderne qui veut construire par exemple un compteur binaire (circuit qui necessite un certain nombre de bascules), il est evidemment bien plus simple, plus rapide et plus sur de se servir de bascules integrees, plutot que de s'echiner a combiner sans erreur plusieurs centaines de transistors et de resistances. D'une maniere analogue, le programmeur moderne que vous etes peut beneficier du travail accumule par ses predecesseurs en utilisant la fonctionnalite integree dans les nombreuses bibliotheques de classes deja disponibles pour Python. Mieux encore, il peut aisem*nt creer lui- meme de nouvelles classes pour encapsuler les principaux composants de son application, particulierement ceux qui y apparaissent en plusieurs exemplaires. Proceder ainsi est plus simple, plus rapide et plus sur que de multiplier les blocs d'instructions similaires dans un corps de programme monolithique, de plus en plus volumineux et de mo ins en mo ins comprehensible. Examinons par exemple notre ebauche dessinee. Les composants les plus importants de ce jeu sont bien evidemment les petit* canons, qu'il faudra pouvoir dessiner a differents emplacements et dans differentes orientations, et dont il nous faudra au moins deux exemplaires. Plutot que de les dessiner morceau par morceau dans le canevas au fur et a mesure du deroulement du jeu, nous avons interet a les considerer comme des objets logiciels a part entiere, dotes de plusieurs proprietes ainsi que d'un certain comportement (ce que nous voulons exprimer par la est le fait qu'il devront etre dotes de divers mecanismes, que nous pourrons activer par programme a l'aide de methodes particulieres). II est done certainement judicieux de leur consacrer une classe specifique. 15.1.1 Prototypage d'une classe « Canon » En definissant une telle classe, nous gagnons sur plusieurs tableaux. Non seulement nous rassemblons ainsi tout le code correspondant au dessin et au fonctionnement du canon dans une meme « capsule », bien a l'ecart du reste du programme, mais de surcroit nous nous donnons la possibilite d'instancier aisem*nt un nombre quelconque de ces canons dans le jeu, ce qui nous ouvre des perspectives de developpements ulterieurs. Lorsqu'une premiere implementation de la classe Canon() aura ete construite et testee, il sera egalement possible de la perfectionner en la dotant de caracteristiques supplementaires, sans modifier (ou tres peu) son interface, e'est-a-dire en quelque sorte son « mode d'emploi » : a savoir Gerard Swinnen : Apprendre a programmer avec Python 229. les instructions necessaires pour l'instancier et l'utiliser dans des applications diverses. Entrons a present dans le vif du sujet. Le dessin de notre canon peut etre simplifie a l'extreme. Nous avons estime qu'il pouvait se resumer a un cercle combine avec un rectangle, celui-ci pouvant d'ailleurs etre lui-meme considere comme un simple segment de ligne droite particulierement epais. Si l'ensemble est rempli d'une couleur uniforme (en noir, par exemple), nous obtiendrons ainsi une sorte de petite bombarde suffisamment credible. Dans la suite du raisonnement, nous admettrons que la position du canon est en fait la position du centre du cercle (coordonnees x et y dans le dessin ci-contre). Ce point cle indique egalement l'axe de rotation de la buse du canon, ainsi que l'une des extremites de la ligne epaisse qui representera cette buse. Pour terminer notre dessin, il nous restera alors a determiner les coordonnees de l'autre extremite de cette ligne. Ces coordonnees peuvent etre calculees sans grande difficulty, a la condition de nous rememorer deux concepts fondamentaux de la trigonometrie (le sinus certainement bien connaitre : Dans un triangle rectangle, le rapport entre le cote oppose a un angle et I'hypotenuse du triangle est une propriete specifique de cet angle qu'on appelle sinus de I'angle. Le cosinus du meme angle est le rapport entre le cote adjacent a I'angle et I'hypotenuse. Ainsi, dans le schema ci-contre : sin a = — et cos a — — . h h Pour representer la buse de notre canon, en supposant que nous connaissions sa longueur 1 et Tangle de tir a , il nous faut done tracer un segment de ligne droite epaisse, a partir des coordonnees du centre du cercle (x et y), jusqu'a un autre point situe plus a droite et plus haut, l'ecart horizontal Ax etant egal a l.cos a , et l'ecart vertical Ay etant egal a l.sin a . En resumant tout ce qui precede, dessiner un canon au point x, y consistera simplement a : • tracer un cercle noir centre sur x, y • tracer une ligne noire epaisse depuis le point x, y jusqu'au point x + l.cos a, y + l.sin a. Nous pouvons a present commencer a envisager une ebauche de programmation correspondant a une classe « Canon ». II n'est pas encore question ici de programmer le jeu proprement dit. Nous voulons seulement verifier si l'analyse que nous avons faite jusqu'a present « tient la route », en realisant un premier prototype fonctionnel. Un prototype est un petit programme destine a experimenter une idee, que Ton se propose d'integrer ensuite dans une application plus vaste. Du fait de sa simplicity et de sa concision, Python se prete fort bien a l'elaboration de prototypes, et de nombreux programmeurs l'utilisent pour mettre au point divers composants logiciels qu'ils reprogrammeront eventuellement ensuite dans d'autres langages plus « lourds », tels que le C par exemple. Dans notre premier prototype, la classe Canon() ne comporte que deux methodes : un constructeur qui cree les elements de base du dessin, et une methode permettant de modifier celui-ci a volonte pour ajuster I'angle de tir (l'inclinaison de la buse). Comme nous l'avons souvent fait dans d'autres exemples, nous inclurons quelques lignes de code a la fin du script afin de pouvoir tester la classe tout de suite : et le cosinus) que vous devez 230. Gerard Swinnen : Apprendre a programmer avec Python 1 . from Tkinter import * 2. from math import pi, sin, cos 3. 4. class Canon: 5 """Petit canon graphique""" 6. def init (self, boss, x, y) : 7. self. boss = boss # reference du canevas 8. self.xl, self.yl = x, y # axe de rotation du canon 9. # dessiner la buse du canon, a 1 ' horizontale pour commencer : 10. self.lbu =50 # longueur de la buse 11. self.x2, self.y2 = x + self.lbu, y 12. self. buse = boss . create_line (self . xl, self.yl, self.x2, self.y2, 13. width =10) 14 . # dessiner ensuite le corps du canon par-dessus : 15. r = 15 # rayon du cercle 16. boss . create_oval (x-r, y-r, x+r, y+r, fill='blue', width =3) 17. 18. def orienter (self , angle): 19. "choisir 1 ' angle de tir du canon" 20. # rem : le parametre est recu en tant que chaine de car. 21. # il faut le traduire en nombre reel, puis convertir en radians : 22. self. angle = float (angle) *2*pi/360 23. self.x2 = self.xl + self .lbu*cos (self .angle) 24. self.y2 = self.yl - self . lbu*sin (self . angle) 25. self .boss . coords (self .buse, self.xl, self.yl, self.x2, self.y2) 26. 27. if name == ' main ' : 28. # Code pour tester sommairement la classe Canon : 29. f = Tk() 30. can = Canvas (f, width =250, height =250, bg =' ivory') 31. can .pack (padx =10, pady =10) 32. cl = Canon (can, 50, 200) 33. 34. si =Scale(f, label= ' hausse ' , from_=90, to=0, command=cl . orienter) 35. si .pack (side=LEFT, pady =5, padx =20) 36. si. set (25) # angle de tir initial 37. 38. f.mainloopO Commentaires : • Ligne 6 : Dans la liste des parametres qui devront etre transmis au constructeur lors de l'instanciation, nous prevoyons les coordonnees x et y, qui indiqueront l'emplacement du canon dans le canevas, mais egalement une reference au canevas lui-meme (la variable boss). Cette reference est indispensable : elle sera utilisee pour invoquer les methodes du canevas. Nous pourrions inclure aussi un parametre pour choisir un angle de tir initial, mais puisque nous avons l'intention d'implementer une methode specifique pour regler cette orientation, il sera plus judicieux de faire appel a celle-ci au moment voulu. • Lignes 7 et 8 : Ces references seront utilisees un peu partout dans les differentes methodes que nous allons developper dans la classe. II faut done en faire des attributs d'instance. • Lignes 9 a 16 : Nous dessinons la buse d'abord, et le corps du canon ensuite. Ainsi une partie de la buse reste cachee. Cela nous permet de colorer eventuellement le corps du canon. • Lignes 18 a 25 : Cette methode sera invoquee avec un argument « angle », lequel sera fourni en degres (comptes a partir de l'horizontale). S'il est produit a l'aide d'un widget tel que Entry ou Scale, il sera transmis sous la forme d'une chaine de caracteres, et nous devrons done le convertir d'abord en nombre reel avant de l'utiliser dans nos calculs (ceux-ci ont ete decrits a la page precedente). • Lignes 27 a 38 : Pour tester notre nouvelle classe, nous ferons usage d'un widget Scale. Pour definir la position initiale de son curseur, et done fixer Tangle de hausse initial du canon, nous Gerard Swinnen : Apprendre a programmer avec Python 231. devons faire appel a sa methode set() (ligne 36). 15.1.2 Ajout de methodes au prototype Notre prototype est fonctionnel, mais beaucoup trop rudimentaire. Nous devons a present le perfectionner pour lui ajouter la capacite de tirer des obus. Ceux-ci seront traites plutot comme des « boulets » : ce seront de simples petit* cercles que nous ferons partir de la bouche du canon avec une vitesse initiale d'orientation identique a celle de sa buse. Pour leur faire suivre une trajectoire realiste, nous devons a present nous replonger dans notre cours de physique : Comment un objet laisse a lui-meme evolue-t-il dans I'espace, si Von neglige les phenomenes secondaires tels que la resistance de Vair ? Ce probleme peut vous paraitre complexe, mais en realite sa resolution est tres simple : il vous suffit d'admettre que le boulet se deplace a la fois horizontalement et verticalement, et que ces deux mouvements simultanes sont tout a fait independants l'un de l'autre. Vous allez done etablir une boucle d'animation dans laquelle vous recalculez les nouvelles coordonnees x et y du boulet a intervalles de temps reguliers, en sachant que : • Le mouvement horizontal est uniforme. A chaque iteration, il vous suffit d'augmenter graduellement la coordonnee x du boulet, en lui ajoutant toujours un meme deplacement Ax. • Le mouvement vertical est uniformement accelere. Cela signifie simplement qu'a chaque iteration, vous devez ajouter a la coordonnee y un deplacement Ay qui augmente lui-meme graduellement, toujours de la meme quantite. Voyons cela dans le script : A) Pour commencer, il faut ajouter les lignes suivantes a la fin de la methode constructeur. Elles vont servir a creer l'objet « obus », et a preparer une variable d' instance qui servira d'interrupteur de l'animation. L'obus est cree au depart avec des dimensions minimales (un cercle d'un seul pixel) afin de rester presqu'invisible : # dessiner un obus (reduit a un simple point, avant animation) : self. obus =boss . create_oval (x, y, x, y, fill='red') self.anim =False # interrupteur d'animation # retrouver la largeur et la hauteur du canevas : self . xMax =int (boss . cget ( ' width ' ) ) self . yMax =int (boss . cget ( ' height ' ) ) Les deux dernieres lignes utilisent la methode cget() du widget « maitre » (le canevas, ici), afin de retrouver certaines de ses caracteristiques. Nous voulons en effet que notre classe Canon soit generaliste, e'est-a-dire reutilisable dans n'importe quel contexte, et nous ne pouvons done pas tabler a l'avance sur des dimensions particulieres pour le canevas dans lequel ce canon sera utilise. Note : Tkinter renvoie ces valeurs sous la forme de chaines de caracteres. II faut done les convertir dans un type numerique si nous voulons pouvoir les utiliser dans un calcul. B) Ensuite, nous devons ajouter deux nouvelles methodes : l'une pour declencher le tir, et l'autre pour gerer l'animation du boulet une fois que celui-ci aura ete lance : 232. Gerard Swinnen : Apprendre a programmer avec Python 1. def feu (self) : 2. "declencher le tir d'un obus" 3. if not self.anim: 4 . self . anim =True 5. # position de depart de l'obus (c'est la bouche du canon) : 6. self .boss . coords (self . obus, self.x2 -3, self.y2 -3, 7. self.x2 +3, self.y2 +3) 8. v =15 # vitesse initiale 9. # composantes verticale et horizontale de cette vitesse : 10. self.vy = -v *sin (self . angle) 11. self.vx = v *cos (self .angle) 12. self . animer_obus ( ) 13. 14. def animer_obus (self ) : 15. "animation de l'obus (trajectoire balistique) " 16. if self.anim: 17. self . boss .move (self . obus, int (self . vx) , int (self . vy) ) 18. c = self .boss . coords (self . obus) # coord, resultantes 19. xo, yo = c[0] +3, c[l] +3 # coord, du centre de l'obus 20. if yo > self.yMax or xo > self.xMax: 21. self.anim =False # arreter 1' animation 22. self.vy += .5 23. self .boss . after (30, self . animer_obus) Commentaires : • Lignes 1 a 4 : Cette methode sera invoquee par appui sur un bouton. Elle declenche le mouvement de l'obus, et attribue une valeur « vraie » a notre « interrupteur d'animation » (la variable self.anim : voir ci-apres). II faut cependant nous assurer que pendant toute la duree de cette animation, un nouvel appui sur le bouton ne puisse pas activer d'autres boucles d'animation parasites. C'est le role du test effectue a la ligne 3 : le bloc d'instruction qui suit ne peut s'executer que si la variable self.anim possede la valeur « faux », ce qui signifie que l'animation n'a pas encore commence. • Lignes 5 a 7 : Le canevas Tkinter dispose de deux methodes pour deplacer les objets graphiques : La methode coords() effectue un positionnement absolu ; il faut cependant lui fournir toutes les coordonnees de l'objet (comme si on le redessinait). La methode move() , utilisee a la ligne 17, provoque quant a elle un deplacement relatif ; elle s'utilise avec deux arguments seulement : les composantes horizontale et verticale du deplacement souhaite. • Lignes 8 a 12 : La vitesse initiale de l'obus est choisie a la ligne 8. Comme nous l'avons explique a la page precedente, le mouvement du boulet est la resultante d'un mouvement horizontal et d'un mouvement vertical. Nous connaissons la valeur de la vitesse initiale ainsi que son inclinaison (c'est- a-dire Tangle de tir). Pour determiner les composantes horizontale et verticale de cette vitesse, il nous suffit d'utiliser des relations trigonometriques tout a fait similaires a que celles que nous avons deja exploitees pour dessiner la buse du canon. Le signe - utilise a la ligne 9 provient du fait que les coordonnees verticales se comptent de haut en bas. La ligne 12 active l'animation proprement dite. • Lignes 14 a 23: Cette procedure se re-appelle elle-meme toutes les 30 millisecondes par l'intermediaire de la methode after() invoquee a la ligne 23. Cela continue aussi longtemps que la variable self.anim (notre « interrupteur d'animation ») reste « vraie », condition qui changera lorsque les coordonnees de l'obus sortiront des limites imposees (test de la ligne 20). • Lignes 18, 19 : Pour retrouver ces coordonnees apres chaque deplacement, on fait appel encore une fois a la methode coords() du canevas : utilisee avec la reference d'un objet graphique comme unique argument, elle renvoie ses quatre coordonnees dans un tuple. • Lignes 17 et22 : La coordonnee horizontale de l'obus augmente toujours de la meme quantite Gerard Swinnen : Apprendre a programmer avec Python 233. (mouvement uniforme), tandis que la coordonnee verticale augmente d'une quantite qui est elle- meme augmentee a chaque fois a la ligne 24 (mouvement uniformement accelere). Le resultat est une trajectoire parabolique. Rappel : l'operateur += permet d'incrementer une variable : « a += 3 » equivaut a « a = a + 3 ». C) II reste enfin a ajouter un bouton declencheur dans la fenetre principale. Une ligne telle que la suivante (a inserer dans le code de test) fera parfaitement l'affaire : Button(f, text='Feu !', command =cl . feu) .pack (side=LEFT) 15.1.3 Developpement de I'application Disposant desormais d'une classe d'objets « canon » assez bien degrossie, nous pouvons a present envisager l'elaboration de I'application proprement dite. Et puisque nous sommes decides a exploiter la methodologie de la programmation orientee objet, nous devons concevoir cette application comme un ensemble d'objets qui interagissent par Vintermediaire de leurs methodes. Plusieurs de ces objets proviendront de classes preexistantes, bien entendu : ainsi le canevas, les boutons, etc. Mais nous avons vu dans les pages precedentes que nous avons interet a regrouper des ensembles bien delimites de ces objets basiques dans de nouvelles classes, chaque fois que nous pouvons identifier pour ces ensembles une fonctionnalite particuliere. C'etait le cas par exemple pour cet ensemble de cercles et de lignes mobiles que nous avons decide d'appeler « canon ». Pouvons-nous encore distinguer dans notre projet initial d'autres composants qui meriteraient d'etre encapsules dans des nouvelles classes ? Certainement. II y a par exemple le pupitre de controle que nous voulons associer a chaque canon : nous pouvons y rassembler le dispositif de reglage de la hausse (Tangle de tir), le bouton de mise a feu, le score realise, et peut-etre d'autres indications encore, comme le nom du joueur, par exemple. II est d'autant plus interessant de lui consacrer une classe particuliere, que nous savons d'emblee qu'il nous en faudra deux instances. II y a aussi I'application elle-meme, bien sur. En l'encapsulant dans une classe, nous en ferons notre objet principal, celui qui dirigera tous les autres. Veuillez a present analyser le script ci-dessous. Vous y retrouverez la classe Canon() encore davantage developpee : nous y avons ajoute quelques attributs et trois methodes supplementaires, 234. Gerard Swinnen : Apprendre a programmer avec Python afin de pouvoir gerer les emplacements du canon lui-meme, ainsi que les coups au but. La classe Application() remplace desormais le code de test des prototypes precedents. Nous y instancions deux objets Canon(), et deux objets de la nouvelle classe Pupitre(), que nous placons dans des dictionnaires en prevision de developpements ulterieurs (nous pouvons en effet imaginer d'augmenter le nombre de canons et done de pupitres). Le jeu est a present fonctionnel : les canons se deplacent apres chaque tir, et les coups au but sont comptabilises. 1 . from Tkinter import * 2. from math import sin, cos, pi 3 . from random import randrange 4. 5. class Canon: 6. """Petit canon graphique" " " 7. def init (self, boss, id, x, y, sens, coul) : 8. self. boss = boss # ref. du canevas 9. self.appli = boss. master # ref. de la fenetre d' application 10. self. id = id # identifiant du canon (chaine) 11. self. coul = coul # couleur associee au canon 12. self.xl, self.yl = x, y # axe de rotation du canon 13. self. sens = sens # sens de tir (-1: gauche, +l:droite) 14. self.lbu =30 # longueur de la buse 15. self. angle =0 # hausse par defaut (angle de tir) 16. # retrouver la largeur et la hauteur du canevas : 17. self.xMax = int (boss . cget (' width ') ) 18. self.yMax = int (boss . cget (' height ') ) 19. # dessiner la buse du canon (horizontale) : 20. self.x2, self.y2 = x + self.lbu * sens, y 21. self. buse = boss . create_line (self . xl, self.yl, 22. self.x2, self.y2, width =10) 23. # dessiner le corps du canon (cercle de couleur) : 24. self.rc =15 # rayon du cercle 25. self. corps = boss . create_oval (x -self.rc, y -self.rc, x +self.rc, 26. y +self.rc, fill =coul) 27. # pre-dessiner un obus cache (point en dehors du canevas) 28. self. obus = boss . create_oval (-10, -10, -10, -10, fill='red') 29. self.anim = False # indicateurs d' animation 30. self.explo = False # et d' explosion 31. 32. def orienter (self , angle): 33. "regler la hausse du canon" 34. # rem: le parametre est recu en tant que chaine. 35. #11 faut done le traduire en reel, puis le convertir en radians : 36. self. angle = float (angle) *pi/180 37. # rem: utiliser la methode coords de preference avec des entiers : 38. self.x2 = int (self.xl + self.lbu * cos (self . angle) * self. sens) 39. self.y2 = int (self.yl - self.lbu * sin (self . angle) ) 40. self .boss . coords (self .buse, self.xl, self.yl, self.x2, self.y2) 41. 42. def deplacer (self , x, y) : 43. "amener le canon dans une nouvelle position x, y" 44. dx, dy = x -self.xl, y -self.yl # valeur du deplacement 45. self .boss .move (self .buse, dx, dy) 46. self .boss .move (self . corps, dx, dy) 47. self.xl += dx 48. self.yl += dy 49. self.x2 += dx 50. self.y2 += dy 51. 52. def feu (self) : 53. "tir d'un obus - seulement si le precedent a fini son vol" 54. if not (self.anim or self.explo) : 55. self.anim =True 56. # recuperer la description de tous les canons presents : 57. self. guns = self . appli . dictionnaireCanons ( ) Gerard Swinnen : Apprendre a programmer avec Python 235. 58. # position de depart de l'obus (c'est la bouche du canon) : 59. self .boss . coords (self . obus, self.x2 -3, self.y2 -3, 60. self.x2 +3, self.y2 +3) 61. v = 17 # vitesse initiale 62. # composantes verticale et horizontale de cette vitesse : 63. self.vy = -v *sin (self . angle) 64. self.vx = v *cos (self . angle) *self.sens 65. self .animer_obus () 66. return True # => signaler que le coup est parti 67. else: 68. return False # => le coup n'a pas pu etre tire 69. 70. def animer_obus (self ) : 71. "animer l'obus (trajectoire balistique) " 72. if self.anim: 73. self .boss .move (self . obus, int (self . vx) , int (self . vy) ) 74. c = self .boss . coords (self . obus) # coord, resultantes 75. xo, yo = c[0] +3, c[l] +3 # coord, du centre de l'obus 76. self . test_obstacle (xo, yo) # a-t-on atteint un obstacle ? 77. self.vy += . 4 # acceleration verticale 78. self .boss . after (20, self . animer_obus) 79. else: 80 . # animation terminee - cacher 1 ' obus et deplacer les canons : 81. self . f in_animation ( ) 82. 83. def test_obstacle (self , xo, yo) : 84. "evaluer si l'obus a atteint une cible ou les limites du jeu" 85. if yo >self.yMax or xo <0 or xo >self.xMax: 86. self.anim =False 87 . return 88. # analyser le dictionnaire des canons pour voir si les coord. 89. # de l'un d'entre eux sont proches de celles de l'obus : 90. for id in self. guns: # id = clef dans dictionn. 91. gun = self . guns [id] # valeur correspondante 92. if xo < gun.xl +self.rc and xo > gun.xl -self.rc \ 93. and yo < gun.yl +self.rc and yo > gun.yl -self.rc : 94. self.anim =False 95. # dessiner 1' explosion de l'obus (cercle jaune) : 96. self.explo = self .boss . create_oval (xo -12, yo -12, 97. xo +12, yo +12, fill ='yellow', width =0) 98. self. hit =id # reference de la cible touchee 99. self .boss . after (150, self . fin_explosion) 100. break 101. 102. def f in_explosion (self ) : 103. "ef facer 1' explosion ; re-initaliser l'obus ; gerer le score" 104. self .boss . delete (self . explo) # ef facer 1' explosion 105. self.explo =False # autoriser un nouveau tir 106. # signaler le succes a la fenetre maitresse : 107. self . appli . goal (self . id, self. hit) 108. 109. def f in_animation (self ) : 110. "actions a accomplir lorsque l'obus a termine sa trajectoire" 111. self . appli . disperser () # deplacer les canons 112. # cacher l'obus (en l'expediant hors du canevas) : 113. self .boss. coords (self .obus, -10, -10, -10, -10) 114 . 115. 116. class Pupitre (Frame) : 117. """Pupitre de pointage associe a un canon""" 118. def init (self, boss, canon): 119. Frame. init (self, bd =3, relief =GROOVE) 120. self. score =0 121. self. appli =boss # ref. de 1 ' application 122. self. canon =canon # ref. du canon associe 123. # Systeme de reglage de 1' angle de tir : 124. self.regl =Scale(self, from_ =75, to =-15, troughcolor=canon . coul, 125. command =self . orienter) 126. self . regl . set (45) # angle initial de tir 127. self . regl .pack (side =LEFT) 236. Gerard Swinnen : Apprendre a programmer avec Python 128. # Etiquette d ' identification du canon : 129. Label(self, text =canon . id) .pack (side =TOP, anchor =W, pady =5) 130. # Bouton de tir : 131. self.bTir =Button (self , text ='Feu !', command =self.tirer) 132. self .bTir. pack (side =BOTTOM, padx =5, pady =5) 133. Label(self, text ="points" ) .pack () 134. self. points =Label (self , text=' 0 ' , bg =' white') 135. self . points. pack () 136. # positionner a gauche ou a droite suivant le sens du canon : 137. if canon. sens == -1: 138. self .pack (padx =5, pady =5, side =RIGHT) 139. else: 140. self .pack (padx =5, pady =5, side =LEFT) 141. 142. def tirer (self) : 143. "declencher le tir du canon associe" 144. self . canon . feu ( ) 145. 146. def orienter (self , angle): 147. "ajuster la hausse du canon associe" 148. self . canon . orienter (angle) 149. 150. def attribuerPoint (self , p) : 151. "incrementer ou decrementer le score, de

points" 152. self. score += p 153. self .points. config (text = ' %s ' % self. score) 154. 155. class Application (Frame) : 156. ' ' 'Fenetre principale de 1 ' application 11 ' 157. def init (self): 158. Frame. init (self) 159. self .master .title (' »»> Bourn ! Bourn ! ««< ' ) 160. self. pack () 161. self.jeu = Canvas (self, width =400, height =250, bg =' ivory ' , 162. ~ bd =3, relief = SUNKEN) 163. self . jeu .pack (padx =8, pady =8, side =TOP) 164. 165. self. guns ={} # dictionnaire des canons presents 166. self.pupi = {} # dictionnaire des pupitres presents 167. # Instanciation de 2 'objets canons (+1, -1 = sens opposes) : 168. self .guns ["Billy"] = Canon (self . jeu, "Billy", 30, 200, 1, "red") 169. self .guns ["Linus"] = Canon (self . jeu, "Linus", 370,200,-1, "blue") 170. # Instanciation de 2 pupitres de pointage associes a ces canons : 171. self .pupi[ "Billy"] = Pupitre (self , self . guns [ "Billy" ] ) 172. self .pupi[ "Linus"] = Pupitre (self , self . guns [ "Linus" ] ) 173. 174. def disperser (self ) : 175. "deplacer aleatoirement les canons" 176. for id in self. guns: 177. gun =self . guns [id] 178. # positionner a gauche ou a droite, suivant sens du canon : 17 9. if gun. sens == -1 : 180. x = randrange (320, 380) 181. else: 182. x = randrange (20, 80) 183. # deplacement proprement dit : 184. gun . deplacer (x, randrange (150, 240) ) 185. 186. def goal(self, i, j) : 187. "le canon signale qu'il a atteint 1 ' adversaire " 188. if i != j: 189. self .pupi[i] . attribuerPoint (1) 190. else: 191. self .pupi[i] . attribuerPoint (-1) 192. 193. def dictionnaireCanons (self ) : 194. "renvoyer le dictionnaire decrivant les canons presents" 195. return self. guns 196. 197. if name == ' main ' : Gerard Swinnen : Apprendre a programmer avec Python 237. 198. Application () .mainloop () Commentaires : • Ligne 7 : Par rapport au prototype, trois parametres ont ete ajoutes a la methode constructeur. Le parametre id nous permet d'identifier chaque instance de la classe Canon() a l'aide d'un nom quelconque. Le parametre sens indique s'il s'agit d'un canon qui tire vers la droite (sens = 1) ou vers la gauche (sens = -1). Le parametre coul specifie la couleur associee au canon. • Ligne 9 : II faut savoir que tous les widgets Tkinter possedent un attribut master qui contient la reference leur widget maitre eventuel (leur « contenant »). Cette reference est done pour nous celle de l'application principale. (Nous avons implements nous-memes une technique similaire pour referencer le canevas, a l'aide de l'attribut boss). • Lignes 42 a 50 : Cette methode permet d'amener le canon dans un nouvel emplacement. Elle servira a repositionner les canons au hasard apres chaque tir, ce qui augmente l'interet du jeu. Note : l'operateur += permet d'incrementer une variable : « a += 3 » equivaut a « a = a + 3 ». • Lignes 56, 57 : Nous essayons de construire notre classe canon de telle maniere qu'elle puisse etre reutilisee dans des projets plus vastes, impliquant un nombre quelconque d'objets canons qui pourront apparaitre et disparaitre au fil des combats. Dans cette perspective, il faut que nous puissions disposer d'une description de tous les canons presents, avant chaque tir, de maniere a pouvoir determiner si une cible a ete touchee ou non. Cette description est geree par l'application principale, dans un dictionnaire, dont on peut lui demander une copie par l'intermediaire de sa methode dictionnaireCanons(). • Lignes 66 a 68 : Dans cette meme perspective generaliste, il peut etre utile d'informer eventuellement le programme appelant que le coup a effectivement ete tire ou non. • Ligne 76 : L'animation de l'obus est desormais traitee par deux methodes complementaires. Afin de clarifier le code, nous avons place dans une methode distincte les instructions servant a determiner si une cible a ete atteinte (methode test_obstacle()). • Lignes 79 a 8 1 : Nous avons vu precedemment que Ton interrompt l'animation de l'obus en attribuant une valeur « fausse » a la variable self.anim. La methode animer_obus() cesse alors de boucler et execute le code de la ligne 8 1 . • Lignes 83 a 100 : Cette methode evalue si les coordonnees actuelles de l'obus sortent des limites de la fenetre, ou encore si elles s'approchent de celles d'un autre canon. Dans les deux cas, l'interrupteur d' animation est actionne, mais dans le second, on dessine une « explosion » jaune, et la reference du canon touche est memorisee. La methode annexe fin_explosion() est invoquee apres un court laps de temps pour terminer le travail, e'est-a-dire effacer le cercle d'explosion et envoyer un message signalant le coup au but a la fenetre maitresse. • Lignes 115 a 153 : La classe Pupitre() definit un nouveau widget par derivation de la classe Frame(), selon une technique qui doit desormais vous etre devenue familiere. Ce nouveau widget regroupe les commandes de hausse et de tir, ainsi que l'afficheur de points associes a un canon bien determine. La correspondance visuelle entre les deux est assuree par l'adoption d'une couleur commune. Les methodes tirer() et orienter() communiquent avec l'objet Canon() associe, par l'intermediaire des methodes de celui-ci. • Lignes 155 a 172 : La fenetre d'application est elle aussi un widget derive de Frame(). Son constructeur instancie les deux canons et leurs pupitres de pointage, en placant ces objets dans les deux dictionnaires self.guns et self.pupi. Cela permet d'effectuer ensuite divers traitements systematiques sur chacun d'eux (comme par exemple a la methode suivante). En procedant ainsi, on se reserve en outre la possibility d'augmenter sans effort le nombre de ces canons si necessaire, dans les developpements ulterieurs du programme. 238. Gerard Swinnen : Apprendre a programmer avec Python Lignes 174 a 184 : Cette methode est invoquee apres chaque tir pour deplacer aleatoirement les deux canons, ce qui augmente la difficulte du jeu. 15.1.4 Developpements complementaires Tel qu'il vient d'etre decrit, notre programme correspond deja plus ou mo ins au cahier des charges initial, mais il est evident que nous pouvons continuer a le perfectionner. A) Nous devrions par exemple mieux le parametrer. Qu'est-ce a dire ? Dans sa forme actuelle, notre jeu comporte un canevas de taille predeterminee (400 x 250 pixels, voir ligne 161). Si nous voulons modifier ces valeurs, nous devons veiller a modifier aussi les autres lignes du script ou ces dimensions interviennent (comme par exemple aux lignes 168-169, ou 179-184). De telles lignes interdependantes risquent de devenir nombreuses si nous ajoutons encore d'autres fonctionnalites. II serait done plus judicieux de dimensionner le canevas a l'aide de variables, dont la valeur serait definie en un seul endroit. Ces variables seraient ensuite exploitees dans toutes les lignes d'instructions ou les dimensions du canevas interviennent. Nous avons deja effectue une partie de ce travail : dans la classe Canon(), en effet, les dimensions du canevas sont recuperees a l'aide dune methode predefinie (voir lignes 17-18), et placees dans des attributs d'instance qui peuvent etre utilises partout dans la classe. B) Apres chaque tir, nous provoquons un deplacement aleatoire des canons, en redefinissant leurs coordonnees au hasard. II serait probablement plus realiste de provoquer de veritables deplacements relatifs, plutot que de redefinir au hasard des positions absolues. Pour ce faire, il suffit de retravailler la methode deplacer() de la classe Canon(). En fait, il serait encore plus interessant de faire en sorte que cette methode puisse produire a volonte, aussi bien un deplacement relatif qu'un positionnement absolu, en fonction d'une valeur transmise en argument. C) Le systeme de commande des tirs devrait etre ameliore : puisque nous ne disposons que d'une seule souris, il faut demander aux joueurs de tirer a tour de role, et nous n'avons mis en place aucun mecanisme pour les forcer a le faire. Une meilleure approche consisterait a prevoir des commandes de hausse et de tir utilisant certaines touches du clavier, qui soient distinctes pour les deux joueurs. 59 Romeo J points -5 ! Juliette points Feu ! Virginie points -3 Feu ! I 'J Paul points Feu ! D) Mais le developpement le plus interessant pour notre programme serait certainement d'en faire une application reseau. Le jeu serait alors installe sur plusieurs machines communicantes, chaque joueur ayant le controle d'un seul canon. II serait d'ailleurs encore plus attrayant de Gerard Swinnen : Apprendre a programmer avec Python 239. permettre la mise en oeuvre de plus de deux canons, de maniere a autoriser des combats impliquant davantage de joueurs. Ce type de developpement suppose cependant que nous ayons appris a maitriser au prealable deux domaines de programmation qui debordent un peu le cadre de ce cours : • la technique des sockets, qui permet d'etablir une communication entre deux ordinateurs ; • la technique des threads, qui permet a un meme programme d'effectuer plusieurs taches simultanement (cela nous sera necessaire, si nous voulons construire une application capable de communiquer en meme temps avec plusieurs partenaires). Ces matieres ne font pas strictement partie des objectifs que nous nous sommes fixes pour ce cours, et leur leur traitement necessite a lui seul un chapitre entier. Nous n'aborderons done pas cette question ici. Que ceux que le sujet interesse se rassurent cependant : ce chapitre existe, mais sous la forme d'un complement a la fin du livre (chapitre 1 8) : vous y trouverez la version reseau de notre jeu de bombardes. En attendant, voyons tout de meme comment nous pouvons encore progresser, en apportant a notre projet quelques ameliorations qui en feront un jeu pour 4 joueurs. Nous nous efforcerons aussi de mettre en place une programmation bien compartimentee, de maniere a ce que les methodes de nos classes soient reutilisables dans une large mesure. Nous allons voir au passage comment cette evolution peut se faire sans modifier le code existant, en utilisant I'heritage pour produire de nouvelles classes a partir de celles qui sont deja ecrites. Commencons par sauvegarder notre ouvrage precedent dans un fichier, dont nous admettrons pour la suite de ce texte que le nom est : canon03.py. Nous disposons ainsi d'un module Python, que nous pouvons importer dans un nouveau script a l'aide d'une seule ligne d'instruction. En exploitant cette technique, nous continuons a perfectionner notre application, en ne conservant sous les yeux que les nouveautes : 1 . from Tkinter import * 2. from math import sin, cos, pi 3 . from random import randrange 4 . import canon03 5. 6. class Canon (canon03 . Canon) : 7. """Canon ameliore""" 8. def init (self, boss, id, x, y, sens, coul) : 9. canon03. Canon. init (self, boss, id, x, y, sens, coul) 10. 11. def deplacer (self , x, y, rel =False) : 12. "deplacement, relatif si est vrai, absolu si est faux" 13. if rel: 14 . dx, dy = x, y 15. else: 16. dx, dy = x -self.xl, y -self.yl 17 . # limites horizontales : 18. if self. sens ==1: 19. xa, xb = 20, int (self . xMax *.33) 20. else: 21. xa, xb = int (self . xMax *.66), self.xMax -20 22 . # ne deplacer que dans ces limites : 23. if self.xl +dx < xa: 24. dx = xa -self.xl 25. elif self.xl +dx > xb: 26. dx = xb -self.xl 27 . # limites verticales : 28. ya, yb = int (self . yMax *.4), self.yMax -20 29. # ne deplacer que dans ces limites : 30. if self.yl +dy < ya: 31. dy = ya -self.yl 240. Gerard Swinnen : Apprendre a programmer avec Python 32. elif self.yl +dy > yb: 33. dy = yb -self.yl 34 . # deplacement de la buse et du corps du canon : 35. self .boss .move (self .buse, dx, dy) 36. self .boss .move (self . corps, dx, dy) 37. # renvoyer les nouvelles coord, au programme appelant : 38. self .xl += dx 39. self.yl += dy 40. self.x2 += dx 41. self.y2 += dy 42. return self.xl, self.yl 43. 44. def f in_animation (self ) : 45. "actions a accomplir lorsque 1 ' obus a termine sa trajectoire" 46. # deplacer le canon qui vient de tirer : 47 . self . appli . depl_aleat_canon (self . id) 48. # cacher 1 ' obus (en l'expediant hors du canevas) : 49. self .boss . coords (self . obus, -10, -10, -10, -10) 50. 51. def ef facer (self ) : 52. "faire disparaitre le canon du canevas" 53. self .boss .delete (self .buse) 54. self . boss . delete (self . corps) 55. self .boss . delete (self . obus) 56. 57. class AppBombardes (Frame) : 58. ' ' 'Fenetre principale de 1 ' application 11 ' 59. def init (self, larg_c, haut_c) : 60. Frame. init (self) 61. self. pack () 62. self.xm, self.ym = larg_c, haut_c 63. self.jeu = Canvas (self, width =self.xm, height =self.ym, 64. " bg =' ivory', bd =3, relief =SUNKEN) 65. self . jeu .pack (padx =4, pady =4, side =TOP) 66. 67. self. guns ={} # dictionnaire des canons presents 68. self.pupi ={} # dictionnaire des pupitres presents 69. self . specif icites () # objets differents dans classes derivees 70. 71. def specif icites (self ) : 72. "instanciation des canons et des pupitres de pointage" 73. self .master, title (' «< Jeu des bombardes »>') 74. id_list =[ ("Paul", "red") , ("Romeo", "cyan") , 75. ("Virginie", "orange") , ("Juliette", "blue") ] 76. s = False 77. for id, coul in id_list: 78. if s: 79. sens =1 80. else: 81. sens =-1 82. x, y = self . coord_aleat (sens) 83. self . guns [id] = Canon (self . jeu, id, x, y, sens, coul) 84. self .pupi [id] = canon03 . Pupitre (self , self . guns [id] ) 85 . s = not s # changer de cote a chaque iteration 86. 87. def depl_aleat_canon (self , id): 88. "deplacer aleatoirement le canon " 89. gun =self . guns [id] 90. dx, dy = randrange (-60 , 61), randrange (-60 , 61) 91. # deplacement (avec recuperation des nouvelles coordonnees) : 92. x, y = gun. deplacer (dx, dy, True) 93. return x, y 94. 95. def coord_aleat (self , s) : 96. "coordonnees aleatoires, a gauche (s =1) ou a droite (s =-1)" 97. y =randrange (int (self . ym /2) , self.ym -20) 98. if s == -1: 99. x =randrange (int (self .xm *.7), self.xm -20) 100. else: 101. x =randrange (20, int (self.xm *.3)) Gerard Swinnen : Apprendre a programmer avec Python 241. 102 . return x, y 103. 104. def goal (self, i, j) : 105. "le canon n°i signale qu'il a atteint 1 ' adversaire n°j" 106. # de quel camp font-ils partie chacun ? 107. ti, tj = self .guns [i] . sens, self . guns [ j ]. sens 108. if ti != tj : # ils sont de sens opposes : 109. p = 1 # on gagne 1 point 110. else: # ils sont dans le meme sens : 111. p = -2 # on a touche un allie ! ! 112. self.pupi[i] . attribuerPoint (p) 113. # celui qui est touche perd de toute facon un point : 114. self.pupi[j] . attribuerPoint (-1) 115. 116. def dictionnaireCanons (self ) : 117. "renvoyer le dictionnaire decrivant les canons presents" 118. return self. guns 119. 120. if name == ' main ' : 121. AppBombardes (650, 300) .mainloopO Commentaires : • Ligne 6 : La forme d'importation utilisee a la ligne 4 nous permet de redefinir une nouvelle classe Canon() derivee de la precedente, tout en lui conservant le meme nom. De cette maniere, les portions de code qui utilisent cette classe ne devront pas etre modifiees (Cela n'aurait pas ete possible si nous avions utilise par exemple : « from canon03 import * »). • Lignes 1 1 a 16 : La methode definie ici porte le meme nom qu'une methode de la classe parente. Elle va done remplacer celle-ci dans la nouvelle classe (On pourra dire egalement que la methode deplacer() a ete surchargee). Lorsque Ton realise ce genre de modification, on s'efforce en general de faire en sorte que la nouvelle methode effectue le meme travail que l'ancienne quand elle est invoquee de la meme facon que l'etait cette derniere. On s'assure ainsi que les applications qui utilisaient la classe parente pourront aussi utiliser la classe fille, sans devoir etre elles-memes modifiees. Nous obtenons ce resultat en ajoutant un ou plusieurs parametres, dont les valeurs par defaut forceront l'ancien comportement. Ainsi, lorsque Ton ne fournit aucun argument pour le parametre rel, les parametres x et y sont utilises comme des coordonnees absolues (ancien comportement de la methode). Par contre, si Ton fournit pour rel un argument « vrai », alors les parametres x et y sont traites comme des deplacements relatifs (nouveau comportement). • Lignes 17 a 33 : Les deplacements demandes seront produits aleatoirement. II nous faut done prevoir un systeme de barrieres logicielles, afin d'eviter que l'objet ainsi deplace ne sorte du canevas. • Ligne 42 : Nous renvoyons les coordonnees resultantes au programme appelant. II se peut en effet que celui-ci commande un deplacement du canon sans connaitre sa position initiale. • Lignes 44 a 49 : II s'agit encore une fois de surcharger une methode qui existait dans la classe parente, de maniere a obtenir un comportement different : apres chaque tir, desormais on ne disperse plus tous les canons presents, mais seulement celui qui vient de tirer. • Lignes 51 a 55: Methode ajoutee en prevision d'applications qui souhaiteraient installer ou retirer des canons au fil du deroulement du jeu. • Lignes 57 et suivantes : Cette nouvelle classe est concue des le depart de maniere telle qu'elle puisse aisem*nt etre derivee. C'est la raison pour laquelle nous avons fragmente son constructeur en deux parties : La methode init () contient le code commun a tous les objets, aussi bien ceux qui seront instancies a partir de cette classe que ceux qui seront instancies a partir d'une classe derivee eventuelle. La methode specificitesQ contient des portions de code plus 242. Gerard Swinnen : Apprendre a programmer avec Python specifiques : cette methode est clairement destinee a etre surchargee dans les classes derivees eventuelles. 15.2 Jeu de Ping Dans les pages qui suivent, vous trouverez le script correspondant a un petit programme complet. Ce programme vous est fourni a titre d'exemple de ce que vous pouvez envisager de developper vous-meme comme projet personnel de synthese. II vous montre encore une fois comment vous pouvez utiliser plusieurs classes afin de construire un script bien structure. 15.2.1 Principe Le «jeu» mis en oeuvre ici est plutot une sorte d'exercice mathematique. II se joue sur un panneau ou est represents un quadrillage de dimensions variables, dont toutes les cases sont occupees par des pions. Ces pions possedent chacun une face blanche et une face noire (comme les pions du jeu Othello/Reversi), et au debut de l'exercice ils presentent tous leur face blanche par-dessus. Lorsque Ton clique sur un pion a l'aide de la souris, les 8 pions adjacents se retournent. Le jeu consiste alors a essayer de retourner tous les pions, en cliquant sur certains d'entre eux. L'exercice est tres facile avec une grille de 2 x 2 cases (il suffit de cliquer sur chacun des 4 pions). II devient plus difficile avec des grilles plus grandes, et est meme tout a fait impossible avec certaines d'entre elles. A vous de determiner lesquelles ! (Ne negligez pas d'etudier le cas des grilles 1 x n). Fichier Aide 1 X ■ X [TlHfxl □•□HGP ■ * i r Nombre de lignes : 4 lii Yincipe du jeu \ propos ... 1 i i Nombre de colonnes : 7 1 -J-l i khz ••^ Note : Vous trouverez la discussion complete du jeu de Ping, sa theorie et ses extensions, dans la revue « Pour la science » n° 298 - Aout 2002, pages 98 a 102. Gerard Swinnen : Apprendre a programmer avec Python 243. 15.2.2 Programmation Lorsque vous developpez un projet logiciel, veillez toujours a faire l'effort de decrire votre demarche le plus clairement possible. Commencez par etablir un cahier des charges detaille, et ne negligez pas de commenter ensuite tres soigneusem*nt votre code, au fur et a mesure de son elaboration (et non apres coup !). En procedant ainsi, vous vous forcez vous-meme a exprimer ce que vous souhaitez que la machine fasse, ce qui vous aide a analyser les problemes et a structurer convenablement votre code. Cahier des charges du logiciel a developper • L'application sera construite sur la base d'une fenetre principale comportant le panneau de jeu et une barre de menus. • L'ensemble devra etre extensible a volonte par l'utilisateur, les cases du panneau devant cependant rester carrees. • Les options du menu permettront de : • choisir les dimensions de la grille (en nombre de cases) • reinitialiser le jeu (c'est-a-dire disposer tous les pions avec leur face blanche au-dessus) • afficher le principe du jeu dans une fenetre d'aide • terminer.(fermer l'application) • La programmation fera appel a trois classes : • une classe principale • une classe pour la barre de menus • une classe pour le panneau de jeu • Le panneau de jeu sera dessine dans un canevas, lui-meme installe dans un cadre (frame). En fonction des redimensionnements operes par l'utilisateur, le cadre occupera a chaque fois toute la place disponible : il se presente done au programmeur comme un rectangle quelconque, dont les dimensions doivent servir de base au calcul des dimensions de la grille a dessiner. • Puisque les cases de cette grille doivent rester carrees, il est facile de commencer par calculer leur taille maximale, puis d' etablir les dimensions du canevas en fonction de celle-ci. • Gestion du clic de souris : on liera au canevas une methode-gestionnaire pour l'evenement . Les coordonnees de l'evenement serviront a determiner dans quelle case de la grille (n° de ligne et n° de colonne) le clic a ete effectue, quelles que soient les dimensions de cette grille. Dans les 8 cases adjacentes, les pions presents seront alors « retournes » (echange des couleurs noire et blanche). 244. Gerard Swinnen : Apprendre a programmer avec Python ########################################### # Jeu de ping # # References : Voir article de la revue # # ##### fileMenu = Menubutton (self , text ='Fichier') fileMenu. pack (side =LEFT, padx =5) mel = Menu (fileMenu) mel . add command (label =' Options', underline =0, command = boss . options) mel. add command (label =' Restart', underline =0, command = boss. reset) mel . add_command (label =' Terminer', underline =0, command = boss. quit) fileMenu . configure (menu = mel) ##### Menu ##### helpMenu = Menubutton (self , text ='Aide') helpMenu. pack (side =LEFT, padx =5) mel = Menu (helpMenu) mel. add command (label =' Principe du jeu', underline =0, command = boss .principe) mel. add command (label ='A propos ...', underline =0, command = boss . aPropos) helpMenu . configure (menu = mel) class Panneau (Frame) : """Panneau de jeu (grille de n x m cases)""" def init (self, boss =None) : # Ce panneau de jeu est constitue d'un cadre redimensionnable # contenant lui-meme un canevas . A chaque redimensionnement du # cadre, on calcule la plus grande taille possible pour les # cases (carrees) de la grille, et on adapte les dimensions du # canevas en consequence . Frame . init (self) self.nlig, self.ncol = 4, 4 # Grille initiale =4x4 # Liaison de l'evenement a un gestionnaire approprie : self . bind ( " " , self . redim) # Canevas : self. can =Canvas (self , bg ="dark olive green", borderwidth =0, highlightthickness =1, highlightbackground ="white") # Liaison de l'evenement a son gestionnaire : self . can . bind ( "" , self . clic) self . can . pack ( ) self . init Jeu () Gerard Swinnen : Apprendre a programmer avec Python 245. def init Jeu (self ) : "Initialisation de la liste memorisant l'etat du jeu" self.etat =[] # construction d'une liste de listes for i in range (12) : # (equivalente a un tableau self . etat . append ( [0] *12) # de 12 lignes x 12 colonnes) def redim(self, event) : "Operations effectuees a chaque redimensionnement" # Les proprietes associees a l'evenement de reconfiguration # contiennent les nouvelles dimensions du cadre : self. width, self. height = event. width -4, event. height -4 # La difference de 4 pixels sert a compenser l'epaisseur # de la ' highlightbordure" entourant le canevas) self . traceGrille ( ) def traceGrille (self ) : "Dessin de la grille, en fonction des options & dimensions" # largeur et hauteur maximales possibles pour les cases : lmax = self .width/self .ncol hmax = self . height/self . nlig # Le cote d'une case sera egal a la plus petite de ces dimensions : self. cote = min(lmax, hmax) # -> etablissem*nt de nouvelles dimensions pour le canevas : larg, haut = self . cote*self . ncol, self . cote*self . nlig self .can. configure (width =larg, height =haut) # Trace de la grille : self . can . delete (ALL) # Effacement dessins anterieurs s =self.cote for 1 in range (self . nlig -1): # lignes horizontales self .can. create_line (0, s, larg, s, fill="white") s +=self.cote s =self.cote for c in range (self . ncol -1): # lignes verticales self .can. create_line (s, 0, s, haut, fill = "white") s +=self.cote # Trace de tous les pions, blancs ou noirs suivant l'etat du jeu : for 1 in range (self . nlig) : for c in range (self . ncol) : xl = c *self.cote +5 # taille des pions = x2 = (c +1) *self .cote -5 # taille de la case -10 yl = 1 *self.cote +5 # y2 = (1 +1) *self .cote -5 coul =[ "white", "black"] [self .etat [1] [c] ] self .can. create_oval (xl, yl, x2, y2, outline ="grey", width =1, fill =coul) 246. Gerard Swinnen : Apprendre a programmer avec Python def clic(self, event): "Gestion du clic de souris : retournement des pions" # On commence par determiner la ligne et la colonne : lig, col = event . y/ self . cote, event .x/self. cote # On traite ensuite les 8 cases adjacentes : for 1 in range (lig -1, lig+2) : if 1 <0 or 1 >= self.nlig: continue for c in range (col -1, col +2) : if c <0 or c >= self.ncol: continue if 1 ==lig and c ==col : continue # Retournement du pion par inversion logique : self .etat [1] [c] = not (self .etat [1] [c] ) self . traceGrille ( ) class Ping (Frame): """corps principal du programme""" def init (self) : Frame . init (self) self . master . geometry ( " 4 00x30 0 " ) self .master .title (" Jeu de Ping") self.mbar = MenuBar (self ) self .mbar .pack (side =TOP, expand =NO, fill =X) self. jeu =Panneau (self ) self . jeu. pack (expand =YES, fill=BOTH, padx =8, pady =8) self .pack () def options (self ) : "Choix du nombre de lignes et de colonnes pour la grille" opt =Toplevel (self) curL =Scale(opt, length =200, label ="Nombre de lignes :", orient HORIZONTAL, from_ =1, to =12, command =self .ma jLignes) curL. set (self . jeu. nlig) # position initiale du curseur curL.pack () curH =Scale(opt, length =200, label ="Nombre de colonnes :", orient HORIZONTAL, from_ =1, to =12, command =self .ma jColonnes) curH. set (self. jeu.ncol) curH . pack ( ) def ma jColonnes (self , n) : self . jeu.ncol = int (n) self . jeu . traceGrille ( ) def ma jLignes (self, n) : self . jeu . nlig = int (n) self . jeu . traceGrille ( ) Gerard Swinnen : Apprendre a programmer avec Python 247. def reset (self) : self . jeu . init Jeu ( ) self . jeu . traceGrille ( ) def principe (self ) : "Fenetre-message contenant la description sommaire du principe du jeu" msg =Toplevel (self) Message (msg, bg ="navy", fg ="ivory", width =400, font ="Helvetica 10 bold", text ="Les pions de ce jeu possedent chacun une face blanche et "\ "une face noire. Lorsque 1 ' on clique sur un pion, les 8 "\ "pions adjacents se retournent . \nLe jeu consiste a essayer "\ "de les retouner tous . \n\nSi l'exercice se revele tres facile "\ "avec une grille de 2 x 2 cases. II devient plus difficile avec "\ "des grilles plus grandes . II est meme tout a fait impossible "\ "avec certaines grilles . \nA vous de determiner lesquelles !\n\n"\ "Ref : revue 'Pour la Science' - Aout 2002") \ .pack(padx =10, pady =10) def aPropos (self) : "Fenetre-message indiquant 1 ' auteur et le type de licence" msg =Toplevel (self) Message (msg, width =200, aspect =100, justify =CENTER, text ="Jeu de Ping\n\n(C) Gerard Swinnen, Aout 2002. \n"\ "Licence = GPL") .pack (padx =10, pady =10) if name == ' main ' : Ping() .mainloopO Rappel : Si vous souhaitez experimenter ces programmes sans avoir a les reecrire, vous pouvez trouver leur code source a Vadresse : http://www.ulg.ac.be/cifen/inforef/swi/python.htm 248. Gerard Swinnen : Apprendre a programmer avec Python Chapitre 16 : Gestion d'une base de donnees Les bases de donnees sont des outils de plus en plus frequemment utilises. Elles permettent de stocker des donnees nombreuses dans un seul ensemble bien structure. Lorsqu'il s'agit de bases de donnees relationnelles, il devient en outre tout a fait possible d'eviter l'« enfer des doublons ». Vous avez surement ete deja confrontes a ce probleme : Des donnees identiques ont ete enregistrees dans plusieurs fichiers differents. Lorsque vous souhaitez modifier ou supprimer l'une de ces donnees, vous devez ouvrir et modifier tous les fichiers qui la contiennent ! Le risque d'erreur est tres reel, qui conduit inevitablement a des incoherences, sans compter la perte de temps que cela represente. Les bases de donnees constituent la solution a ce type de probleme. Python vous permet d'en utiliser de nombreux systemes, mais nous n'en examinerons que deux dans nos exemples : Gadfly et MySQL. 16.1 Les bases de donnees II existe de nombreux types de bases de donnees. On peut par exemple deja considerer comme une base de donnees elementaire, un fichier qui contient une liste de noms et d'adresses. Si la liste n'est pas trop longue, et si Ton ne souhaite pas pouvoir y effectuer des recherches en fonction de criteres complexes, il va de soi que Ton peut acceder a ce type de donnees en utilisant des instructions simples, telles celles que nous avons abordees page 108. La situation se complique cependant tres vite si Ton souhaite pouvoir effectuer des selections et des tris parmi les donnees, surtout si celles-ci deviennent tres nombreuses. La difficulty augmente encore si les donnees sont repertories dans differents ensembles relies par un certain nombre de relations hierarchiques, et si plusieurs utilisateurs doivent pouvoir y acceder en parallele. Imaginez par exemple que la direction de votre ecole vous confie la charge de mettre au point un systeme de bulletins informatise. En y reflechissant quelque peu, vous vous rendrez compte rapidement que cela suppose la mise en ceuvre de toute une serie de tables differentes : une table des noms d'eleves (laquelle pourra bien entendu contenir aussi d'autres informations specifiques a ces eleves : adresse, date de naissance, etc.) ; une table contenant la liste des cours (avec le nom du professeur titulaire, le nombre d'heures enseignees par semaine, etc.) ; une table memorisant les travaux pris en compte pour revaluation (avec leur importance, leur date, leur contenu, etc.) ; une table decrivant la maniere dont les eleves sont groupes par classes ou par options, les cours suivis par chacun, etc., etc. Vous comprenez bien que ces differentes tables ne sont pas independantes. Les travaux effectues par un meme eleve sont lies a des cours differents. Pour etablir le bulletin de cet eleve, il faut done extraire des donnees de la table des travaux, bien sur, mais en relation avec des informations trouvees dans d'autres tables (celles des cours, des classes, des options, etc.) Nous verrons plus loin comment representer des tables de donnees et les relations qui les lient. 16.1.1 SGBDR - Le modele client/serveur Les programmes informatiques capables de gerer efficacement de tels ensembles de donnees complexes sont forcement complexes, eux aussi. On appelle ces programmes des SGBDR (Systemes de Gestion de Bases de Donnees Relationnelles). II s'agit d'applications informatiques de premiere importance pour les entreprises. Certaines sont les fleurons de societes specialisees (IBM®, Oracle®, Microsoft®, Informix®, Sybase®...) et sont en general vendues a des prix fort eleves. D'autres ont ete developpees dans des centres de recherche et d'enseignement universitaires Gerard Swinnen : Apprendre a programmer avec Python 249. (PostgreSQV , MySQV ...); elles sont alors en general tout a fait gratuites. Ces systemes ont chacun leurs specificites et leurs performances, mais la plupart fonctionnant sur le modele client/serveur : cela signifie que la plus grosse partie de l'application (ainsi que la base de donnees prise en charge) est installee en un seul endroit, en principe sur une machine puissante (cet ensemble constituant done le serveur), alors que l'autre partie, beaucoup plus simple, est installee sur un nombre indetermine de postes de travail, et on appelle celles-ci des clients. Les clients sont relies au serveur, en permanence ou non, par divers precedes et protocoles (eventuellement par l'intermediaire de l'internet). Chacun d'entre eux peut acceder a une partie plus ou moins importante des donnees, avec autorisation ou non de modifier certaines d'entre elles, d'en aj outer ou d'en supprimer, en fonction de regies d'acces bien determinees. (Ces regies sont definies par un administrateur de la base de donnees). Le serveur et ses clients sont en fait des applications distinctes qui s'echangent des informations. Imaginez par exemple que vous etes l'un des utilisateurs du systeme. Pour acceder aux donnees, vous devez lancer 1' execution d'une application cliente sur un poste de travail quelconque. Dans son processus de demarrage, l'application cliente commence par etablir la connexion avec le serveur et la base de donnees 61 . Lorsque la connexion est etablie, l'application cliente peut interroger le serveur en lui envoyant une requete sous une forme convenue. II s'agit par exemple de retrouver une information precise. Le serveur execute alors la requete en recherchant les donnees correspondantes dans la base, puis il expedie en retour une certaine reponse au client. Cette reponse peut etre l'information demandee, ou encore un message d'erreur en cas d'insucces. La communication entre le client et le serveur est done faite de requetes et de reponses. Les requetes sont de veritables instructions expediees du client au serveur, non seulement pour extraire des donnees de la base, mais aussi pour en aj outer, en supprimer, en modifier, etc. 16.1.2 Le langage SQL - Gadfly Etant donnee la diversite des SGBDR existants, on pourrait craindre que chacun d'eux necessite l'utilisation d'un langage particulier pour les requetes qu'on lui adresse. En fait, de grands efforts ont ete accomplis un peu partout pour la mise au point d'un langage commun, et il existe a present un standard bien etabli : SQL (Structured Query Language, ou langage de requetes structure) 62 . Vous aurez probablement l'occasion de rencontrer SQL dans d'autres domaines (bureautique, par exemple). Dans le cadre de cette introduction a l'apprentissage de la programmation avec Python, nous allons nous limiter a la presentation de deux exemples : la mise en oeuvre d'un petit SGBDR realise exclusivement a l'aide de Python, et l'ebauche d'un logiciel client plus ambitieux destine a communiquer avec un serveur de bases de donnees MySQL. Notre premiere realisation utilisera un module nomme Gadfly. Entierement ecrit en Python, ce module ne fait pas partie de la distribution standard et doit done etre installe separement 63 . II integre un large sous-ensemble de commandes SQL. Ses performances ne sont evidemment pas comparables a celles d'un gros SGBDR specialise 64 , mais elles sont tout a fait excellentes pour la 61 il vous faudra certainement entrer quelques informations pour obtenir l'acces : adresse du serveur sur le reseau, nom de la base de donnees, nom d'utilisateur, mot de passe, ... 62 Quelques variantes subsistent entre differentes implementations du SQL, pour des requetes tres specifiques, mais la base reste cependant la meme. 63 Le module Gadfly est disponible gratuitement sur l'internet. Voir http://sourceforge.net/projects/gadfly L'installation de ce module est decrite dans l'annexe 17.6 , page 306. 64 Gadfly se revele relativement efficace pour la gestion de bases de donnees de taille moyenne, en mode mono- utilisateur. Pour gerer de grosses bases de donnees en mode multi-utilisateur, il faut faire appel a des SGDBR plus 250. Gerard Swinnen : Apprendre a programmer avec Python gestion de bases de donnees modestes. Absolument portable comme Python lui-meme, Gadfly fonctionnera indifferemment sous Windows , Linux ou MacOS. De meme, les repertoires contenant des bases de donnees produites sous Gadfly pourront etre utilisees sans modification depuis l'un ou l'autre de ces systemes. Si vous souhaitez developper une application qui doit gerer des relations relativement complexes dans une petite base de donnees, le module Gadfly peut vous faciliter grandement la tache. 16.2 Mise en oeuvre d'une base de donnees simple avec Gadfly Nous allons ci-apres examiner comment mettre en place une application simple, qui fasse office a la fois de serveur et de client sur la meme machine. 16.2.1 Creation de la base de donnees Comme vous vous y attendez certainement, il suffit d'importer le module gadfly pour acceder aux fonctionnalites correspondantes. Vous devez ensuite creer une instance (un objet) de la classe gadfly : import gadfly baseDonn = gadfly . gadfly ( ) L'objet baseDonn ainsi cree est votre moteur de base de donnees local, lequel effectuera la plupart de ses operations en memoire vive. Ceci permet une execution tres rapide des requetes. Pour creer la base de donnees proprement dite, il faut employer la methode startup de cet objet : baseDonn . startup ( "mydata" , "E : /Python/essais/gadf ly " ) Le premier parametre transmis, mydata, est le nom choisi pour la base de donnees (vous pouvez evidemment choisir un autre nom !). Le second parametre est le repertoire ou Ton souhaite installer cette base de donnees. (Ce repertoire doit avoir ete cree au prealable, et toute base de donnees de meme nom qui preexisterait dans ce repertoire est ecrasee sans avertissem*nt). Les trois lignes de code que vous venez d'entrer sont suffisantes : vous disposez des a present d'une base de donnees fonctionnelle, dans laquelle vous pouvez creer differentes tables, puis ajouter, supprimer ou modifier des donnees dans ces tables. Pour toutes ces operations, vous allez utiliser le langage SQL. Afin de pouvoir transmettre vos requetes SQL a l'objet baseDonn , vous devez cependant mettre en oeuvre un curseur. II s'agit d'une sorte de tampon memoire intermediate, destine a memoriser temporairement les donnees en cours de traitement, ainsi que les operations que vous effectuez sur elles, avant leur transfert definitif dans de vrais fichiers. Cette technique permet done d'annuler si necessaire une ou plusieurs operations qui se seraient revelees inadequates (Vous pouvez en apprendre davantage sur ce concept en consultant l'un des nombreux manuels qui traitent du langage SQL). Veuillez a present examiner le petit script ci-dessous, et noter que les requetes SQL sont des chaines de caracteres, prises en charge par la methode execute de l'objet curseur : cur = baseDonn. cursor () cur .execute ("create table membres (age integer, nom varchar, taille float)") cur. execute ("insert into membres (age, nom, taille) values (21, 'Dupont ' , 1 . 83) ") cur. execute ("INSERT INTO MEMBRES (AGE, NOM, TAILLE) VALUES (15 , ' Suleau ' , 1 . 57 ) " ) cur .execute ("Insert Into Membres (Age, Nom, Taille) Values (18, 'Forcas ' , 1 . 69) ") baseDonn . commit ( ) ambitieux tels que PostgreSQL, pour lesquels des modules clients Python existent aussi (Pygresql, par ex.). Gerard Swinnen : Apprendre a programmer avec Python 251. La premiere des lignes ci-dessus cree l'objet curseur cur. Les chaines de caracteres comprises entre guillemets dans les 4 lignes suivantes contiennent des requetes SQL tres classiques. Notez bien que le langage SQL ne tient aucun compte de la casse des caracteres : vous pouvez encoder vos requetes SQL indifferemment en majuscules ou en minuscules (ce qui n'est pas le cas pour les instructions Python environnantes, bien entendu !) La seconde ligne cree une table nominee membres, laquelle contiendra des enregistrements de 3 champs : le champ age de type « nombre entier », le champ nom de type « chaine de caracteres » (de longueur variable 65 ) et le champ taille, de type « nombre reel » (a virgule flottante). Le langage SQL autorise en principe d'autres types, mais ils ne sont pas implementes dans Gadfly. Les trois lignes qui suivent sont similaires. Nous y avons melange majuscules et minuscules pour bien montrer que la casse n'est pas significative en SQL. Ces lignes servent a inserer trois enregistrements dans la table membres. A ce stade des operations, les enregistrement n'ont pas encore ete transferes dans de veritables fichiers sur disque. II est done possible de revenir en arriere, comme nous le verrons un peu plus loin. Le transfert sur disque est active par la methode commit() de la derniere ligne d' instructions. 16.2.2 Connexion a une base de donnees existante Supposons qu'a la suite des operations ci-dessus, nous decidions de terminer le script, ou meme d'eteindre l'ordinateur. Comment devrons-nous proceder par la suite pour acceder a nouveau a notre base de donnees ? L'acces a une base de donnees existante ne necessite que deux lignes de code : import gadfly baseDonn = gadfly. gadfly ("mydata", "E : /Python/essais/gadf ly" ) Ces deux lignes suffisent en effet pour transferer en memoire vive les tables contenues dans les fichiers enregistres sur disque. La base de donnees peut desormais etre interrogee et modifiee : cur = baseDonn . cursor ( ) cur .execute ("select * from membres") print cur . pp ( ) La premiere de ces trois lignes ouvre un curseur. La requete emise dans la seconde ligne demande la selection d'un ensemble d'enregistrements, qui seront transferes de la base de donnees au curseur. Dans le cas present, la selection n'en n'est pas vraiment une : on y demande en effet d'extraire tous les enregistrements de la table membres (le symbole * est frequemment utilise en informatique avec la signification « tout » ou « tous »). La methode pp() utilisee sur le curseur, dans la troisieme ligne, provoque un affichage de tout ce qui est contenu dans le curseur sous une forme pre-formatee (les donnees presentes sont automatiquement disposees en colonnes). « pp » doit en effet etre compris comme « pretty print ». Si vous preferez controler vous-meme la mise en page des informations, il vous suffit d'utiliser a sa place la methode fetchall() , laquelle renvoie une liste de tuples. Essayez par exemple : for x in cur . f etchall ( ) : print x, x[0], x[l], x[2] Vous pouvez bien entendu aj outer des enregistrements supplementaires : cur .execute ("Insert Into Membres(Age, Nom, Taille) Values (19, 'Ricard' , 1 . 75) " ) 65 Veuillez noter qu'en SQL, les chaines de caracteres doivent etre delimitees par des apostrophes. Si vous souhaitez que la chaine contienne elle-meme une ou plusieurs apostrophes, il vous suffit de doubler celles-ci. 252. Gerard Swinnen : Apprendre a programmer avec Python Pour modifier un ou plusieurs enregistrements, executez une requete du type : cur . execute ( "update membres set nom ='Gerart' where nom='Ricard' ") Pour supprimer un ou plusieurs enregistrements, utilisez une requete telle que : cur . execute ( "delete from membres where nom= ' Gerart ' " ) Si vous effectuez toutes ces operations a la ligne de commande de Python, vous pouvez en observer le resultat a tout moment en effectuant un « pretty print » comme explique plus haut. Etant donne que toutes les modifications apportees au curseur se passent en memoire vive, rien n'est enregistre definitivement tant que vous n'executez pas l'instruction baseDonn.commit(). Vous pouvez done annuler toutes les modifications apportees depuis le commit() precedent, en refermant la connexion a l'aide de l'instruction : baseDonn . close ( ) 16.2.3 Recherches dans une base de donnees (16) Exercice 16.1. Avant d'aller plus loin, et a titre d'exercice de synthese, nous allons vous demander de creer entierement vous-meme une base de donnees « Musique » qui contiendra les deux tables suivantes (Cela represente un certain travail, mais il faut que vous puissiez disposer d'un certain nombre de donnees pour pouvoir experimenter les fonctions de recherche et de tri) : oeuvres comp (chaine) titre (chaine) duree (entier) interpr (chaine) Compositeurs comp (chaine) a_naiss (entier) a mort (entier) Commencez a remplir la table Compositeurs avec les donnees qui suivent (... et profitez de cette occasion pour faire la preuve des competences que vous maitrisez deja, en ecrivant un petit script pour vous faciliter l'entree des informations : une boucle s'impose !) comp a_naiss a_mort Mozart 1756 1791 Beethoven 1770 1827 Handel 1685 1759 Schubert 1797 1828 Vivaldi 1678 1741 Monteverdi 1567 1643 Chopin 1810 1849 Bach 1685 1750 Dans la table oeuvres, entrez les donnees suivantes : comp titre duree interpr Vivaldi Les quatre saisons 20 T . Pinnock Mozart Concerto piano N°12 25 M. Perahia Brahms Concerto violon N°2 40 A . Grumiaux Gerard Swinnen : Apprendre a programmer avec Python 253. Beethoven Sonate "au clair de lune" 14 W. Kempf Beethoven Sonate "pathetique" 17 W. Kempf Schubert Quintette "la truite" 39 SE of London Haydn La creation 109 H. Von Kara j an Chopin Concerto piano N°l 42 M. J. Pires Bach Toccata & fugue 9 P . Burmester Beethoven Concerto piano N°4 33 M. Pollini Mozart Symphonie N°40 29 F. Bruggen Mozart Concerto piano N°22 35 S. Richter Beethoven Concerto piano N°3 37 S . Richter Les champs a_naiss et a_mort contiennent respectivement l'annee de naissance et l'annee de la mort des compositeurs. La duree des oeuvres est fournie en minutes. Vous pouvez evidemment aj outer autant d'enregistrements d' oeuvres et de compositeurs que vous le voulez, mais ceux qui precedent devraient suffire pour la suite de la demonstration. Pour ce qui va suivre, nous supposerons done que vous avez effectivement encode les donnees des deux tables decrites ci-dessus. (Si vous eprouvez des difficultes a ecrire le script necessaire, nous en donnons un exemple dans les annexes de ces notes, a la page 352). Le petit script ci-dessous est fourni a titre purement indicatif. II s'agit d'un client SQL rudimentaire, qui vous permet de vous connecter a la base de donnees « musique » qui devrait a present exister dans l'un de vos repertoires, d'y ouvrir un curseur et d'utiliser celui-ci pour effectuer des requetes. Notez encore une fois que rien n'est transcrit sur le disque tant que la methode commit 0 n'a pas ete invoquee. # Utilisation d'une petite base de donnees acceptant les requetes SQL import gadfly baseDonn = gadfly. gadfly ("musique", "E : /Python/essais/gadf ly" ) cur = baseDonn . cursor ( ) while 1 : print "Veuillez entrer votre requete SQL (ou pour terminer) : " requete = raw_input ( ) if requete =="": break try: cur . execute (requete) # tentative d' execution de la requete SQL except : print '*** Requete incorrecte *** ' else : print cur . pp ( ) # af f ichage du resultat de la requete print choix = raw_input ( "Conf irmez-vous 1 ' enregistrement (o/n) ? ") if choix[0] == "o" or choix[0] == "O" : baseDonn . commit ( ) else : baseDonn . close ( ) 254. Gerard Swinnen : Apprendre a programmer avec Python Cette application tres simple n'est evidemment qu'un exemple. II faudrait y ajouter la possibility de choisir la base de donnees ainsi que le repertoire de travail. Pour eviter que le script ne se « plante » lorsque l'utilisateur encode une requete incorrecte, nous avons utilise ici le traitement des exceptions deja decrit a la page 118. 16.2.4 La requete select L'une des instructions les plus puissantes du langage SQL est l'instruction select, dont nous allons a present explorer quelques fonctionnalites. Rappelons encore une fois que nous n'abordons ici qu'une tres petite partie du sujet : la description detaillee de SQL peut occuper plusieurs livres. Lancez done le script ci-dessus, et analysez attentivement ce qui se passe lorsque vous proposez les requetes suivantes : select * from oeuvres select * from oeuvres where comp = 'Mozart' select comp, titre, duree from oeuvres order by comp select titre, comp from oeuvres where comp='Beethoven' or comp='Mozart' order by comp select count(*) from oeuvres select sum(duree) from oeuvres select avg(duree) from oeuvres select sum(duree) from oeuvres where comp='Beethoven' select * from oeuvres where duree >35 order by duree desc Pour chacune de ces requetes, tachez d'exprimer le mieux possible ce qui se passe. Fondamentalement, vous activez sur la base de donnees des filtres de selection et des tris. Les requetes suivantes sont plus elaborees, car elles concernent les deux tables a la fois. select o.titre, c.nom, c.anaiss from oeuvres o, compositeurs c where o.comp = c.comp select comp from oeuvres intersect select comp from compositeurs select comp from oeuvres except select comp from compositeurs select comp from compositeurs except select comp from oeuvres select distinct comp from oeuvres union select comp from compositeurs II ne nous est pas possible de developper davantage le langage de requetes dans le cadre restreint de ces notes. Nous allons cependant examiner encore un exemple de realisation Python faisant appel a un systeme de bases de donnees, mais en supposant cette fois qu'il s'agisse de dialoguer avec un systeme serveur independant (lequel pourrait etre par exemple un gros serveur de bases de donnees d'entreprise, un serveur de documentation dans une ecole, etc.). Gerard Swinnen : Apprendre a programmer avec Python 255. 16.3 Ebauche d'un logiciel client pour MySQL Pour terminer ce chapitre, nous allons vous proposer dans les pages qui suivent un exemple de realisation concrete. II ne s'agira pas d'un veritable logiciel (le sujet exigerait qu'on lui consacre un ouvrage specifique), mais plutot d'une ebauche d'analyse, destinee a vous montrer comment vous pouvez « penser comme un programmeur » lorsque vous abordez un probleme complexe. Les techniques que nous allons mettre en oeuvre ici sont de simples suggestions, dans lesquelles nous essayerons d'utiliser au mieux les outils que vous avez decouverts au cours de votre apprentissage dans les chapitres precedents, a savoir : les structures de donnees de haut niveau (listes et dictionnaires), et la programmation par objets. II va de soi que les options retenues dans cet exercice restent largement critiquables : vous pouvez bien evidemment traiter les memes problemes en utilisant des approches differentes. Notre objectif concret est d'arriver a realiser rapidement un client rudimentaire, capable de dialoguer avec un « vrai » serveur de bases de donnees tel que MySQL. Nous voudrions que notre client reste un petit utilitaire tres generaliste : qu'il soit capable de mettre en place une petite base de donnees comportant plusieurs tables, qu'il puisse servir a produire des enregistrements pour chacune d'elles, qu'il permette de tester le resultat de requetes SQL basiques. Dans les lignes qui suivent, nous supposerons que vous avez deja acces a un serveur MySQL, sur lequel une base de donnees « discotheque » aura ete creee pour l'utilisateur « jules », lequel s'identifie a l'aide du mot de passe « abcde ». Ce serveur peut etre situe sur une machine distante accessible via un reseau, ou localement sur votre ordinateur personnel 66 . 16.3.1 Decrire la base de donnees dans un dictionnaire d'application Une application dialoguant avec une base de donnees est presque toujours une application complexe. Elle comporte done de nombreuses lignes de code, qu'il s'agit de structurer le mieux possible en les regroupant dans des classes (ou au moins des fonctions) bien encapsulees. En de nombreux endroits du code, souvent fort eloignes les uns des autres, des blocs d'instructions doivent prendre en compte la structure de la base de donnees, e'est-a-dire son decoupage en un certain nombre de tables et de champs, ainsi que les relations qui etablissent une hierarchie dans les enregistrements. Or, l'experience montre que la structure d'une base de donnees est rarement definitive. Au cours 66 ^installation et la configuration d'un serveur MySQL sortent du cadre de cet ouvrage, mais ce n'est pas une tache bien compliquee. C'est meme fort simple si vous travaillez sous Linux, installe depuis une distribution « classique » telle que Debian, RedHat, SuSE ou Mandrake. II vous suffit d'installer les paquetages MySQL-server et Python- MySQL, de demarrer le service MySQL, puis d'entrer les commandes : mysqladmin -u root password xxxx Cette premiere commande defmit le mot de passe de 1'administrateur principal de MySQL. Elle doit etre executee par 1'administrateur du systeme Linux ('root'), avec un mot de passe de votre choix. On se connecte ensuite au serveur sous le compte administrateur ainsi defini (le mot de passe sera demande) : mysql -u root mysql -p grant all privileges on *.* to jules@localhost identified by 'abcde'; grant all privileges on *.* to jules@"%" identified by 'abode'; \q Ces commandes defmissent un nouvel utilisateur « jules » pour le systeme MySQL, et cet utilisateur devra se connecter le mot de passe « abcde » (Les deux lignes autorisent respectivement l'acces local et l'acces via reseau). Le nom d'utilisateur est quelconque : il ne doit pas necessairement correspondre a un utilisateur systeme. L'utilisateur « jules » peut a present se connecter et creer des bases de donnees : mysql -u jules -p create database discotheque; \<* ... etc. A ce stade, le serveur MySQL est pret a dialoguer avec le client Python decrit dans ces pages. 256. Gerard Swinnen : Apprendre a programmer avec Python d'un developpement, on realise souvent qu'il est necessaire de lui ajouter ou de lui retirer des champs, parfois meme de remplacer une table mal concue par deux autres, etc. II n'est done pas prudent de programmer des portions de code trop specifiques d'une structure particuliere, « en dur ». Au contraire, il est hautement recommandable de decrire plutot la structure complete de la base de donnees en un seul endroit du programme, et d'utiliser ensuite cette description comme reference pour la generation semi-automatique des instructions particulieres concernant telle table ou tel champ. On evite ainsi, dans une large mesure, le cauchemar de devoir traquer et modifier un grand nombre d'instructions un peu partout dans le code, chaque fois que la structure de la base de donnees change un tant soit peu. Au lieu de cela, il suffit de changer seulement la description de reference, et la plus grosse partie du code reste correcte sans necessiter de modification. Nous tenons la une idee maitresse pour realiser des applications robustes : Un logiciel destine au traitement de donnees devrait toujours etre construit sur la base d'un dictionnaire d' application. Ce que nous entendons ici par « dictionnaire d'application » ne doit pas necessairement revetir la forme d'un dictionnaire Python. N'importe quelle structure de donnees peut convenir, l'essentiel etant de se construire une reference centrale decrivant les donnees que Ton se propose de manipuler, avec peut-etre aussi un certain nombre d'informations concernant leur mise en forme. Du fait de leur capacite a rassembler en une meme entite des donnees de n'importe quel type, les listes, tuples et dictionnaires de Python conviennent parfaitement pour ce travail. Dans l'exemple des pages suivantes, nous avons utilise nous-memes un dictionnaire, dont les valeurs sont des listes de tuples, mais vous pourriez tout aussi bien opter pour une organisation differente des memes informations. Tout cela etant bien etabli, il nous reste encore a regler une question d'importance : ou allons- nous installer concretement ce dictionnaire d'application ? Ses informations devront pouvoir etre consultees depuis n'importe quel endroit du programme. II semble done obligatoire de l'installer dans une variable globale, de meme d'ailleurs que d'autres donnees necessaires au fonctionnement de l'ensemble de notre logiciel. Or vous savez que l'utilisation de variables globales n'est pas recommandee : elle comporte des risques, qui augmentent avec la taille du programme. De toute facon, les variables dites globales ne sont en fait globales qu'a l'interieur d'un meme module. Si nous souhaitons organiser notre logiciel comme un ensemble de modules (ce qui constitue par ailleurs une excellente pratique), nous n'aurons acces a nos variables globales que dans un seul d'entre eux. Pour resoudre ce petit probleme, il existe cependant une solution simple et elegante : regrouper dans une classe particuliere toutes les variables qui necessitent un statut global pour l'ensemble de I' application. Ainsi encapsulees dans l'espace de noms d'une classe, ces variables peuvent etre utilisees sans probleme dans n'importe quel module : il suffit en effet que celui-ci importe la classe en question. De plus, l'utilisation de cette technique entraine une consequence interessante : le caractere « global » des variables definies de cette maniere apparait tres clairement dans leur nom qualifie, puisque ce nom commence par celui de la classe contenante. Si vous choisissez, par exemple, un nom explicite tel que Glob pour la classe destinee a accueillir vos variables « globales », vous vous assurez de devoir faire reference a ces variables partout dans votre code avec des noms tout aussi explicites tels que Glob.ceci , Glob.cela , etc 67 . 67 Vous pourriez egalement placer vos variables « globales » dans un module nomme Glob.py, puis importer celui-ci. Utiliser un module ou une classe comme espace de noms pour stacker des variables sont done des techniques assez similaires. L'utilisation d'une classe est peut-etre un peu plus souple et plus lisible, puisque la classe peut accompagner le reste du script, alors qu'un module est necessairement un fichier distinct. Gerard Swinnen : Apprendre a programmer avec Python 257. C'est cette technique que vous allez decouvrir a present dans les premieres lignes de notre script. Nous y definissons effectivement une classe Glob(), qui n'est done rien d'autre qu'un simple conteneur. Aucun objet ne sera instancie a partir de celle classe, laquelle ne comporte d'ailleurs aucune methode. Nos variables « globales » y sont definies comme de simples variables de classe, et nous pourrons done y faire reference dans tout le reste du programme en tant qu'attributs de Glob. Le nom de la base de donnees, par exemple, pourra etre retrouve partout dans la variable Glob.dbName ; le nom ou l'adresse IP du serveur dans la variable Glob.host, etc. : 1. class Glob: 2. """Espace de noms pour les variables et fonctions " " " 3. 4. dbName = "discotheque" # nom de la base de donnees 5. user = "jules" # proprietaire ou utilisateur 6. passwd = "abede" # mot de passe d'acces 7. host = "192.168.0.235" # nom ou adresse IP du serveur 8. 9 . # Structure de la base de donnees . Dictionnaire des tables & champs : 10. dicoT ={ "compositeurs" :[(' id_comp ' , "k", "cle primaire"), 11. ('nom', 25, "nom"), 12. ('prenom', 25, "prenom"), 13. ('a_naiss', "i", "annee de naissance") , 14 . ( ' a_mort ' , " i " , "annee de mort " ) ] , 15. "oeuvres" : [ ('id_oeuv' , "k", "cle primaire" ) , 16. ('id_comp', "i", "cle compositeur"), 17. ('titre', 50, "titre de l'oeuvre"), 18. ('duree', "i", "duree (en minutes)"), 19. ('interpr', 30, "interprete principal")]} Le dictionnaire d'application decrivant la structure de la base de donnees est contenu dans la variable Glob.dicoT. II s'agit d'un dictionnaire Python, dont les cles sont les noms des tables. Quant aux valeurs, chacune d'elles est une liste contenant la description de tous les champs de la table, sous la forme d'autant de tuples. Chaque tuple decrit done un champ particulier de la table. Pour ne pas encombrer notre exercice, nous avons limite cette description a trois informations seulement : le nom du champ, son type et un bref commentaire. Dans une veritable application, il serait judicieux d'ajouter encore d'autres informations ici, concernant par exemple des valeurs limites eventuelles pour les donnees de ce champ, le formatage a leur appliquer lorsqu'il s'agit de les afficher a l'ecran ou de les imprimer, le texte qu'il faut placer en haut de colonne lorsque Ton veut les presenter dans un tableau, etc. II peut vous paraitre assez fastidieux de decrire ainsi tres en detail la structure de vos donnees, alors que vous voudriez probablement commencer tout de suite une reflexion sur les divers algorithmes a mettre en oeuvre afin de les traiter. Sachez cependant que si elle est bien faite, une telle description structuree vous fera certainement gagner beaucoup de temps par la suite, parce qu'elle vous permettra d'automatiser pas mal de choses. Vous en verrez une demonstration un peu plus loin. En outre, vous devez vous convaincre que cette tache un peu ingrate vous prepare a bien structurer aussi le reste de votre travail : organisation des formulaires, tests a effectuer, etc. 16.3.2 Definir une classe d'objets-interfaces La classe Glob() decrite a la rubrique precedente sera done installee en debut de script, ou bien dans un module separe importe en debut de script. Pour la suite de l'expose, nous supposerons que c'est cette derniere formule qui est retenue : nous avons sauvegarde la classe Glob() dans un module nomme dict_app.py, d'ou nous pouvons a present l'importer dans le script suivant. Ce nouveau script definit une classe d'objets-interfaces. Nous voulons en effet essayer de mettre 258. Gerard Swinnen : Apprendre a programmer avec Python a profit ce que nous avons appris dans les chapitres precedents, et done privilegier la programmation par objets, afin de creer des portions de code bien encapsulees et largement reutilisables. Les objets-interfaces que nous voulons construire seront similaires aux objets-fichiers que nous avons abondamment utilises pour la gestion des fichiers au chapitre 9. Vous vous rappelez par exemple que nous ouvrons un fichier en creant un objet-fichier, a l'aide de la fonction-fabrique open(). D'une maniere similaire, nous ouvrirons la communication avec la base de donnees en commencant par creer un objet- interface a l'aide de la classe GestionBD(), ce qui etablira la connexion. Pour lire ou ecrire dans un fichier ouvert, nous utilisons diverses methodes de l'objet- fichier. D'une maniere analogue, nous effectuerons nos operations sur la base de donnees par l'intermediaire des diverses methodes de l'objet-interface. 1. import MySQLdb, sys 2 . from dict_app import * 3. 4 . class GestionBD : 5. """Mise en place et interfacage d'une base de donnees MySQL""" 6. def _init (self, dbName, user, passwd, host, port =3306): 7. "Etablissem*nt de la connexion - Creation du curseur" 8. try: 9. self .baseDonn = MySQLdb . connect (db =dbName, 10. user =user, passwd =passwd, host =host, port =port) 11. except Exception, err: 12. print 'La connexion avec la base de donnees a echoue :\n'\ 13. 'Erreur detectee : \n%s ' % err 14. self.echec =1 15. else: 16. self. cursor = self .baseDonn. cursor () # creation du curseur 17. self.echec =0 18. 19. def creerTables (self , dicTables) : 20. "Creation des tables decrites dans le dictionnaire . " 21. for table in dicTables: # parcours des cles du diet. 22. req = "CREATE TABLE %s (" % table 23. pk =' ' 24. for descr in dicTables [table] : 25. nomChamp = descr [0] # libelle du champ a creer 26. tch = descr [1] # type de champ a creer 27. if tch =='i' : 28. typeChamp =' INTEGER' 29. elif tch =='k' : 30. # champ ' cle primaire ' (increments automat iquement) 31. typeChamp =' INTEGER AUTO_INCREMENT ' 32 . pk = nomChamp 33. else: 34. typeChamp = ' VARCHAR(%s) ' % tch 35. req = req + "%s %s, " % (nomChamp, typeChamp) 36. if pk == ' ' : 37. req = req[ :-2] + ") " 38. else: 39. req = req + "CONSTRAINT %s_pk PRIMARY KEY(%s))" % (pk, pk) 40. self . executerReq (req) 41. 42. def supprimerTables (self , dicTables): 43. "Suppression de toutes les tables decrites dans " 44. for table in dicTables . keys () : 45. req ="DROP TABLE %s" % table 46. self . executerReq ( req) 47. self .commit () # transfert -> disque 48. 49. def executerReq (self , req) : 50. "Execution de la requete , avec detection d' erreur eventuelle" 51. try: 52. self . cursor . execute (req) Gerard Swinnen : Apprendre a programmer avec Python 259. 53. 54 . 55. 56. 57 . 58. 59. 60. 61. 62. 63. 64 . 65. 66. 67. 68. 69. 70. 71. def def def except Exception, err: % (req, err) return 0 else : return 1 close (self) : if self . baseDonn : self . baseDonn . close ( ) resultatReq(self ) : "renvoie le resultat de la requete precedente (un tuple de tuples) return self . cursor . fetchall ( ) commit (self) : if self .baseDonn: self . baseDonn . commit ( ) # afficher la requete et le message d'erreur systeme : print "Requete SQL incorrecte : \n%s\nErreur detectee :\n%s"\ # transfert curseur -> disque Commentaires : • Lignes 1-2 : Outre notre propre module dict_app qui contient les variables « globales », nous importons le module sys qui contient quelques fonctions systeme, et le module MySQLdb qui contient tout ce qui est necessaire pour communiquer avec MySQL. Rappelons que ce module ne fait pas partie de la distribution standard de Python, et qu'il doit done etre installe separement. • Ligne 5 : Lors de la creation des objets-interfaces, nous devrons fournir les parametres de la connexion : nom de la base de donnees, nom de son utilisateur, nom ou adresse IP de la machine ou est situe le serveur. Le n° du port de communication est habituellement celui que nous avons prevu par defaut. Toutes ces informations sont supposees etre en votre possession. • Lignes 8 a 17 : II est hautement recommandable de placer le code servant a etablir la connexion a l'interieur d'un gestionnaire d' exceptions try-except-else (voir page 118), car nous ne pouvons pas presumer que le serveur sera necessairement accessible. Remarquons au passage que la methode init () ne peut pas renvoyer de valeur (a l'aide de l'instruction return), du fait qu'elle est invoquee automatiquement par Python lors de l'instanciation d'un objet. En effet : ce qui est renvoye dans ce cas au programme appelant est l'objet nouvellement construit. Nous ne pouvons done pas signaler la reussite ou l'echec de la connexion au programme appelant a l'aide d'une valeur de retour. Une solution simple a ce petit probleme consiste a memoriser le resultat de la tentative de connexion dans un attribut d'instance (variable self.echec), que le programme appelant peut ensuite tester quand bon lui semble. • Lignes 19 a 40 : Cette methode automatise la creation de toutes les tables de la base de donnees, en tirant profit de la description du dictionnaire d'application, lequel doit lui etre transmis en argument. Une telle automatisation sera evidemment d'autant plus appreciable, que la structure de la base de donnees sera plus complexe (Imaginez par exemple une base de donnees contenant 35 tables !). Afin de ne pas alourdir la demonstration, nous avons restreint les capacites de cette methode a la creation de champs des types « integer » et « varchar ». Libre a vous d'aj outer les instructions necessaires pour creer des champs d'autres types. Si vous detaillez le code, vous constaterez qu'il consiste simplement a construire une requete SQL pour chaque table, morceau par morceau, dans la chaine de caracteres req. Celle-ci est ensuite transmise a la methode executerReq() pour execution. Si vous souhaitez visualiser la requete ainsi construite, vous pouvez evidemment aj outer une instruction « print req » juste apres la ligne 40. Vous pouvez egalement ajouter a cette methode la capacite de mettre en place les contraintes 260. Gerard Swinnen : Apprendre a programmer avec Python d'integrite referentielle, sur la base d'un complement au dictionnaire d'application qui decrirait ces contraintes. Nous ne developpons pas cette question ici, mais cela ne devrait pas vous poser de probleme si vous savez de quoi il retourne. • Lignes 42 a 47 : Beaucoup plus simple que la precedente, cette methode utilise le meme principe pour supprimer toutes les tables decrites dans le dictionnaire d'application. • Lignes 49 a 59 : Cette methode transmet simplement la requete a l'objet curseur. Son utilite est de simplifier Faeces a celui-ci et de produire un message d'erreur si necessaire. • Lignes 61 a 71 : Ces methodes ne sont que de simples relais vers les objets produits par le module MySQLdb : l'objet-connecteur produit par la fonction-fabrique MySQLdb.connect(), et l'objet curseur correspondant. Elles permettent de simplifier legerement le code du programme appelant. 16.3.3 Construire un generateur de formulaires Nous avons ajoute cette classe a notre exercice pour vous expliquer comment vous pouvez utiliser le meme dictionnaire d'application afin d'elaborer du code generaliste. L'idee developpee ici est de realiser une classe d'objets-formulaires capables de prendre en charge l'encodage des enregistrements de n'importe quelle table, en construisant automatiquement les instructions d'entree adequates grace aux informations tirees du dictionnaire d'application. Dans une application veritable, ce formulaire trop simpliste devrait certainement etre fortement remanie, et il prendrait vraisemblablement la forme d'une fenetre specialisee, dans laquelle les champs d'entree et leurs libelles pourraient encore une fois etre generes de maniere automatique. Nous ne pretendons done pas qu'il constitue un bon exemple, mais nous voulons simplement vous montrer comment vous pouvez automatiser sa construction dans une large mesure. Tachez de realiser vos propres formulaires en vous servant de principes semblables. 1. class Enregistreur : 2. """classe pour gerer 1' entree d' enregistrements divers""" 3. def init (self, bd, table): 4. self.bd =bd 5. self. table =table 6. self . descriptif =Glob . dicoT [table] # descriptif des champs 7. 8. def entrer (self ) : 9. "procedure d'entree d'un enregistrement entier" 10. champs ="(" # ebauche de chaine pour les noms de champs 11. valeurs ="(" # ebauche de chaine pour les valeurs 12 . # Demander successivement une valeur pour chaque champ : 13. for cha, type, nom in self . descriptif : 14. if type =="k" : # on ne demandera pas le n° d ' enregistrement 15. continue # a 1 ' utilisateur (numerotation auto.) 16. champs = champs + cha + "," 17. val = raw_input ( "Entrez le champ %s :" % nom) 18. if type =="i" : 19. valeurs = valeurs + val +"," 20. else: 21. valeurs = valeurs + " ' %s ' , " % (val) 22. 23. champs = champs [:-l] + ")" # supprimer la derniere virgule, 24. valeurs = valeurs [:-l] + ")" # ajouter une parenthese 25. req ="INSERT INTO %s %s VALUES %s" % (self. table, champs, valeurs) 26. self . bd . executerReq (req) 27. 28. ch =raw_input ("Continuer (O/N) ? ") 29. if ch. upper () == "O" : 30. return 0 31. else: 32 . return 1 Gerard Swinnen : Apprendre a programmer avec Python 261. Commentaires : • Lignes 1 a 6 : Au moment de leur instanciation, les objets de cette classe recoivent la reference de l'une des tables du dictionnaire. C'est ce qui leur donne acces au descriptif des champs. • Ligne 8 : Cette methode entrer() genere le formulaire proprement dit. Elle prend en charge l'entree des enregistrements dans la table, en s'adaptant a leur structure propre grace au descriptif trouve dans le dictionnaire. Sa fonctionnalite concrete consiste encore une fois a construire morceau par morceau une chaine de caracteres qui deviendra une requete SQL, comme dans la methode creerTables() de la classe GestionBD() decrite a la rubrique precedente. Vous pourriez bien entendu aj outer a la presente classe encore d'autres methodes, pour gerer par exemple la suppression et/ou la modification d'enregistrements. • Lignes 12 a 21 : L'attribut d'instance self.descriptif contient une liste de tuples, et chacun de ceux-ci est fait de trois elements, a savoir le nom d'un champ, le type de donnees qu'il est cense recevoir, et sa description « en clair ». La boucle for de la ligne 13 parcourt cette liste et affiche pour chaque champ un message d'invite construit sur la base de la description qui accompagne ce champ. Lorsque l'utilisateur a entre la valeur demandee, celle-ci et formatee dans une chaine en construction. Le formatage s'adapte aux conventions du langage SQL, conformement au type requis pour le champ. • Lignes 23 a 26 : Lorsque tous les champs ont ete parcourus, la requete proprement dite est assemblee et executee. Si vous souhaitez visualiser cette requete, vous pouvez bien evidemment ajouter une instruction « print req » juste apres la ligne 25. 16.3.4 Le corps de I'application II ne nous parait pas utile de developper davantage encore cet exercice dans le cadre d'un manuel d'initiation. Si le sujet vous interesse, vous devriez maintenant en savoir assez pour commencer deja quelques experiences personnelles. Veuillez alors consulter les bons ouvrages de reference, comme par exemple « Python : How to program » de Deitel & coll., ou encore les sites web consacres aux extensions de Python. Le script qui suit est celui d'une petite application destinee a tester les classes decrites dans les pages qui precedent. Libre a vous de la perfectionner, ou alors d'en ecrire une autre tout a fait differente ! I. ###### Programme principal : ######### 2 . 3 . # Creation de 1 ' ob jet-interface avec la base de donnees : 4. bd = GestionBD (Glob . dbName, Glob. user, Glob.passwd, Glob. host) 5. if bd.echec: 6. sys.exit() 7 . 8 . while 1 : 9. print "\nQue voulez-vous faire :\n"\ 10. "1) Creer les tables de la base de donnees\n"\ II. "2) Supprimer les tables de la base de donnees ?\n"\ 12. "3) Entrer des compositeurs\n" \ 13. "4) Entrer des oeuvres\n"\ 14. "5) Lister les compositeurs\n" \ 15. "6) Lister les oeuvres\n"\ 16. "7) Executer une requete SQL quelconque\n" \ 17. "9) terminer ? Votre choix :", 18. ch = int (raw_input () ) 19. if ch ==1: 20. # creation de toutes les tables decrites dans le dictionnaire : 21. bd. creerTables (Glob . dicoT) 22. elif ch ==2: 262. Gerard Swinnen : Apprendre a programmer avec Python 23. # suppression de toutes les tables decrites dans le die. 24 . bd. supprimerTables (Glob . dicoT) 25. elif ch ==3 or ch ==4: 26. # creation d'un de compositeurs ou d'oeuvres : 27. table ={3 :' compositeurs ' , 4 : ' oeuvres ' } [ch] 28. enreg =Enregistreur (bd, table) 29. while 1: 30. if enreg. entrer () : 31 . break 32. elif ch ==5 or ch ==6: 33. # listage de tous les compositeurs, ou toutes les oeuvres : 34. table ={5 :' compositeurs ' , 6 :' oeuvres '} [ch] 35. if bd.executerReq( "SELECT * FROM %s" % table): 36. # analyser le resultat de la requete ci-dessus : 37. records = bd. resultatReq ( ) # ce sera un tuple de tuples 38 . for rec in records : # => chaque enregistrement 39. for item in rec: # => chaque champ dans 1' enreg. 40. print item, 41. print 42. elif ch ==7: 43. req =raw_input ( "Entrez la requete SQL : ") 44. if bd.executerReq(req) : 45. print bd. resultatReq ( ) # ce sera un tuple de tuples 46. else: 47. bd. commit () 48. bd. close () 49. break Commentaires : • On supposera bien evidemment que les classes decrites plus haut soient presentes dans le meme script, ou qu'elles aient ete importees. • Lignes 3 a 6 : L'objet- interface est cree ici. Si la creation echoue, l'attribut d'instance bd.echec contient la valeur 1 . Le test des lignes 5 et 6 permet alors d'arreter l'application immediatement (la fonction exit() du module sys sert specifiquement a cela). • Ligne 8 : Le reste de l'application consiste a proposer sans cesse le meme menu, jusqu'a ce que l'utilisateur choisisse l'option n° 9. • Lignes 27 et 28 : La classe Enregistreur() accepte de gerer les enregistrements de n'importe quelle table. Afin de determiner laquelle doit etre utilisee lors de l'instanciation, on utilise un petit dictionnaire qui indique quel nom retenir, en fonction du choix opere par l'utilisateur (option n° 3 ou n° 4). • Lignes 29 a 31 : La methode entrer() de l'objet-enregistreur renvoie une valeur 0 ou 1 suivant que l'utilisateur ait choisi de continuer a entrer des enregistrements, ou bien d'arreter. Le test de cette valeur permet d'interrompre la boucle de repetition en consequence. • Lignes 35 et 44 : La methode executerReq() renvoie une valeur 0 ou 1 suivant que la requete ait ete acceptee ou non par le serveur. On peut done tester cette valeur pour decider si le resultat doit etre affiche ou non. Exercices : 16.2. Modifiez le script decrit dans ces pages de maniere a ajouter une table supplemental a la base de donnees. Ce pourrait etre par exemple une table « orchestres », dont chaque enregistrement contiendrait le nom de l'orchestre, le nom de son chef, et le nombre total d' instruments. 16.3. Ajoutez d'autres types de champ a l'une des tables (par exemple un champ de type float (reel) ou de type date), et modifiez le script en consequence. Gerard Swinnen : Apprendre a programmer avec Python 263. Chapitre 17 : Applications web Vous avez certainement deja appris par ailleurs un grand nombre de choses concernant la redaction de pages web. Vous savez que ces pages sont des documents au format HTML, que Ton peut consulter via un reseau (intranet ou internet) a l'aide d'un logiciel appele browser web ou navigateur (Netscape, Konqueror, Internet explorer, ...). Les pages HTML sont installees dans les repertoires publics d'un autre ordinateur ou fonctionne en permanence un logiciel appele serveur Web (Apache, IIS, Zope, ...). Lorsqu'une connexion a ete etablie entre cet ordinateur et le votre, votre logiciel navigateur peut dialoguer avec le logiciel serveur (par l'intermediaire de toute une serie de dispositifs materiels et logiciels dont nous ne traiterons pas ici : lignes telephoniques, routeurs, caches, protocoles de communication ...). Le protocole HTTP qui gere la transmission des pages web autorise l'echange de donnees dans les deux sens. Mais dans la grande majorite des cas, le transfert d'informations n'a pratiquement lieu que dans un seul, a savoir du serveur vers le navigateur : des textes, des images, des fichiers divers lui sont expedies en grand nombre (ce sont les pages consultees) ; en revanche, le navigateur n'envoie guere au serveur que de toutes petites quantites d'information : essentiellement les adresses URL des pages que l'internaute desire consulter. 17.1 Pages web interactives Vous savez cependant qu'il existe des sites web ou vous etes invite a fournir vous-meme des quantites d'information plus importantes : vos references personnelles pour l'inscription a un club ou la reservation d'une chambre d'hotel, votre numero de carte de credit pour la commande d'un article sur un site de commerce electronique, votre avis ou vos suggestions, etc. Dans un cas comme ceux-la, vous vous doutez bien que l'information transmise doit etre prise en charge, du cote du serveur, par un programme specifique. II faut done que les pages web destinees a accueillir cette information soient dotees d'un mecanisme assurant son transfert vers le logiciel destine a la traiter. II faudra egalement que ce logiciel puisse lui-meme transmettre en retour une information au serveur, afin que celui-ci puisse presenter le resultat de l'operation a l'internaute, sous la forme d'une nouvelle page web. Le but du present chapitre est de vous expliquer comment vous pouvez vous servir de vos competences de programmeur Python pour ajouter une telle interactivite a un site web, en y integrant de veritables applications. Remarque importante : Ce que nous allons expliquer dans les paragraphes qui suivent sera directement fonctionnel sur l'intranet de votre etablissem*nt scolaire ou de votre entreprise (a la condition toutefois que l'administrateur de cet intranet ait configure son serveur de maniere appropriee). En ce qui concerne l'internet, par contre, les choses sont un peu plus compliquees. II va de soi que l'installation de logiciels sur un ordinateur serveur relie a l'internet ne peut se faire qu'avec l'accord de son proprietaire. Si un fournisseur d'acces a l'internet a mis a votre disposition un certain espace ou vous etes autorise a installer des pages web « statiques » (e'est-a-dire de simples documents a consulter), cela ne signifie pas pour autant que vous pourrez y faire fonctionner des scripts Python. Pour que cela puisse marcher, vous devrez demander une autorisation et un certain nombre de renseignements a votre fournisseur d'acces. II faudra en particulier lui demander si vous pouvez activer des scripts CGI ecrits en Python a partir de vos pages, et dans quel(s) repertoire(s) vous pouvez les installer. 264. Gerard Swinnen : Apprendre a programmer avec Python 17.2 L'interface CGI L'interface CGI (pour Common Gateway Interface) est un composant de la plupart des logiciels serveurs de pages web. II s'agit d'une passerelle qui leur permet de communiquer avec d'autres logiciels tournant sur le meme ordinateur. Avec CGI, vous pouvez ecrire des scripts dans differents langages (Perl, C, Tel, Python ...). Plutot que de limiter le web a des documents ecrits a l'avance, CGI permet de generer des pages web sur le champ, en fonction des donnees que fournit l'internaute par l'intermediaire de son logiciel de navigation. Vous pouvez utiliser les scripts CGI pour creer une large palette d'applications : des services d'inscription en ligne, des outils de recherche dans des bases de donnees, des instruments de sondage d'opinions, des jeux, etc. L'apprentissage de la programmation CGI peut faire l'objet de manuels entiers. Dans cet ouvrage d'initiation, nous vous expliquerons seulement quelques principes de base, afin de vous faire comprendre, par comparaison, l'enorme avantage que presentent les modules serveurs d'applications specialises tels que Karrigell, CherryPy ou Zope, pour le programmeur desireux de developper un site web interacif. 17.2.1 Une interaction CGI rudimentaire Pour la bonne comprehension de ce qui suit, nous supposerons que Vadministrateur reseau de votre etablissem*nt scolaire ou de votre entreprise a configure un serveur web d'intranet d'une maniere telle que vous puissiez installer des pages HTML et des scripts Python dans un repertoire personnel. Notre premier exemple sera constitute d'une page web extremement simple. Nous n'y placerons qu'un seul element d'interactivite, a savoir un unique bouton. Ce bouton servira a lancer l'execution d'un petit programme que nous decrirons par apres. Veuillez done encoder le document HTML ci-dessous a l'aide d'un editeur quelconque (n'encodez pas les numeros de lignes, ils ne sont la que pour faciliter les explications qui suivent) : I . 2 . Exercice avec Python 3 . 4. 5.

6. Exercice avec Python 3 . (1) 7 .

Page Web interactive

8 .

Cette page est associee a un script Python

9. 10.

13. 14 . Vous savez certainement deja que les balises initiales , ,

,

, ainsi que les balises finales correspondantes, sont communes a tous les documents HTML. Nous ne detaillerons done pas leur role ici. La balise

utilisee a la ligne 5 sert habituellement a diviser un document HTML en sections distinctes. Nous l'utilisons ici pour definir une section dans laquelle tous les elements seront centres (horizontalement) sur la page. A la ligne 6, nous inserons une petite image. La ligne 7 definit une ligne te texte comme etant un titre de 2 e importance. La ligne 8 est un paragraphe ordinaire. Gerard Swinnen : Apprendre a programmer avec Python 265. Les lignes 10 a 12 contiennent le code important (pour ce qui nous occupe ici). Les balises

definissent en effet un formulaire, c'est-a-dire une portion de page Web susceptible de contenir divers widgets a l'aide desquels l'internaute pourra exercer une certaine activite : champs d'entree, boutons, cases a cocher, boutons radio, etc. La balise FORM doit contenir deux indications essentielles : Taction a accomplir lorsque le formulaire sera expedie (il s'agit en fait de fournir ici l'adresse URL du logiciel a invoquer pour traiter les donnees transmises), et la methode a utiliser pour transmettre l'information (en ce qui nous concerne, ce sera toujours la methode « post »). Dans notre exemple, le logiciel que nous voulons invoquer est un script Python nomme input_query.py qui est situe dans un repertoire particulier du serveur d'intranet. Sur de nombreux serveurs, ce repertoire s'appelle souvent cgi-bin , par pure convention. Nous supposerons ici que l'administrateur de votre intranet scolaire vous autorise a installer vos scripts Python dans le meme repertoire que celui ou vous placez vos pages Web personnelles. Vous devrez done modifier la ligne 10 de notre exemple, en remplacant l'adresse http: //Serveur /cgi-bin/input_query.py par ce que votre professeur vous indiquera 68 . La ligne 1 1 contient la balise qui definit un widget de type « bouton d' envoi » (balise ). Le texte qui doit apparaitre sur le bouton est precise par l'attribut VALUE ="texte". L'indication NAME est facultative dans le cas present. Elle mentionne le nom du widget lui-meme (au cas ou le logiciel destinataire en aurait besoin). Lorsque vous aurez termine l'encodage de ce document, sauvegardez-le dans le repertoire que Ton vous a attribue specifiquement pour y placer vos pages, sous un nom quelconque, mais de preference avec l'extension .html ou .htm (par exemple : essai.html). Le script Python input_query.py est detaille ci-dessous. Comme deja signale plus haut, vous pouvez installer ce script dans le meme repertoire que votre document HTML initial : I. #! /usr/bin/python 2 . 3. # Affichage d'un formulaire HTML simplifie : 4. print "Content-Type: text/html\n" 5. print """ 6.

7 . Page web produite par un script Python 8 .

9. 10.

i a " " » 68 Par exemple : http://l 92.1 68.0. 100/cgi/Classe6A/Dupont/input_query.py . Gerard Swinnen : Apprendre a programmer avec Python 266. Ce script ne fait rien d'autre que d'afficher une nouvelle page web, laquelle contient encore une fois un formulaire, mais celui-ci nettement plus elabore que le precedent. La premiere ligne est absolument necessaire : elle indique a l'interface CGI qu'il faut lancer l'interpreteur Python pour pouvoir executer le script. La seconde ligne est un simple commentaire. La ligne 4 est indispensable. Elle permet a l'interpreteur Python d'initialiser un veritable document HTML qui sera transmis au serveur web. Celui-ci pourra a son tour le reexpedier au logiciel navigateur de l'internaute, et celui-ci le verra done s'afficher dans la fenetre de navigation. La suite est du pur code HTML, traite par Python comme une simple chaine de caracteres que Ton affiche a l'aide de l'instruction print. Pour pouvoir y inserer tout ce que nous voulons, y compris les sauts a la ligne, les apostrophes, les guillemets, etc., nous delimitons cette chaine de caracteres a l'aide de « triples guillemets » (Rappelons egalement ici que les sauts a la ligne sont completement ignores en HTML : nous pouvons done en utiliser autant que nous voulons pour « aerer » notre code et le rendre plus lisible). 17.2.2 Un formulaire HTML pour /'acquisition des donnees Analysons a present le code HTML lui-meme. Nous y trouvons essentiellement un nouveau formulaire, qui comporte plusieurs paragraphes, parmi lesquels on peut reconnaitre quelques widgets. La ligne 10 indique le nom du script CGI auquel les donnees du formulaire seront transmises : il s'agira bien evidemment d'un autre script Python. A la ligne 12, on trouve la definition d'un widget de type « champ d'entree » (Balise INPUT, avec TYPE="text"). L'utilisateur est invite a y encoder son nom. Le parametre MAXLENGTH definit une longueur maximale pour la chaine de caracteres qui sera entree ici (20 caracteres, en l'occurrence). Le parametre SIZE definit la taille du champ tel qu'il doit apparaitre a l'ecran, et le parametre NAME est le nom que nous choisissons pour la variable destinee a memoriser la chaine de caracteres attendue. Un second champ d'entree un peu different est defini a la ligne 14 (balise TEXT ARE A). II s'agit d'un receptacle plus vaste, destine a accueillir des textes de plusieurs lignes. (Ce champ est automatiquement pourvu d'ascenseurs si le texte a inserer se revele trop volumineux). Ses parametres ROWS et COLS sont assez explicites. Entre les balises initiale et finale, on peut inserer un texte par defaut (« Mississippi » dans notre exemple). Comme dans l'exemple precedent, la ligne 16 contient la definition du bouton qu'il faudra actionner pour transmettre les donnees au script CGI destinataire, lequel est decrit ci-apres. Gerard Swinnen : Apprendre a programmer avec Python 267. 17.2.3 Un script CGI pour le traitement des donnees Le mecanisme utilise a l'interieur d'un script CGI pour receptionner les donnees transmises par un formulaire HTML est fort simple, comme vous pouvez l'analyser dans l'exemple ci-dessous : 1. #! /usr/bin/python 2 . # Traitement des donnees transmises par un formulaire HTML 3. 4. import cgi # Module d' interface avec le serveur web 5. form = cgi . FieldStorage () # Reception de la requete utilisateur : 6. # il s'agit d'une sorte de dictionnaire 7. if form. has_key ( "phrase" ) : # La cle n'existera pas si le champ 8. text = form ["phrase"] .value # correspondant est reste vide 9. else: 10. text ="*** le champ phrase etait vide ! ***" 11. 12. if form.has_key ("visiteur") : # La cle n'existera pas si le champ 13. nomv = form [ "visiteur" ]. value # correspondant est reste vide 14. else: 15. nomv ="mais vous ne m'avez pas indique votre nom" 16. 17. print "Content-Type: text/html\n" 18. print """ 19.

Merci, %s !

20.

La phrase que vous m'avez fournie etait :

21.

%s " " " % (nomv, text) 22 . 23. histogr ={} 24 . for c in text : 25. histogr [c] = histogr . get (c, 0) +1 26. 27. liste = histogr. items () # conversion en une liste de tuples 28. liste. sort () # tri de la liste 29. print "

Frequence de chaque caractere dans la phrase :

" 30. for c, f in liste: 31. print ' le caractere "%s" apparait %s fois
' % (c, f) Les lignes 4 et 5 sont les plus importantes : Le module cgi importe a la ligne 4 assure la connexion du script Python avec l'interface CGI , laquelle permet de dialoguer avec le serveur web. A la ligne 5, la fonction FieldStorage() de ce module renvoie un objet qui contient l'ensemble des donnees transmises par le formulaire HTML. Nous placons cet objet, lequel est assez semblable a un dictionnaire classique, dans la variable form. Par rapport a un veritable dictionnaire, l'objet place dans form presente la difference essentielle qu'il faudra lui appliquer la methode valuefj pour en extraire les donnees. Les autres methodes applicables aux dictionnaires, telles la methode has_key() , par exemple, peuvent etre utilisees de la maniere habituelle. Une caracteristique importante de l'objet dictionnaire retourne par FieldStorage() est qu'iV ne possedera aucune cle pour les champs laisses vides dans le formulaire HTML correspondant. Dans notre exemple, le formulaire comporte deux champs d'entree, auxquels nous avons associe les noms « visiteur » et « phrase ». Si ces champs ont effectivement ete completes par l'utilisateur, nous trouverons leurs contenus dans l'objet dictionnaire, aux index « visiteur » et « phrase ». Par contre, si l'un ou l'autre de ces champs n'a pas ete complete, l'index correspondant n'existera tout simplement pas. Avant toute forme de traitement de valeurs, il est done indispensable de s'assurer 268. Gerard Swinnen : Apprendre a programmer avec Python de la presence de chacun des index attendus, et c'est ce que nous faisons aux lignes 7 a 15. (17) Exercice : 17.1. Pour verifier ce qui precede, vous pouvez par exemple desactiver (en les transformant en commentaires) les lignes 7, 9, 10, 12, 14 & 15 du script. Si vous testez le fonctionnement de l'ensemble, vous constaterez que tout se passe bien si l'utilisateur complete effectivement les champs qui lui sont proposes. Si l'un des champs est laisse vide, par contre, une erreur se produit. Note importante : le script etant lance par l'intermediaire d'une page web, les messages d'erreur de Python ne seront pas affiches dans cette page, mais plutot enregistres dans le journal des evenements du serveur web. Veuillez consulter l'administrateur de ce serveur pour savoir comment vous pouvez acceder a ce journal. De toute maniere, attendez-vous a ce que la recherche des erreurs dans un script CGI soit plus ardue que dans une application ordinaire. Le reste du script est assez classique. • Aux lignes 17 a 21, nous ne faisons qu'afficher les donnees transmises par le formulaire. Veuillez noter que les variables nomv et text doivent exister au prealable, ce qui rend indispensables les lignes 9, 10, 14 & 15. • Aux lignes 23, 24 & 25, nous nous servons d'un dictionnaire pour construire un histogramme simple, comme nous l'avons explique a la page 149. • A la ligne 27, nous convertissons le dictionnaire resultant en une liste de tuples, pour pouvoir trier celle-ci dans l'ordre alphabetique a la ligne 28. • La boucle for des lignes 30 et 3 1 se passe de commentaires. 17.3 Un serveur web en pur Python ! Dans les pages precedentes, nous vous avons explique quelques rudiments de programmation CGI afin que vous puissiez mieux comprendre comment fonctionne une application web. Mais si vous voulez veritablement developper une telle application (par exemple un site web personnel dote d'une certaine interactivite), vous constaterez rapidement que l'interface CGI est un outil trop sommaire. Son utilisation telle quelle dans des scripts se revele fort lourde, et il est done preferable de faire appel a des outils plus elabores. L'interet pour le developpement web est devenu tres important, et il existe done une forte demande pour des interfaces et des environnements de programmation bien adaptes a cette tache. Or, meme s'il ne peut pas pretendre a l'universalite de langages tels que C/C++, Python est deja largement utilise un peu partout dans le monde pour ecrire des programmes tres ambitieux, y compris dans le domaine des serveurs d'applications web. La robustesse et la facilite de mise en oeuvre du langage ont seduit de nombreux developpeurs de talent, qui ont realise des outils de developpement web de tres haut niveau. Plusieurs de ces applications peuvent vous interesser si vous souhaitez realiser vous-meme des sites web interactifs de differents types. Les produits existants sont pour la plupart des logiciels libres. lis permettent de couvrir une large gamme de besoins, depuis le petit site personnel de quelques pages, jusqu'au gros site commercial collaboratif, capable de repondre a des milliers de requetes journalieres, et dont les differents secteurs sont geres sans interference par des personnes de competences variees (infographistes, programmeurs, specialistes de bases de donnees, etc.). Gerard Swinnen : Apprendre a programmer avec Python 269. Le plus celebre de ces produits est le logiciel Zope, deja adopte par de grands organismes prives et publics pour le developpement d' intranets et d'extranets collaboratifs. II s'agit en fait d'un systeme serveur d'applications, tres performant, securise, presqu'entierement ecrit en Python, et que Ton peut administrer a distance a l'aide d'une simple interface web. II ne nous est pas possible de decrire l'utilisation de Zope dans ces pages : le sujet est trop vaste, et un livre entier n'y suffirait pas. Sachez cependant que ce produit est parfaitement capable de gerer de tres gros sites d'entreprise en offrant d'enormes avantages par rapport a des solutions classiques telles que PHP ou Java. D'autres outils moins ambitieux mais tout aussi interessants sont disponibles. Tout comme Zope, la plupart d'entre eux peuvent etre telecharges librement depuis l'internet. Le fait qu'ils soient ecrits en Python assure en outre leur portabilite : vous pourrez done les employer aussi bien sous Windows que sous Linux ou MacOs. Chacun d'eux peut etre utilise en conjonction avec un serveur web « classique » tel que Apache ou Xitami (e'est preferable si le site a realiser est destine a supporter une charge de connexions tres importante), mais certains d'entre eux integrent en outre leur propre serveur web, ce qui leur permet de fonctionner egalement de maniere tout a fait autonome. Cette possibilite se revele particulierement interessante au cours de la mise au point d'un site, car elle facilite la recherche des erreurs. Cette totale autonomic alliee a la grande facilite de leur mise en oeuvre fait de ces produits de fort bonnes solutions pour la realisation de sites web d'intranet specialises, notamment dans des petites et moyennes entreprises, des administrations, ou dans des ecoles. Si vous souhaitez developper une application Python qui soit accessible par l'intermediaire d'un simple navigateur web, via un intranet d'entreprise (ou meme via l'internet, si la charge previsible n'est pas trop importante), ces applications sont faites pour vous. II en existe une grande variete : Poor man's Zope, Spyce, Karrigell, Webware, Cherrypy, Quixote, Twisted, etc. Choisissez en fonction de vos besoins : vous n'aurez que l'embarras du choix. Dans les lignes qui suivent, nous allons decrire une petite application web fonctionnant a l'aide de Karrigell. Vous pouvez trouver ce systeme a l'adresse : http://karrigell.sourceforge.net. II s'agit d'une solution de developpement web simple, bien documented en anglais et en francais (son auteur, Pierre Quentel, est en effet originaire de Bretagne, tout comme le mot karrigell, d'ailleurs, lequel signifie « charrette »). 17.3.1 Installation de Karrigell L'installation de Karrigell est un jeu d'enfant : il vous suffit d'extraire dans un repertoire quelconque le fichier archive que vous aurez telecharge depuis l'internet. L'operation de desarchivage cree automatiquement un sous-repertoire nomme Karrigell-nwffieiro de version. C'est ce repertoire que nous considererons comme repertoire racine dans les lignes qui suivent. Si vous ne comptez pas utiliser le serveur de bases de donnees Gadfly 69 qui vous est fourni en complement de Karrigell lui-meme, c'est tout ! Sinon, entrez dans le sous-repertoire gadfly-1.0.0 et lancez la commande : python setup. py install (Sous Linux, il faut etre root). Vous devez effectuer cette operation si vous souhaitez visualiser la totalite de la demonstration integree. 17.3.2 Demarrage du serveur : II s'agit done bel et bien de mettre en route un serveur web, auquel vous pourrez acceder ensuite a l'aide d'un navigateur quelconque, localement ou par l'intermediaire d'un reseau. Avant de le faire demarrer, il est cependant conseille de jeter un petit coup d'oeil dans son fichier de configuration, lequel se nomme Karrigell.ini et se trouve dans le repertoire -racine. Par defaut, Karrigell attend les requetes http sur le port n° 80. Et c'est bien ce numero de port 69 Voyez le chapitre precedent : Gadfly est un serveur de bases de donnees ecrit en Python. 270. Gerard Swinnen : Apprendre a programmer avec Python que la plupart des logiciels navigateurs utilisent eux-memes par defaut. Cependant, si vous installez Karrigell sur une machine Linux dont vous n'etes pas l'administrateur, vous n'avez pas le droit d'utiliser les numeros de port inferieurs a 1024 (pour des raisons de securite). Si vous etes dans ce cas, vous devez done modifier le fichier de configuration afin que Karrigell utilise un numero de port plus eleve. En general, vous choisirez d'enlever simplement le caractere # au debut de la ligne 39, ce qui activera l'utilisation du n° de port 8080. Plus tard, vous souhaiterez peut-etre encore modifier le fichier de configuration afin de modifier l'emplacement du repertoire racine pour votre site web (par defaut, e'est le repertoire du serveur lui-meme). Une fois le fichier de configuration modifie, entrez dans le repertoire racine du serveur, si vous n'y etes pas deja, et lancez simplement la commande : python Karrigell. py C'est tout. Votre serveur Karrigell se met en route, et vous pouvez en verifier le fonctionnement tout de suite a l'aide de votre navigateur web prefere. Si vous lancez celui-ci sur la meme machine que le serveur, vous le dirigerez vers une adresse telle que : http://localhost:8080/index.html, « localhost » etant le terme consacre pour designer la machine locale, « 8080 » le numero de port choisi dans le fichier de configuration, et « index.html » le nom du fichier qui contient la page d'accueil du site. Par contre, si vous voulez acceder a cette meme page d'accueil depuis une autre machine, vous devrez (dans le navigateur de celle-ci) indiquer le nom ou l'adresse IP du serveur, en lieu et place de localhost. Avec l'adresse indiquee au paragraphe precedent 70 , vous atteindrez la page d'accueil d'un site de demonstration de Karrigell, qui est deja pre-installe dans le repertoire racine. Vous y retrouverez la documentation de base, ainsi que toute une serie d'exemples. Dans ce qui precede, il est sous-entendu que vous avez lance le serveur depuis une console texte, ou depuis une fenetre de terminal. Dans un cas comme dans l'autre, les messages de controle emis par le serveur apparaitront dans cette console ou cette fenetre. C'est la que vous pourrez rechercher des messages d'erreur eventuels. C'est la aussi que vous devrez intervenir si vous voulez arreter le serveur (avec la combinaison de touches CTRL-C). 17.3.3 Ebauche de site web Essayons a present de realiser notre propre ebauche de site web. A la difference d'un serveur web classique, Karrigell peut gerer non seulement des pages HTML statiques (fichiers .htm, .html, .gif, . jpg, .ess) mais egalement : • des scripts Python (fichiers .py) • des scripts hybrides Python Inside HTML (fichiers .pih) • des scripts hybrides HTML Inside Python (fichiers .hip) Laissons de cote les scripts hybrides, dont vous pourrez etudier vous-meme la syntaxe (par ailleurs tres simple) si vous vous lancez dans une realisation d'une certaine importance (ils pourront vous faciliter la vie). Dans le contexte limite de ces pages, nous nous contenterons de quelques experiences de base avec des scripts Python ordinaires. Comme tous les autres elements du site (fichiers .html, .gif, .jpeg, etc.), ces scripts Python devront etre places dans le repertoire racine 71 . Vous pouvez tout de suite effectuer un test 70 Si vous avez laisse en place le n° de port par defaut (80), il est inutile de le rappeler dans les adresses, puisque c'est ce n° de port qui est utilise par defaut par la plupart des navigateurs. Une autre convention consiste a considerer que la page d'accueil d'un site Web se trouve presque toujours dans un fichier nomme index. htm ou index.html, Lorsque Ton souhaite visiter un site Web en commencant par sa page d'accueil, on peut done en general omettre ce nom dans l'adresse. Karrigell respecte cette convention, et vous pouvez done vous connecter en utilisant une adresse simplifiee telle que : http: //localhost: 8080 ou meme : http:/ /localhost (si le n° de port est 80). 71 ...ou bien dans des sous-repertoires du repertoire racine, comme il est d'usage de le faire lorsque Ton cherche a Gerard Swinnen : Apprendre a programmer avec Python 211. elementaire en redigeant un petit script d'une seule ligne, tel que : print "Bienvenue sur mon site web ! " Sauvegardez ce script sous le nom hello.py dans le repertoire racine, puis entrez l'adresse : http://localhostfhello.py (ou meme : http://localhost/hello - l'extension .py peut etre omise) dans votre navigateur. Vous devriez y voir apparaitre le message. Cela signifie done que dans l'environnement Karrigell, la sortie de l'instruction print est redirigee vers la fenetre du navigateur client, plutot que la console (ou la fenetre de terminal) du serveur. Etant donne que l'affichage a lieu dans une fenetre de navigateur web, vous pouvez utiliser toutes les ressources de la syntaxe HTML afin d'obtenir un formatage determine. Vous pouvez par exemple afficher un petit tableau de 2 lignes et 3 colonnes, avec les instructions suivantes : print """ TABLE > T? Tl T? Rappelons que la balise TABLE definit un tableau. Son option BORDER specifie la largeur des bordures de separation, et CELLP ADDING l'ecart a reserver autour du contenu des cellules. Les Balises TR et TD (Table Row et Table Data) definissent les lignes et les cellules du tableau. Vous pouvez bien entendu utiliser egalement toutes les ressources de Python, comme dans l'exemple ci- dessous ou nous construisons une table des sinus, cosinus et tangentes des angles compris entre 0° et 90°, a l'aide d'une boucle classique. Ligne 7 : Nous nous servons de la fonction range() pour definir la gamme d'angles a couvrir (de zero a 60 degres par pas de 10). Ligne 9 : Les fonctions trigonometriques de Python necessitent que les angles soient exprimes en radians. II faut done effectuer une conversion. Ligne 12 : Chaque ligne du tableau comporte quatre valeurs, lesquelles sont mises en forme a l'aide du systeme de formatage des chaines de caracteres decrit deja a la page 130 : le marqueur de conversion « %8.7f » force un affichage a 8 chiffres, dont 7 apres la « virgule » decimale. Le marqueur « %8.7g » fait a peu pres la meme chose, mais passe a la notation scientifique lorsque e'est necessaire. from math import sin, cos, tan, File Edit View Go Bookmarks m * #Getting Started & ® [lT@ [el 1 . 2 . 3. 4 . 5. 6. 7 . 8. 9. 10 11 Angle Sinus C osinus Tangente 0 0.0000000 1.0000000 0 10 0.1736482 0.9848078 0.176327 20 0.3420201 0.9396926 0.3639702 30 0.5000000 0.8660254 0.5773503 40 0.6427876 0.7660444 0.8390996 50 0.7660444 0.6427876 1 1.191754 60 0.8660254 0.5000000 1.732051 Done pi # Construction de l'en-tete du tableau avec les titres de colonnes : print " " "
Rouge Vert Bleu
15 % 62 % 23 %
AngleSinusCosinusTangente" " " for angle in range (0, 62, 10) : # conversion des degres en radians : aRad = angle * pi / 180 # construction d'une ligne de tableau, en exploitant le formatage des # chaines de caracteres pour fignoler l'affichage : structurer convenablement le site en construction. II vous suffira dans ce cas d'inclure le nom de ces sous- repertoires dans les adresses correspondantes. 272. Gerard Swinnen : Apprendre a programmer avec Python 12 . print "
%s%8 . 7f%8 . 7f%8 . 7g"\ 13. % (angle, sin(aRad), cos (aRad) , tan (aRad) ) 14. 15. print " TABLE >" A ce stade, vous vous demandez peut-etre ou se situe la difference entre ce que nous venons d'experimenter ici et un script CGI classique (tels ceux des pages 266 et suivantes). L'interet de travailler dans un environnement plus specifique tel que Karrigell apparait cependant tres vite si vous faites des erreurs. En programmation CGI classique, les messages d'erreur emis par l'interpreteur Python ne s'affichent pas dans la fenetre du navigateur. lis sont enregistres dans un fichier journal du serveur (Apache, par exemple), ce qui ne facilite pas leur consultation. Avec un outil comme Karrigell, par contre, vous disposez d'une signalisation tres efficace, ainsi que d'un outil de deboguage complet. Faites l'experience d'introduire une petite erreur dans le script ci-dessus, et relancez votre navigateur sur la page modifiee. Par exemple, en supprimant le double point a la fin de la ligne 7, nous avons obtenu nous-memes l'affichage suivant : File Edit View Go Bookmarks lools Help O 0 4p - & - & O ft I u http://localhost:8080/triglH (E2, ♦Getting Started £3 Latest Headlines Error in /trigono2.py Scrl pt /trigono2 . py SyntaxError Li ne 7 for angle in r ange (0 . 91 . 10) Traceback (most recent call last) : File "/wi ndows/D/dos_data/py thon/Kar r igell-2 . 0 . 5/Template . py " . line 186. in render exec pythonCode in ns File "", line 7 for angle in range(@,91. 10) A SyntaxError: invalid syntax Debug | Done En cliquant sur le bouton « Debug », on obtient encore une foule d'informations complementaires (affichage du script complet, variables d'environnement, etc.). 17.3.4 Prise en charge des sessions Lorsque Ton elabore un site web interactif, on souhaite frequemment que la personne visitant le site puisse s'identifier et fournir un certain nombre de renseignements tout au long de sa visite dans differentes pages (l'exemple type etant le remplissage d'un « caddy » au cours de la consultation d'un site commercial), toutes ces informations etant conservees quelque part jusqu'a la fin de sa visite. Et il faut bien entendu realiser cela independamment pour chaque client connecte. II serait possible de transmettre les informations de page en page a l'aide de champs de formulaires caches, mais ce serait complique et tres contraignant. II est preferable que le systeme serveur soit dote d'un mecanisme specifique, qui attribue a chaque client une session particuliere. Gerard Swinnen : Apprendre a programmer avec Python 273. Karrigell realise cet objectif par l'intermediaire de cookies. Lorsqu'un nouveau visiteur du site s'identifie, le serveur genere un cookie appele sessionld et l'envoie au navigateur web, qui l'enregistre. Ce cookie contient un « identifiant de session » unique, auquel correspond un objet- session sur le serveur. Lorsque le visiteur parcourt les autres pages du site, son navigateur renvoie a chaque fois le contenu du cookie au serveur, et celui-ci peut done retrouver l'objet-session correspondant, a l'aide de son identifiant. L'objet-session reste done disponible tout au long de la visite de l'internaute : il s'agit d'un objet Python ordinaire, dans lequel on memorise un nombre quelconque d' informations sous forme d'attributs. Au niveau de la programmation, voici comment cela se passe : Pour chaque page dans laquelle vous voulez consulter ou modifier une information de session, vous commencez par creer un objet de la classe Session() : ob jet_session = Session () Si vous etes au debut de la session, Karrigell genere un identifiant unique, le place dans un cookie et envoie celui-ci au navigateur web. Vous pouvez alors ajouter un nombre quelconque d'attributs a l'objet-session : ob jet_session . nom = "Jean Dupont" Dans les autres pages, vous procedez de la meme maniere, mais l'objet produit dans ce cas par la classe Session() n'est pas nouveau : e'est l'objet cree en debut de session, retrouve en interne par le serveur grace a son identifiant relu dans le cookie. Vous pouvez acceder aux valeurs de ses attributs, et aussi en ajouter de nouveaux : obj_sess = Session () # recuperer l'objet indique par le cookie om = ob j_sess . nom # retrouver la valeur d'un attribut existant obj_sess . article = 49137 # ajouter un nouvel attribut Les objets-sessions prennent aussi en charge une methode close(), qui a pour effet d'effacer l'information de session. Vous n'etes cependant pas oblige de clore explicitement les sessions : Karrigell s'assure de toute facon qu'il n'y ait jamais plus de 1000 sessions simultanees : il efface les plus anciennes quand on arrive a la 1000 eme . Exemple de mise en oeuvre : Sauvegardez les trois petit* scripts ci-dessous dans le repertoire-racine. Le premier genere un formulaire HTML similaire a ceux qui ont ete decrits plus haut. Nommez-le sessionTestl.py : I. # Affichage d'un formulaire d' inscription : 2 . 3. print """ 4.

Veuillez vous identifier, SVP :

5. 6. """ Le suivant sera nomme sessionTest2.py. C'est le script mentionne dans la balise d'ouverture du formulaire ci-dessus a la ligne 6, et qui sera invoque lorsque l'utilisateur actionnera le bouton mis en place a la ligne 10. Ce script recevra les valeurs entrees par l'utilisateur dans les differents champs du formulaire, par l'intermediaire d'un dictionnaire de requete situe dans la variable 274. Gerard Swinnen : Apprendre a programmer avec Python d'environnement QUERY de Karrigell I. obSess = Session () 2. 3. obSess.nom = QUERY [ "nomClient" ] 4. obSess .prenom = QUERY [ "prenomClient" ] 5. obSess. sexe = QUERY [ "sexeClient" ] 6. 7. if obSess . sexe .upper () == "M" : 8. vedette ="Monsieur" 9. else: 10. vedette ="Madame" II. print "

Bienvenue, %s %s

" % (vedette, obSess.nom) 12. print "" 13. print """ 14. Suite ... " " " La premiere ligne de ce script cree l'objet-session, genere pour lui un identifiant unique, et expedie celui-ci au navigateur sous la forme d'un cookie. Dans les lignes 3, 4, 5, on recupere les valeurs entrees dans les champs du formulaire precedent, en utilisant leurs noms comme cles d'acces au dictionnaire de requetes. La ligne 14 definit un lien http pointant vers le troisieme script, nomme sessionTest3.py : 1. suiviSess = Session () # retrouver l'objet-session 2. suiviSess . article = 12345 # lui ajouter des attributs 3. suiviSess .prix = 43.67 4. 5. print """ 6.

Page suivante

7. Suivi de la commande du client :
%s %s
8. Article n° %s, Prix : %s 9. """ % (suiviSess . prenom, suiviSess . nom, 10. suiviSess . article, suiviSess .prix) Dirigez votre navigateur web vers l'adresse : http://localhost:8080/sessionTestl . Entrez des valeurs de votre choix dans les champs du formulaire, et cliquez sur le bouton OK : 72 Karrigell met en place un certain nombre de variables globales dont les noms sont en majuscules pour eviter un conflit eventuel avec les votres. Celle-ci joue le meme role que la fonction FieldStorageQ du module cgi. Veuillez consulter la documentation de Karrigell si vous souhaitez obtenir des explications plus detaillees. Gerard Swinnen : Apprendre a programmer avec Python 275. File Edit View Go Bookmarks Toe ♦ Getting Started B Latest Headlines Veuillez vous identifier, SVP : Votre Dom : |Dupont Votre prenoni : |Charles Votre sexe (ni/f) : |m OK Done 1 ■ <-■■ - .zl°l*l File Edit View Go Bookmarks Toe • l; • & © lu H (El ♦ Getting Started 13 Latest Headlines Bienvenue, Monsieur Dnpont Suite... Done Comme attendu, les informations entrees dans le formulaire sont transmises a la deuxieme page. A present, si vous cliquez sur le lien : « Suite... » dans celle-ci, vous dirigez encore une fois votre navigateur vers une nouvelle page, mais celle-ci n'aura fait l'objet d'aucune transmission de donnees (puisqu'on n'y accede pas par l'intermediaire d'un formulaire). Dans le script sessionTest3.py qui genere cette page, vous ne pouvez done pas utiliser la variable QUERY pour retrouver les informations entrees par le visiteur. C'est ici qu'intervient le mecanisme des objets-sessions. Lors du lancement de ce troisieme script, le cookie memorise par le navigateur est relu par le serveur, ce qui lui permet de regenerer l'objet-session cree dans le script precedent. File Edit View Go Bookmarks ♦ Getting Started B Latest Headlines Page suivante Suivi de la commande du client : Charles Dupont Article n° 12345, Prix : 43.67 Done Analysez les trois premieres lignes du script sessionTest3.py : l'objet suiviSess instancie a partit de la classe Session() est l'objet-session regenere. II contient les informations sauvegardees a la page precedente, et on peut lui en ajouter d'autres dans des attributs supplementaires. Vous aurez compris que vous pouvez desormais recuperer toutes ces informations de la meme maniere dans n'importe quelle autre page, car elles persisteront jusqu'a ce que l'utilisateur termine sa visite du site, a moins que vous ne fermiez vous-meme cette session par programme, a l'aide de la methode close() evoquee plus haut. Exercice : 17.2. Ajoutez au script precedent un lien vers une quatrieme page, et ecrivez le script qui 276. Gerard Swinnen : Apprendre a programmer avec Python generera celle-ci. Les informations devront cette fois etre affichees dans un tableau : Nom Prenom Sexe Article Prix 17.3.5 Autres developpements Nous terminons ici cette breve etude de Karrigell, car il nous semble vous avoir explique l'essentiel de ce qu'il vous faut connaitre pour demarrer. Si vous desirez en savoir davantage, il vous suffira de consulter la documentation et les exemples fournis avec le produit. Comme nous l'avons deja signale plus haut, l'installation de Karrigell inclut l'installation du systeme de bases de donnees Gadfly. Vous pouvez done tres rapidement et tres aisem*nt realiser un site interactif permettant la consultation a distance d'un ensemble de donnees quelconques, en admettant bien entendu que la charge de requetes de votre site reste moderee, et que la taille de la base de donnees elle-meme ne devienne pas gigantesque. N'esperez pas gerer a l'aide de Karrigell un site commercial susceptible de traiter plusieurs millions de requetes journalieres ! Si vous ambitionnez de realiser ce genre de choses, il vous faudra etudier d'autres offres logicielles, comme par exemple CherryPy ou Zope associes a Apache pour le systeme serveur, et SQLite, MySQL ou PostgreSQL pour le gestionnaire de bases de donnees. Gerard Swinnen : Apprendre a programmer avec Python 277. Chapitre 18 : Communications a travers un reseau Le developpement extraordinaire de l'internet a amplement demontre que les ordinateurs peuvent etre des outils de communication tres efficaces. Dans ce chapitre, nous allons experimenter la plus simple des techniques d'interconnexion de deux programmes, qui leur permette de s'echanger des informations par l'intermediaire d'un reseau. Pour ce qui va suivre, nous supposerons done que vous collaborez avec un ou plusieurs de vos condisciples, et que vos postes de travail Python sont connectes a un reseau local dont les communications utilisent le protocole TCP/IP. Le systeme d'exploitation n'a pas d'importance : vous pouvez par exemple installer l'un des scripts Python decrits ci-apres sur un poste de travail fonctionnant sous Linux, et le faire dialoguer avec un autre script mis en oeuvre sur un poste de travail confie aux bons soins d'un systeme d'exploitation different, tel que MacOS ou Windows. Vous pouvez egalement experimenter ce qui suit sur une seule et meme machine, en mettant les differents scripts en oeuvre dans des fenetres independantes. 18.1 Les sockets Le premier exercice qui va vous etre propose consistera a etablir une communication entre deux machines seulement. L'une et l'autre pourront s'echanger des messages a tour de role, mais vous constaterez cependant que leurs configurations ne sont pas symetriques. Le script installe sur l'une de ces machines jouera en effet le role d'un logiciel serveur, alors que l'autre se comportera comme un logiciel client. Le logiciel serveur fonctionne en continu, sur une machine dont l'identite est bien definie sur le reseau grace a une adresse IP specifique 73 . II guette en permanence l'arrivee de requetes expediees par les clients potentiels en direction de cette adresse, par l'intermediaire d'un port de communication bien determine. Pour ce faire, le script correspondant doit mettre en oeuvre un objet logiciel associe a ce port, que Ton appelle un socket. Au depart d'une autre machine, le logiciel client tente d'etablir la connexion en emettant une requete appropriee. Cette requete est un message qui est confie au reseau, un peu comme on confie une lettre a la Poste. Le reseau pourrait en effet acheminer la requete vers n'importe quelle autre machine, mais une seule est visee : pour que la destination visee puisse etre atteinte, la requete contient dans son en-tete l'indication de l'adresse IP et du port de communication destinataires. Lorsque la connexion est etablie avec le serveur, le client lui assigne lui-meme l'un de ses propres ports de communication. A partir de ce moment, on peut considerer qu'un canal privilegie relie les deux machines, comme si on les avait connectees l'une a l'autre par l'intermediaire d'un fil (les deux ports de communication respectifs jouant le role des deux extremites de ce fil). L'echange d'informations proprement dit peut commencer. Pour pouvoir utiliser les ports de communication reseau, les programmes font appel a un ensemble de procedures et de fonctions du systeme d'exploitation, par l'intermediaire d'objets interfaces que Ton appelle des sockets. Ceux-ci peuvent mettre en oeuvre deux techniques de communication differentes et complementaires : celle des paquets (que Ton appelle aussi des datagrammes), tres largement utilisee sur l'internet, et celle de la connexion continue, ou stream socket, qui est un peu plus simple. 73 Une machine particuliere peut egalement etre designee par un nom plus explicite, mais a la condition qu'un mecanisme ait ete mis en place sur le reseau (DNS) pour traduire automatiquement ce nom en adresse IP. Veuillez consulter votre cours sur les systemes d'exploitation et les reseaux pour en savoir davantage. 278. Gerard Swinnen : Apprendre a programmer avec Python 18.2 Construction d'un serveur elementaire Pour nos premieres experiences, nous allons utiliser la technique des stream sockets. Celle-ci est en effet parfaitement appropriee lorsqu'il s'agit de faire communiquer des ordinateurs interconnects par l'intermediaire d'un reseau local. C'est une technique particulierement aisee a mettre en oeuvre, et elle permet un debit eleve pour l'echange de donnees. L'autre technologie (celle des paquets) serait preferable pour les communications expediees via l'internet, en raison de sa plus grande fiabilite (les memes paquets peuvent atteindre leur destination par differents chemins, etre emis ou re-emis en plusieurs exemplaires si cela se revele necessaire pour corriger les erreurs de transmission), mais sa mise en ceuvre est un peu plus complexe. Nous ne l'etudierons pas dans ce cours. Le script ci-dessous met en place un serveur capable de communiquer avec un seul client. Nous verrons un peu plus loin ce qu'il faut lui aj outer afin qu'il puisse prendre en charge en parallele les connexions de plusieurs clients. 1. # Definition d'un serveur reseau rudimentaire 2. # Ce serveur attend la connexion d'un client, pour entamer un dialogue avec lui 3. 4. import socket, sys 5. 6. HOST = '192.168.14.152' 7. PORT = 50000 8. 9. #1) creation du socket : 10. mySocket = socket . socket (socket .AF_INET, socket . SOCK_STREAM) 11. 12. #2) liaison du socket a une adresse precise : 13. try: 14. mySocket. bind ( (HOST, PORT)) 15. except socket . error : 16. print "La liaison du socket a 1' adresse choisie a echoue . " 17 . sys .exit () 18. 19. while 1: 20. #3) Attente de la requete de connexion d'un client : 21. print "Serveur pret, en attente de requetes ..." 22. mySocket . listen (5) 23. 24. #4) Etablissem*nt de la connexion : 25. connexion, adresse = mySocket . accept ( ) 26. print "Client connecte, adresse IP %s, port %s" % (adresse [0], adresse [1]) 27. 28. #5) Dialogue avec le client : 29. connexion . send ( "Vous etes connecte au serveur Marcel. Envoyez vos messages.") 30. msgClient = connexion . recv (1024) 31. while 1: 32. print "C>", msgClient 33. if msgClient . upper () == "FIN" or msgClient =="": 34 . break 35. msgServeur = raw_input ( "S> ") 36. connexion . send (msgServeur) 37. msgClient = connexion . recv (1024) 38. 39. #6) Fermeture de la connexion : 40. connexion . send ( "Au revoir !") 41. print "Connexion interrompue . " 42. connexion . close ( ) 43. 44. ch = raw_input ( "ecommencer erminer ? ") 45. if ch. upper () == ' T ' : 46. break Gerard Swinnen : Apprendre a programmer avec Python 279. Commentaires : • Ligne 4 : Le module socket contient toutes les fonctions et les classes necessaires pour construire des programmes communiquants. Comme nous allons le voir dans les lignes suivantes, l'etablissem*nt de la communication comporte six etapes. • Lignes 6 & 7 : Ces deux variables definissent l'identite du serveur, telle qu'on l'integrera au socket. HOST doit contenir une chaine de caracteres indiquant l'adresse IP du serveur sous la forme decimale habituelle, ou encore le nom DNS de ce meme serveur (mais a la condition qu'un mecanisme de resolution des noms ait ete mis en place sur le reseau). PORT doit contenir un entier, a savoir le numero d'un port qui ne soit pas deja utilise pour un autre usage, et de preference une valeur superieure a 1024 (Cfr. votre cours sur les services reseau). • Lignes 9 & 10 : Premiere etape du mecanisme d'interconnexion. On instancie un objet de la classe socket(), en precisant deux options qui indiquent le type d'adresses choisi (nous utiliserons des adresses de type « internet ») ainsi que la technologie de transmission (datagrammes ou connexion continue {stream) : nous avons decide d'utiliser cette derniere). • Lignes 12 a 17 : Seconde etape. On tente d'etablir la liaison entre le socket et le port de communication. Si cette liaison ne peut etre etablie (port de communication occupe, par exemple, ou nom de machine incorrect), le programme se termine sur un message d'erreur. Remarque : la methode bind() du socket attend un argument du type tuple, raison pour laquelle nous devons enfermer nos deux variables dans une double paire de parentheses. • Ligne 19 : Notre programme serveur etant destine a fonctionner en permanence dans l'attente des requetes de clients potentiels, nous le lancons dans une boucle sans fin. • Lignes 20 a 22 : Troisieme etape. Le socket etant relie a un port de communication, il peut a present se preparer a recevoir les requetes envoyees par les clients. C'est le role de la methode listen(). L'argument qu'on lui transmet indique le nombre maximum de connexions a accepter en parallele. Nous verrons plus loin comment gerer celles-ci. • Lignes 24 a 26 : Quatrieme etape. Lorsqu'on fait appel a sa methode accept(), le socket attend indefiniment qu'une requete se presente. Le script est done interrompu a cet endroit, un peu comme il le serait si nous faisions appel a une fonction input() pour attendre une entree clavier. Si une requete est receptionnee, la methode accept() renvoie un tuple de deux elements : le premier est la reference d'un nouvel objet de la classe socket() 74 , qui sera la veritable interface de communication entre le client et le serveur, et le second un autre tuple contenant les coordonnees de ce client (son adresse IP et le n° de port qu'il utilise lui-meme). Lignes 28 a 30 : Cinquieme etape. La communication proprement dite est etablie. Les methodes send() et recv() du socket servent evidemment a remission et a la reception des messages, qui doivent etre de simples chaines de caracteres. Remarques : la methode send() renvoie le nombre d'octets expedies. L'appel de la methode recv 0 doit comporter un argument entier indiquant le nombre maximum d'octets a receptionner en une fois (Les octets surnumeraires sont mis en attente dans un tampon. lis sont transmis lorsque la meme methode recv() est appelee a nouveau). 74 Nous verrons plus loin l'utilite de creer ainsi un nouvel objet socket pour prendre en charge la communication, plutot que d'utiliser celui qui a deja cree a la ligne 10. En bref, si nous voulons que notre serveur puisse prendre en charge simultanement les connexions de plusieurs clients, il nous faudra disposer d'un socket distinct pour chacun d'eux, independamment du premier que Ton laissera fonctionner en permanence pour receptionner les requetes qui continuent a arriver en provenance de nouveaux clients. 280. Gerard Swinnen : Apprendre a programmer avec Python • Lignes 31 a 37 : Cette nouvelle boucle sans fin maintient le dialogue jusqu'a ce que le client decide d'envoyer le mot « fin » ou une simple chaine vide. Les ecrans des deux machines afficheront chacune revolution de ce dialogue. • Lignes 39 a 42 : Sixieme etape. Fermeture de la connexion. 18.3 Construction d'un client rudimentaire Le script ci-dessous definit un logiciel client complementaire du serveur decrit dans les pages precedentes. On notera sa grande simplicite. 1. # Definition d'un client reseau rudimentaire 2 . # Ce client dialogue avec un serveur ad hoc 3. 4. import socket, sys 5. 6. HOST = '192.168.14.152' 7. PORT = 50000 8. 9. #1) creation du socket : 10. mySocket = socket . socket (socket .AF_INET, socket . SOCK_STREAM) 11. 12. #2) envoi d'une requete de connexion au serveur : 13. try: 14. mySocket. connect ( (HOST, PORT)) 15. except socket . error : 16. print "La connexion a echoue." 17. sys. exit () 18. print "Connexion etablie avec le serveur." 19. 20. #3) Dialogue avec le serveur : 21. msgServeur = mySocket . recv (1024) 22 . 23. while 1: 24. if msgServeur . upper () == "FIN" or msgServeur =="": 25. break 26. print "S>", msgServeur 27. msgClient = raw_input("C> ") 28. mySocket . send (msgClient) 29. msgServeur = mySocket . recv (1024) 30. 31. #4) Fermeture de la connexion : 32. print "Connexion interrompue . " 33. mySocket . close () Commentaires : • Le debut du script est similaire a celui du serveur. L'adresse IP et le port de communication doivent etre ceux du serveur. • Lignes 12 a 18 : On ne cree cette fois qu'un seul objet socket, dont on utilise la methode connect () pour envoyer la requete de connexion. • Lignes 20 a 33 : Une fois la connexion etablie, on peut dialoguer avec le serveur en utilisant les methodes send() et recv() deja decrites plus haut pour celui-ci. Gerard Swinnen : Apprendre a programmer avec Python 281. 18.4 Gestion de plusieurs taches en parallele a I'aide des threads Le systeme de communication que nous avons elabore dans les pages precedentes est vraiment tres rudimentaire : d'une part il ne met en relation que deux machines seulement, et d'autre part il limite la liberte d'expression des deux interlocuteurs. Ceux-ci ne peuvent en effet envoyer des messages que chacun a leur tour. Par exemple, lorsque l'un d'eux vient d'emettre un message, son systeme reste bloque tant que son partenaire ne lui a pas envoye une reponse. Lorsqu'il vient de recevoir une telle reponse, son systeme reste incapable d'en receptionner une autre, tant qu'il n'a pas entre lui-meme un nouveau message, ... et ainsi de suite. Tous ces problemes proviennent du fait que nos scripts habituels ne peuvent s'occuper que d'une seule chose a la fois. Lorsque le flux d'instructions rencontre une fonction input(), par exemple, il ne se passe plus rien tant que l'utilisateur n'a pas introduit la donnee attendue. Et meme si cette attente dure tres longtemps, il n'est habituellement pas possible que le programme effectue d'autres taches pendant ce temps. Ceci n'est toutefois vrai qu'au sein d'un seul et meme programme : vous savez certainement que vous pouvez executer d'autres applications entretemps sur votre ordinateur, car les systemes d' exploitation modernes sont « multi-taches ». Les pages qui suivent sont destinees a vous expliquer comment vous pouvez introduire cette fonctionnalite multi-taches dans vos programmes, afin que vous puissiez developper de veritables applications reseau, capables de communiquer simultanement avec plusieurs partenaires. Veuillez a present considerer le script de la page precedents Sa fonctionnalite essentielle reside dans la boucle while des lignes 23 a 29. Or, cette boucle s'interrompt a deux endroits : • a la ligne 27, pour attendre les entrees clavier de l'utilisateur (fonction raw_input()) ; • a la ligne 29, pour attendre l'arrivee d'un message reseau. Ces deux attentes sont done successives, alors qu'il serait bien plus interessant qu'elles soient simultanees. Si e'etait le cas, l'utilisateur pourrait expedier des messages a tout moment, sans devoir attendre a chaque fois la reaction de son partenaire. II pourrait egalement recevoir n'importe quel nombre de messages, sans l'obligation d'avoir a repondre a chacun d'eux pour recevoir les autres. Nous pouvons arriver a ce resultat si nous apprenons a gerer plusieurs sequences d'instructions en parallele au sein d'un meme programme. Mais comment cela est-il possible ? Au cours de l'histoire de l'informatique, plusieurs techniques ont ete mises au point pour partager le temps de travail d'un processeur entre differentes taches, de telle maniere que celles-ci paraissent etre effectuees en meme temps (alors qu'en realite le processeur s'occupe d'un petit bout de chacune d'elles a tour de role). Ces techniques sont implementees dans le systeme d'exploitation, et il n'est pas necessaire de les detailler ici, meme s'il est possible d'acceder a chacune d'elles avec Python. Dans les pages suivantes, nous allons apprendre a utiliser celle de ces techniques qui est a la fois la plus facile a mettre en oeuvre, et la seule qui soit veritablement portable (elle est en effet supportee par tous les grands systemes d'exploitation) : on l'appelle la technique des processus legers ou threads 75 . Dans un programme d'ordinateur, les threads sont des flux d'instructions qui sont menes en parallele (quasi-simultanement), tout en partageant le meme espace de noms global. En fait, le flux d'instructions de n'importe quel programme Python suit toujours au moins un thread : le thread principal. A partir de celui-ci, d'autres threads « enfants » peuvent etre amorces, 75 Dans un systeme d'exploitation de type Unix (comme Linux), les differents threads d'un meme programme font partie d'un seul processus. II est egalement possible de gerer differents processus a I'aide d'un meme script Python (operation fork), mais l'explication de cette technique depasse largement le cadre de ce cours. 282. Gerard Swinnen : Apprendre a programmer avec Python qui seront executes en parallele. Chaque thread enfant se termine et disparait sans autre forme de proces lorsque toutes les instructions qu'il contient ont ete executees. Par contre, lorsque le thread principal se termine, il faut parfois s'assurer que tous ses threads enfants « meurent » avec lui. 18.5 Client gerant remission et la reception simultanees Nous allons maintenant mettre en pratique la technique des threads pour construire un systeme de « chat » 76 simplifie. Ce systeme sera constitute d'un seul serveur et d'un nombre quelconque de clients. Contrairement a ce qui se passait dans notre premier exercice, personne n'utilisera le serveur lui-meme pour communiquer, mais lorsque celui-ci aura ete mis en route, plusieurs clients pourront s'y connecter et commencer a s'echanger des messages. Chaque client enverra tous ses messages au serveur, mais celui-ci les re-expediera immediatement a tous les autres clients connectes, de telle sorte que chacun puisse voir l'ensemble du trafic. Chacun pourra a tout moment envoyer ses messages, et recevoir ceux des autres, dans n'importe quel ordre, la reception et remission etant gerees simultanement, dans des threads separes. Le script ci-apres definit le programme client. Le serveur sera decrit un peu plus loin. Vous constaterez que la partie principale du script (ligne 38 et suivantes) est similaire a celle de l'exemple precedent. Seule la partie « Dialogue avec le serveur » a ete remplacee. Au lieu d'une boucle while, vous y trouvez a present les instructions de creation de deux objets threads (aux lignes 49 et 50), dont on demarre la fonctionnalite aux deux lignes suivantes. Ces objets threads sont crees par derivation, a partir de la classe Thread() du module threading. lis s'occuperont independamment de la reception et le remission des messages. Les deux threads « enfants » sont ainsi parfaitement encapsules dans des objets distincts, ce qui facilite la comprehension du mecanisme. 1. # Definition d'un client reseau gerant en parallele 1' emission 2. # et la reception des messages (utilisation de 2 THREADS). 3. 4. host = '192.168.0.235' 5. port = 40000 6. 7. import socket, sys, threading 8. 9. class ThreadReception (threading. Thread) : 10. """objet thread gerant la reception des messages""" 11. def init (self, conn) : 12 . threading . Thread. init (self) 13. self . connexion = conn # ref. du socket de connexion 14. 15. def run (self): 16. while 1: 17. message_recu = self . connexion . recv (1024) 18. print "*" + message_recu + "*" 19. if message_recu =='' or message_recu. upper () == "FIN": 20. break 21. # Le thread se termine ici . 22. # On force la fermeture du thread : 23 . th_E . _Thread stop ( ) 24. print "Client arrete. Connexion interrompue . " 25. self . connexion . close ( ) 26. 27. class ThreadEmission (threading . Thread) : 28. """objet thread gerant 1' emission des messages""" 29. def init (self, conn) : 76 Le « chat » est l'occupation qui consiste a « papoter » par l'intermediaire d'ordinateurs. Les canadiens francophones ont propose le terme de clavardage pour designer ce « bavardage par claviers interposes ». Gerard Swinnen : Apprendre a programmer avec Python 283. 30 . threading . Thread . init (self) 31. self . connexion = conn # ref. du socket de connexion 32. 33. def run (self): 34 . while 1 : 35. message_emis = raw_input() 36 . self . connexion . send (message_emis) 37 . 38. # Programme principal - Etablissem*nt de la connexion : 39. connexion = socket . socket (socket . AF_INET, socket . SOCK_STREAM) 40. try: 41. connexion . connect ( (host, port)) 42. except socket . error : 43. print "La connexion a echoue." 44. sys.exit() 45. print "Connexion etablie avec le serveur." 46. 47 . # Dialogue avec le serveur : on lance deux threads pour gerer 48. # independamment 1' emission et la reception des messages : 49. th_E = ThreadEmiss ion (connexion) 50. th_R = ThreadReception (connexion) 51. th_E. start () 52. th_R. start () Commentaires : • Remarque generate : Dans cet exemple, nous avons decide de creer deux objets threads independants du thread principal, afin de bien mettre en evidence les mecanismes. Notre programme utilise done trois threads en tout, alors que le lecteur attentif aura remarque que deux pourraient suffire. En effet : le thread principal ne sert en definitive qu'a lancer les deux autres ! II n'y a cependant aucun interet a limiter le nombre de threads. Au contraire : a partir du moment ou Ton decide d'utiliser cette technique, il faut en profiter pour compartimenter l'application en unites bien distinctes. • Ligne 7 : Le module threading contient la definition de toute une serie de classes interessantes pour gerer les threads. Nous n'utiliserons ici que la seule classe Thread(), mais une autre sera exploitee plus loin (la classe Lock()), lorsque nous devrons nous preoccuper de problemes de synchronisation entre differents threads concurrents. • Lignes 9 a 25 : Les classes derivees de la classe Thread() contiendront essentiellement une methode run(). C'est dans celle-ci que Ton placera la portion de programme specifiquement confiee au thread. II s'agira souvent dune boucle repetitive, comme ici. Vous pouvez parfaitement considerer le contenu de cette methode comme un script independant, qui s'execute en parallele avec les autres composants de votre application. Lorsque ce code a ete completement execute, le thread se referme. • Lignes 16 a 20: Cette boucle gere la reception des messages. A chaque iteration, le flux d'instructions s'interrompt a la ligne 17 dans l'attente d'un nouveau message, mais le reste du programme n'est pas fige pour autant : les autres threads continuent leur travail independamment. • Ligne 19 : La sortie de boucle est provoquee par la reception d'un message 'fin' (en majuscules ou en minuscules), ou encore d'un message vide (c'est notamment le cas si la connexion est coupee par le partenaire). Quelques instructions de « nettoyage » sont alors executees, et puis le thread se termine. • Ligne 23 : Lorsque la reception des messages est terminee, nous souhaitons que le reste du programme se termine lui aussi. II nous faut done forcer la fermeture de l'autre objet thread, celui que nous avons mis en place pour gerer remission des messages. Cette fermeture forcee peut etre 284. Gerard Swinnen : Apprendre a programmer avec Python obtenue a l'aide de la methode _Thread stop() 77 . • Lignes 27 a 36 : Cette classe definit done un autre objet thread, qui contient cette fois une boucle de repetition perpetuelle. II ne se pourra done se terminer que contraint et force par methode decrite au paragraphe precedent. A chaque iteration de cette boucle, le flux d'instructions s'interrompt a la ligne 35 dans l'attente d'une entree clavier, mais cela n'empeche en aucune maniere les autres threads de faire leur travail. • Lignes 38 a 45 : Ces lignes sont reprises a l'identique des scripts precedents. • Lignes 47 a 52 : Instanciation et demarrage des deux objets threads « enfants ». Veuillez noter qu'il est recommande de provoquer ce demarrage en invoquant la methode integree start(), plutot qu'en faisant appel directement a la methode run() que vous aurez definie vous-meme. Sachez egalement que vous ne pouvez invoquer start() qu'une seule fois (une fois arrete, un objet thread ne peut pas etre redemarre). 18.6 Serveur gerant les connexions de plusieurs clients en par allele Le script ci-apres cree un serveur capable de prendre en charge les connexions d'un certain nombre de clients du meme type que ce que nous avons decrit dans les pages precedentes. Ce serveur n'est pas utilise lui-meme pour communiquer : ce sont les clients qui communiquent les uns avec les autres, par l'intermediaire du serveur. Celui-ci joue done le role d'un relais : il accepte les connexions des clients, puis attend l'arrivee de leurs messages. Lorsqu'un message arrive en provenance d'un client particulier, le serveur le re-expedie a tous les autres, en lui ajoutant au passage une chaine d'identification specifique du client emetteur, afin que chacun puisse voir tous les messages, et savoir de qui ils proviennent. 1. # Definition d'un serveur reseau gerant un systeme de CHAT simplifie . 2. # Utilise les threads pour gerer les connexions clientes en parallele . 3. 4. HOST = '192.168.0.235' 5. PORT = 40000 6. 7. import socket, sys, threading 8. 9. class ThreadClient (threading. Thread) : 10. 11 'derivation d'un objet thread pour gerer la connexion avec un client' ' ' 11. def init (self, conn): 12 . threading. Thread. init (self) 13. self . connexion = conn 14. 15. def run (self) : 16. # Dialogue avec le client : 17. nom = self .getName () # Chaque thread possede un nom 18. while 1: 19. msgClient = self . connexion . recv (1024 ) 20. if msgClient . upper () == "FIN" or msgClient =="": 21. break 22. message = "%s> %s" % (nom, msgClient) 23 . print message 24 . # Faire suivre le message a tous les autres clients : 25. for cle in conn_client : 26. if cle != nom: # ne pas le renvoyer a 1 ' emetteur 27. conn_client [cle] . send (message) 28. 77 Que les puristes veuillent bien me pardonner : j'admets volontiers que cette astuce pour forcer l'arret d'un thread n'est pas vraiment recommandable. Je me suis autorise ce raccourci afin de ne pas trop alourdir ce texte, qui se veut seulement une initiation. Le lecteur exigeant pourra approfondir cette question en consultant l'un ou l'autre des ouvrages de reference mentionnes dans la bibliographie (voir page 8) Gerard Swinnen : Apprendre a programmer avec Python 285. 29. # Fermeture de la connexion : 30. self . connexion . close ( ) # couper la connexion cote serveur 31 . del conn_client [nom] # supprimer son entree dans le dictionnalre 32. print "Client %s deconnecte." % nom 33 . # Le thread se termine ici 34. 35. # Initialisation du serveur - Mise en place du socket : 36. mySocket = socket . socket (socket . AF_INET, socket . SOCK_STREAM) 37. try: 38. my Socket. bind ( (HOST, PORT)) 39. except socket. error: 40. print "La liaison du socket a l'adresse choisie a echoue." 41. sys.exit() 42. print "Serveur pret, en attente de requetes ..." 43. mySocket . listen (5) 44. 45. # Attente et prise en charge des connexions demandees par les clients : 46. conn_client = {} # dictionnalre des connexions clients 47 . while 1 : 48. connexion, adresse = mySocket . accept ( ) 49. # Creer un nouvel objet thread pour gerer la connexion : 50. th = ThreadClient (connexion) 51. th. start () 52 . # Memoriser la connexion dans le dictionnalre : 53. it = th.getName() # identifiant du thread 54. conn_client [it] = connexion 55. print "Client %s connecte, adresse IP %s, port %s." %\ 56. (it, adresse [0], adresse [1]) 57 . # Dialogue avec le client : 58. connexion . send ( "Vous etes connecte. Envoyez vos messages.") Commentaires : • Lignes 35 a 43 : L'initialisation de ce serveur est identique a celle du serveur rudimentaire decrit au debut du present chapitre. • Ligne 46 : Les references des differentes connexions doivent etre memorisees. Nous pourrions les placer dans une liste, mais il est plus judicieux de les placer dans un dictionnaire, pour deux raisons : La premiere est que nous devrons pouvoir aj outer ou enlever ces references dans n'importe quel ordre, puisque les clients se connecteront et se deconnecteront a leur guise. La seconde est que nous pouvons disposer aisem*nt d'un identifiant unique pour chaque connexion, lequel pourra servir de cle d'acces dans un dictionnaire. Cet identifiant nous sera en effet fourni automatiquement par La classe ThreadO- • Lignes 47 a 5 1 : Le programme commence ici une boucle de repetition perpetuelle, qui va constamment attendre l'arrivee de nouvelles connexions. Pour chacune de celles-ci, un nouvel objet ThreadClient() est cree, lequel pourra s'occuper d'elle independamment de toutes les autres. • Lignes 52 a 54 : Obtention d'un identifiant unique a l'aide de la methode getName(). Nous pouvons profiter ici du fait que Python attribue automatiquement un nom unique a chaque nouveau thread : ce nom convient bien comme identifiant (ou cle) pour retrouver la connexion correspondante dans notre dictionnaire. Vous pourrez constater qu'il s'agit d'une chaine de caracteres, de la forme : « Thread-N » (N etant le numero d'ordre du thread). • Lignes 15 a 17 : Gardez bien a l'esprit qu'il se creera autant d'objets ThreadClient() que de connexions, et que tous ces objets fonctionneront en parallele. La methode getName() peut alors etre utilisee au sein de l'un quelconque de ces objets pour retrouver son identite particuliere. Nous utiliserons cette information pour distinguer la connexion courante de toutes les autres (voir ligne 26). • Lignes 18 a 23 : L'utilite du thread est de receptionner tous les messages provenant d'un client particulier. II faut done pour cela une boucle de repetition perpetuelle, qui ne s'interrompra qu'a 286. Gerard Swinnen : Apprendre a programmer avec Python la reception du message specifique : « fin », ou encore a la reception d'un message vide (cas ou la connexion est coupee par le partenaire). • Lignes 24 a 27 : Chaque message recu d'un client doit etre re-expedie a tous les autres. Nous utilisons ici une boucle for pour parcourir l'ensemble des cles du dictionnaire des connexions, lesquelles nous permettent ensuite de retrouver les connexions elles-memes. Un simple test (a la ligne 26) nous evite de re-expedier le message au client dont il provient. • Ligne 3 1 : Lorsque nous fermons un socket de connexion, il est preferable de supprimer sa reference dans le dictionnaire, puisque cette reference ne peut plus servir. Et nous pouvons faire cela sans precaution particuliere, car les elements d'un dictionnaire ne sont pas ordonnes (nous pouvons en ajouter ou en enlever dans n'importe quel ordre). 18.7 Jeu des bombardes, version reseau Iclient Thread- 3 connects, adresse IP 192. 168. 0. 23S, port 3493S. \X Client Thread-4 connecte, adresse IP 192. 168 0. 23S, port 34936. Client Thread-S connecte, adresse IP 192. 168 0, 23S, port 34937. Client Thread-6 connecte, adresse IP 192. 168. 0. 23S, port 34938. I— 1 Client Thread-7 connecte, adresse IP 192. 168. 0. 23S, port 34939. / EThread-3 ■ Thread-S ■ Thread-7 ■ Thread-S | Thread-4 points points points M points 45 M P oints 45 M -5 1 Z5H 1 U 4 U -3 _ Feu I | i , -ii ; | r.-.j ■ [ Feu ! Feu I | Thread-2 points -1 Au chapitre 15, nous avons commente le developpement d'un petit jeu de combat dans lequel des joueurs s'affrontaient a l'aide de bombardes. L'interet de ce jeu reste toutefois fort limite, tant qu'il se pratique sur un seul et meme ordinateur. Nous allons done le perfectionner, en y integrant les techniques que nous venons d'apprendre. Comme le systeme de « chat » decrit dans les pages precedentes, l'application complete se composera desormais de deux programmes distincts : un logiciel serveur qui ne sera mis en fonctionnement que sur une seule machine, et un logiciel client qui pourra etre lance sur toute une serie d' autres. Du fait du caractere portable de Python, il vous sera meme possible d'organiser des combats de bombardes entre ordinateurs geres par des systemes d'exploitation differents (MacOS <> Linux <> Windows !). Gerard Swinnen : Apprendre a programmer avec Python 287. 18.7.1 Programme serveur : vue d'ensemble Les programmes serveur et client exploitent la meme base logicielle, elle-meme largement recuperee de ce qui avait deja ete mis au point tout au long du chapitre 15. Nous admettrons done pour la suite de cet expose que les deux versions precedentes du jeu ont ete sauvegardees dans les fichiers-modules canon03.py et canon04.py, installes dans le repertoire courant. Nous pouvons en effet reutiliser une bonne partie du code qu'ils contiennent, en nous servant judicieusem*nt de I'importation et de I'heritage de classes. Du module canon04, nous allons reutiliser la classe Canon() telle quelle, aussi bien pour le logiciel serveur que pour le logiciel client. De ce meme module, nous importerons egalement la classe AppBombardes(), dont nous ferons deriver la classe maitresse de notre application serveur : AppServeur(). Vous constaterez plus loin que celle-ci produira elle-meme la sous-classe AppClient(), toujours par heritage. Du module canon03, nous recupererons la classe Pupitre() dont nous tirerons une version plus adaptee au « controle a distance ». Enfin, deux nouvelles classes viendront s'ajouter aux precedentes, chacune specialisee dans la creation d'un objet thread : la classe ThreadClients(), dont une instance surveillera en permanence le socket destine a receptionner les demandes de connexion de nouveaux clients, et la classe ThreadConnexion(), qui servira a creer autant d'objets sockets que necessaire pour assurer le dialogue avec chacun des clients deja connectes. Ces nouvelles classes seront inspirees de celles que nous avions developpees pour notre serveur de « chat » dans les pages precedentes. La principale difference par rapport a celui-ci est que nous devrons activer un thread specifique pour le code qui gere l'attente et la prise en charge des connexions clientes, afin que l'application principale puisse faire autre chose pendant ce temps. A partir de la, notre plus gros travail consistera a developper un protocole de communication pour le dialogue entre le serveur et ses clients. De quoi est-il question ? Tout simplement de definir la teneur des messages que vont s'echanger les machines connectees. Rassurez-vous : la mise au point de ce « langage » peut etre progressive. On commence par etablir un dialogue de base, puis on y ajoute petit a petit un « vocabulaire » plus etendu. L'essentiel de ce travail peut etre accompli en s 'aidant du logiciel client developpe precedemment pour le systeme de « chat ». On se sert de celui-ci pour envoyer des « ordres » au serveur en cours de developpement, et on corrige celui-ci jusqu'a ce qu'il « obeisse » : en clair, les procedures que Ton met en place progressivement sur le serveur sont testees au fur et a mesure, en reponse aux messages correspondants emis « a la main » a partir du client. 18.7.2 Protocole de communication II va de soi que le protocole decrit ci-apres est tout a fait arbitraire. II serait parfaitement possible de choisir d'autres conventions completement differentes. Vous pouvez bien evidemment critiquer les choix effectues, et vous souhaiterez peut-etre meme les remplacer par d'autres, plus efficients ou plus simples. Vous savez deja que les messages echanges sont de simples chaines de caracteres. Prevoyant que certains de ces messages devront transmettre plusieurs informations a la fois, nous avons decide que chacun d'eux pourrait comporter plusieurs champs, que nous separerons a l'aide de virgules. Lors de la reception de l'un quelconque de ces messages, nous pourrons alors aisem*nt recuperer tous ses composants dans une liste, a l'aide de la methode integree split(). Voici un exemple de dialogue type, tel qu'il peut etre suivi du cote d'un client. Les messages 288. Gerard Swinnen : Apprendre a programmer avec Python entre asterisques sont ceux qui sont recus du serveur ; les autres sont ceux qui sont emis par le client lui-meme : 1 . *serveur OK* 2 . client OK 3. *canons, Thread-3; 104; 228; 1; dark red, Thread-2 ; 454 ; 166; -1 ; dark blue,* 4 . OK 5. *nouveau_canon, Thread-4, 481, 245, -1, dark green, le_votre* 6. orienter,25, 7. feu 8 . *mouvement_de, Thread-4, 549, 280, * 9. feu 10 . *mouvement_de , Thread-4 , 504 , 278 , * 11 . *scores , Thread-4 ; 1 , Thread-3 ; -1 , Thread-2 ; 0 , * 12. *angle, Thread-2, 23, * 13. * angle, Thread-2, 20, * 14. *tir_de, Thread-2, * 1 5 . *mouvement_de , Thread-2 ,407,191,* 16 . *depart_de, Thread-2* 17. *nouveau_canon, Thread-5, 502, 27 6, -1, dark green* Lorsqu'un nouveau client demarre, il envoie une requete de connexion au serveur, lequel lui expedie en retour le message : « serveur OK ». A la reception de ce dernier, le client repond alors en envoyant lui-meme : « client OK ». Ce premier echange de politesses n'est pas absolument indispensable, mais il permet de verifier que la communication passe bien dans les deux sens. Etant done averti que le client est pret a travailler, le serveur lui expedie alors une description des canons deja presents dans le jeu (eventuellement aucun) : identifiant, emplacement sur le canevas, orientation et couleur (ligne 3). En reponse a l'accuse de reception du client (ligne 4), le serveur installe un nouveau canon dans l'espace de jeu, puis il signale les caracteristiques de cette installation non seulement au client qui l'a provoquee, mais egalement a tous les autres clients connectes. Le message expedie au nouveau client comporte cependant une difference (car e'est lui le proprietaire de ce nouveau canon) : en plus des caracteristiques du canon, qui sont fournies a tout le monde, il comporte un champ supplementaire contenant simplement « le votre » (comparez par exemple la ligne 5 avec la ligne 17, laquelle signale la connexion d'un autre joueur). Cette indication supplementaire permet au client proprietaire du canon de distinguer parmi plusieurs messages similaires eventuels, celui qui contient l'identifiant unique que lui a attribue le serveur. Les messages des lignes 6 et 7 sont des commandes envoyees par le client (reglage de la hausse et commande de tir). Dans la version precedente du jeu, nous avions deja convenu que les canons se deplaceraient quelque peu (et au hasard) apres chaque tir. Le serveur effectue done cette operation, et s'empresse ensuite d'en faire connaitre le resultat a tous les clients connectes. Le message recu du serveur a la ligne 8 est done l'indication d'un tel deplacement (les coordonnees fournies sont les coordonnees resultantes pour le canon concerne). La ligne 1 1 reproduit le type de message expedie par le serveur lorsqu'une cible a ete touchee. Les nouveaux scores de tous les joueurs sont ainsi communiques a tous les clients. Les messages serveur des lignes 12, 13 et 14 indiquent les actions entreprises par un autre joueur (reglage de hausse suivi d'un tir). Cette fois encore, le canon concerne est deplace au hasard apres qu'il ait tire (ligne 15). Lignes 16 et 17 : lorsque l'un des clients coupe sa connexion, le serveur en avertit tous les autres, afin que le canon correspondant disparaisse de l'espace de jeu sur tous les postes. A l'inverse, de nouveaux clients peuvent se connecter a tout moment pour participer au jeu. Remarques complementaires : Le premier champ de chaque message indique sa teneur. Les messages envoyes par le client sont tres simples : ils correspondent aux differentes actions entreprises par le joueur (modifications de Gerard Swinnen : Apprendre a programmer avec Python 289. Tangle de tir et commandes de feu). Ceux qui sont envoyes par le serveur sont un peu plus complexes. La plupart d'entre eux sont expedies a tous les clients connectes, afin de les tenir informes du deroulement du jeu. En consequence, ces messages doivent mentionner l'identifiant du joueur qui a commande une action ou qui est concerne par un changement quelconque. Nous avons vu plus haut que ces identifiants sont des noms generes automatiquement par le gestionnaire de threads du serveur, chaque fois qu'un nouveau client se connecte. Certains messages concernant l'ensemble du jeu contiennent plusieurs informations par champ. Dans ce cas, les differents « sous-champs » sont separes par des points-virgules (lignes 3 et 1 1). 18.7.3 Programme serveur : premiere partie Vous trouverez dans les pages qui suivent le script complet du programme serveur. Nous vous le presentons en trois morceaux successifs afin de rapprocher les commentaires du code correspondant, mais la numerotation de ses lignes est continue. Bien qu'il soit deja relativement long et complexe, vous estimerez probablement qu'il merite d'etre encore perfectionne, notamment au niveau de la presentation generale. Nous vous laisserons le soin d'y ajouter vous-meme tous les complements qui vous sembleront utiles (par exemple, une proposition de choisir les coordonnees de la machine hote au demarrage, une barre de menus, etc.) : 1 . ####################################################### 2 . # Jeu des bombardes - partie serveur # 3. # (C) Gerard Swinnen, Liege (Belgique)- Juillet 2004 # 4. # Licence : GPL # 5. # Avant d'executer ce script, verifiez que l'adresse # 6 . # IP ci-dessous soit bien celle de la machine hote . # 7. # Vous pouvez choisir un numero de port different, ou # 8. # changer les dimensions de 1 ' espace de jeu. # 9. # Dans tous les cas, verifiez que les memes choix ont # 10. # ete effectues pour chacun des scripts clients. # 11 . ####################################################### 12 . 13. host, port = '192.168.0.235', 35000 14. largeur, hauteur = 700, 400 # dimensions de l'espace de jeu 15 . 16. from Tkinter import * 17. import socket, sys, threading, time 18. import canon03 19. from canon04 import Canon, AppBombardes 20. 21. class Pupitre (canon03 .Pupitre) : 22. """Pupitre de pointage ameliore""" 23. def init (self, boss, canon): 24. canon03. Pupitre. init (self, boss, canon) 25. 26. def tirer (self) : 27. "declencher le tir du canon associe" 28 . self . appli . tir_canon (self . canon . id) 29. 30. def orienter (self , angle): 31. "ajuster la hausse du canon associe" 32. self . appli . orienter_canon (self . canon . id, angle) 33 . 34. def valeur_score (self , sc =None) : 35. "imposer un nouveau score , ou lire le score existant" 36. if sc == None: 37. return self. score 38. else: 39. self. score =sc 40. self .points . config (text = ' %s ' % self. score) 41. 42. def inactiver (self ) : 43. "desactiver le bouton de tir et le systeme de reglage d' angle" 290. Gerard Swinnen : Apprendre a programmer avec Python 44. self .bTir.config (state =DISABLED) 45. self . regl . config (state =DISABLED) 46. 47. def activer (self ) : 48. "activer le bouton de tir et le systeme de reglage d' angle" 49. self .bTir.config (state =NORMAL) 50. self . regl . config (state =NORMAL ) 51. 52. def reglage (self , angle): 53. "changer la position du curseur de reglage" 54. self .regl. config (state =NORMAL) 55. self . regl . set (angle) 56. self . regl . config (state =DISABLED) 57. La classe PupitreO est constitute par derivation de la classe de meme nom importee du modune canon03. Elle herite done toutes les caracteristiques de celle-ci, mais nous devons surcharged ses methodes tirer() et orienter() : Dans la version monoposte du logiciel, en effet, chacun des pupitres pouvait commander directement l'objet canon correspondant. Dans cette version reseau, par contre, ce sont les clients qui controlent a distance le fonctionnement des canons. Par consequent, les pupitres qui apparaissent dans la fenetre du serveur ne peuvent etre que de simples repetiteurs des manoeuvres effectuees par les joueurs sur chaque client. Le bouton de tir et le curseur de reglage de la hausse sont done desactives, mais les indications fournies obeissent aux injunctions qui leur sont adressees par l'application principale. Cette nouvelle classe Pupitre() sera egalement utilisee telle quelle dans chaque exemplaire du programme client. Dans la fenetre de celui-ci comme dans celle du serveur, tous les pupitres seront affiches comme des repetiteurs, mais l'un d'entre eux cependant sera completement fonctionnel : celui qui correspond au canon du joueur. Toutes ces raisons expliquent egalement l'apparition des nouvelles methodes : activer(), desactiver(), reglage() et valeur_score(), qui seront elles aussi invoquees par l'application principale, en reponse aux messages-instructions echanges entre le serveur et ses clients. La classe ThreadConnexion() ci-dessous sert a instancier la serie d'objets threads qui s'occuperont en parallele de toutes les connexions lancees par les clients. Sa methode run() contient la fonctionnalite centrale du serveur, a savoir la boucle d'instructions qui gere la reception des messages provenant d'un client particulier, lesquels entrainent chacun toute une cascade de reactions. Vous y trouverez la mise en oeuvre concrete du protocole de communication decrit dans les pages precedentes (certains messages etant cependant generes par les methodes depl_aleat_canon() et goal() de la classe AppServeur() decrite plus loin). 58. class ThreadConnexion (threading . Thread) : 59. """objet thread gestionnaire d'une connexion client""" 60. def init (self, boss, conn): 61 . threading. Thread. init (self) 62. self . connexion = conn # ref. du socket de connexion 63. self.app = boss # ref. de la fenetre application 64 . 65. def run (self): 66. "actions entreprises en reponse aux messages recus du client" 67. nom = self . getName ( ) # id. du client = nom du thread 68. while 1: 69. msgClient = self . connexion . recv (1024) 70. print "**%s** de %s" % (msgClient, nom) 71. deb = msgClient. split (',') [0] 78 Rappel : dans une classe derivee, vous pouvez definir une nouvelle methode avec le meme nom qu'une methode de la classe parente, afm de modifier sa fonctionnalite dans la classe derivee. Cela s'appelle surcharger cette methode (voir aussi page 167). Gerard Swinnen : Apprendre a programmer avec Python 291. 72. if deb == "fin" or deb =="": 73. self . app . enlever_canon (nom) 74 . # signaler le depart de ce canon aux autres clients : 75. self . app . verrou . acquire ( ) 76. for cli in self . app . conn_client : 77. if cli != nom: 78. message = "depart_de, %s" % nom 79. self .app. conn_client [cli] . send (message) 80. self .app. verrou. release () 81. # fermer le present thread : 82 . break 83. elif deb =="client OK": 84 . # signaler au nouveau client les canons de ja enregistres : 85. msg ="canons," 86. for g in self . app . guns : 87. gun = self . app . guns [g] 88. msg =msg +"%s; %s; %s; %s; %s, " % \ 89. (gun. id, gun.xl, gun.yl, gun. sens, gun.coul) 90. self . app . verrou . acquire ( ) 91. self . connexion . send (msg) 92. # attendre un accuse de reception ('OK') : 93. self . connexion . recv (100) 94. self .app. verrou. release () 95. # ajouter un canon dans 1 ' espace de jeu serveur. 96. # la methode invoquee renvoie les caract . du canon cree : 97. x, y, sens, coul = self . app . a jouter_canon (nom) 98 . # signaler les caract . de ce nouveau canon a tous les 99. # clients deja connectes : 100. self . app . verrou . acquire ( ) 101. for cli in self . app . conn_client : 102. msg ="nouveau_canon, %s, %s, %s, %s, %s" % \ 103. (nom, x, y, sens, coul) 104. # pour le nouveau client, ajouter un champ indiquant 105. # que le message concerne son propre canon : 106. if cli == nom: 107. msg =msg +",le_votre" 108 . self . app . conn_client [cli] . send (msg) 109. self .app. verrou. release () 110. elif deb =='feu': 111. self . app . tir_canon (nom) 112 . # Signaler ce tir a tous les autres clients : 113. self . app . verrou . acquire ( ) 114. for cli in self . app . conn_client : 115. if cli != nom: 116. message = "tir_de,%s," % nom 117. self .app. conn_client [cli] . send (message) 118. self .app. verrou. release () 119. elif deb =="orienter" : 120. t =msgClient . split (',' ) 121. # on peut avoir recu plusieurs angles, utiliser le dernier: 122. self . app . orienter_canon (nom, t[-2]) 123. # Signaler ce changement a tous les autres clients : 124. self . app . verrou . acquire ( ) 125. for cli in self . app . conn_client : 126. if cli != nom: 127. # virgule terminale, car messages parfois groupes : 128. message = "angle, %s, %s, " % (nom, t[-2]) 129. self .app. conn_client [cli] . send (message) 130. self .app. verrou. release () 131. 132 . # Fermeture de la connexion : 133. self . connexion . close () # couper la connexion 134. del self . app . conn_client [nom] # suppr. sa ref. dans le dictionn. 135. self . app. afficher ( "Client %s deconnecte . \n" % nom) 136. # Le thread se termine ici 137 . 292. Gerard Swinnen : Apprendre a programmer avec Python 18.7.4 Synchronisation de threads concurrents a I'aide de « verrous » (thread locks) Au cours de votre examen du code ci-dessus, vous aurez certainement remarque la structure particuliere des blocs destructions par lesquelles le serveur expedie un meme message a tous ses clients. Considerez par exemple les lignes 74 a 80 : La ligne 75 active la methode acquire() d'un objet « verrou » qui a ete cree par le constructeur de l'application principale (voir plus loin). Cet objet est une instance de la classe Lock(), laquelle fait partie du module threading que nous avons importe en debut de script. Les lignes suivantes (76 a 79) provoquent l'envoi d'un message a tous les clients connectes (sauf un). Ensuite, l'objet « verrou » est a nouveau sollicite, cette fois pour sa methode release(). A quoi cet objet « verrou » peut-il done bien servir ? Puisqu'il est produit par une classe du module threading, vous pouvez deviner que son utilite concerne les threads. En fait, de tels objets « verrous » servent a synchroniser les threads concurrents. De quoi s'agit-il ? Vous savez que le serveur demarre un thread different pour chacun des clients qui se connecte. Ensuite, tous ces threads fonctionnent en parallele. II existe done un risque que de temps a autre, deux ou plusieurs de ces threads essaient d'utiliser une ressource commune en meme temps. Dans les lignes de code que nous venons de discuter, par exemple, nous avons affaire a un thread qui souhaite exploiter quasiment toutes les connexions presentes pour poster un message. II est done parfaitement possible que pendant ce temps, un autre thread tente d'exploiter lui aussi l'une ou l'autre de ces connexions, ce qui risque de provoquer un dysfonctionnement (en l'occurrence, la superposition chaotique de plusieurs messages). Un tel probleme de concurrence entre threads peut etre resolu par l'utilisation d'un objet-verrou {thread lock). Un tel objet n'est cree qu'en un seul exemplaire, dans un espace de noms accessible a tous les threads concurrents. II se caracterise essentiellement par le fait qu'il se trouve toujours dans l'un ou l'autre de deux etats : soit verrouille, soit deverrouille. Son etat initial est l'etat deverrouille. Utilisation : Lorsqu'un thread quelconque s'apprete a acceder a une ressource commune, il active d'abord la methode acquire() du verrou. Si celui-ci etait dans l'etat deverrouille, il se verrouille, et le thread demandeur peut alors utiliser la ressource commune, en toute tranquillite. Lorsqu'il aura fini d'utiliser la ressource, il s'empressera cependant d'activer la methode release() du verrou, ce qui le fera repasser dans l'etat deverrouille. En effet : Si un autre thread concurrent active lui aussi la methode acquire() du verrou, alors que celui-ci est dans l'etat verrouille, la methode « ne rend pas la main », provoquant le blocage de ce thread, lequel suspend done son activite jusqu'a ce que le verrou repasse dans l'etat deverrouille. Ceci l'empeche done d'acceder a la ressource commune durant tout le temps ou un autre thread s'en serf Lorsque le verrou est deverrouille, l'un des threads en attente (il peut en effet y en avoir plusieurs) reprend alors son activite, et ainsi de suite. L'objet verrou memorise les references des threads bloques, de maniere a n'en debloquer qu'un seul a la fois lorsque sa methode release() est invoquee. II faut done toujours veiller a ce que chaque thread qui active la methode acquire() du verrou avant d'acceder a une ressource, active egalement sa methode release() peu apres. Pour autant que tous les threads concurrents respectent la meme procedure, cette technique simple empeche done qu'une ressource commune soit exploitee en meme temps par plusieurs d'entre eux. On dira dans ce cas que les threads ont ete synchronises. Gerard Swinnen : Apprendre a programmer avec Python 293. 18.7.5 Programme serveur : suite et fin Les deux classes ci-dessous completent le script serveur. Le code implements dans la classe ThreadClients() est assez similaire a celui que nous avions developpe precedemment pour le corps d'application du logiciel de « Chat ». Dans le cas present, toutefois, nous le placons dans une classe derivee de Thread(), parce que devons faire fonctionner ce code dans un thread independant de celui de l'application principale. Celui-ci est en effet deja completement accapare par la boucle mainloop() de l'interface graphique 79 . La classe AppServeur() derive de la classe AppBombardes() du module canon04. Nous lui avons ajoute un ensemble de methodes complementaires destinees a executer toutes les operations qui resulteront du dialogue entame avec les clients. Nous avons deja signal e plus haut que les clients instancieront chacun une version derivee de cette classe (afin de profiter des memes definitions de base pour la fenetre, le canevas, etc.). 138. class ThreadClients (threading. Thread) : 139. """objet thread gerant la connexion de nouveaux clients""" 140. def init (self, boss, connex) : 141 . threading. Thread. init (self) 142. self. boss = boss # ref. de la fenetre application 143. self. connex = connex # ref. du socket initial 144 . 145. def run (self): 146. "attente et prise en charge de nouvelles connexions clientes" 147. txt ="Serveur pret, en attente de requetes ...\n" 148. self .boss . afficher (txt) 149. self . connex . listen (5) 150. # Gestion des connexions demandees par les clients : 151. while 1: 152. nouv_conn, adresse = self . connex . accept ( ) 153. # Creer un nouvel objet thread pour gerer la connexion : 154. th = ThreadConnexion (self . boss , nouv_conn) 155. th. start () 156. it = th.getName() # identifiant unique du thread 157 . # Memoriser la connexion dans le dictionnaire : 158. self . boss . enregistrer_connexion (nouv_conn, it) 159. # Afficher : 160. txt = "Client %s connecte, adresse IP %s, port %s.\n" %\ 161. (it, adresse [0], adresse [1]) 162. self .boss . afficher (txt) 163. # Commencer le dialogue avec le client : 164. nouv_conn . send ( "serveur OK") 165. 166. class AppServeur (AppBombardes) : 167. """fenetre principale de l'application (serveur ou client)""" 168. def init (self, host, port, larg_c, haut_c) : 169. self. host, self. port = host, port 170. AppBombardes. init (self, larg_c, haut_c) 171. self. active =1 # temoin d'activite 172 . # veiller a quitter proprement si 1 ' on ref erme la fenetre : 173 . self . bind ( ' ' , self . fermer_threads) 174 . 175. def specif icites (self ) : 176. "preparer les objets specifiques de la partie serveur" 177. self .master, title ( '«< Serveur pour le jeu des bombardes »> ' ) 178. 179. # widget Text, associe a une barre de defilement : 180. st =Frame(self) 181. self. avis =Text(st, width =65, height =5) 182. self . avis .pack (side =LEFT) 183. scroll =Scrollbar (st, command =self . avis . yview) 184. self . avis . configure (yscrollcommand =scroll.set) 79 Nous detaillerons cette question quelques pages plus loin, car elle ouvre quelques perspectives interessantes. Voir : Optimiser les animations a l'aide des threads, page 300. 294. Gerard Swinnen : Apprendre a programmer avec Python 185. scroll. pack (side =RIGHT, fill =Y) 186. st. pack () 187. 188. # partie serveur reseau : 189. self . conn_client = {} # dictionn. des connexions clients 190. self.verrou =threading . Lock ( ) # verrou pour synchroniser threads 191. # Initialisation du serveur - Mise en place du socket : 192. connexion = socket . socket (socket .AF_INET, socket . SOCK_STREAM) 193. try: 194. connexion. bind ( (self .host, self .port)) 195. except socket . error : 196. txt ="La liaison du socket a l'hote %s, port %s a echoue.\n" %\ 197. (self. host, self. port) 198. self . avis . insert (END, txt) 199. self.accueil =None 200. else: 201. # demarrage du thread guettant la connexion des clients : 202. self.accueil = ThreadClients (self , connexion) 203. self . accueil . start () 204. 205. def depl_aleat_canon (self , id): 206. "deplacer aleatoirement le canon " 207. x, y = AppBombardes . depl_aleat_canon (self , id) 208. # signaler ces nouvelles coord, a tous les clients : 209. self .verrou. acquire () 210. for cli in self . conn_client : 211. message = "mouvement_de, %s, %s, %s, " % (id, x, y) 212. self . conn_client [cli] . send (message) 213. self .verrou. release () 214. 215. def goal(self, i, j) : 216. "le canon signale qu'il a atteint 1 ' adversaire " 217. AppBombardes . goal (self , i, j) 218. # Signaler les nouveaux scores a tous les clients : 219. self .verrou. acquire () 220. for cli in self . conn_client : 221. msg ='scores,' 222. for id in self.pupi: 223. sc = self .pupi [id] . valeur_score () 224. msg = msg +"%s;%s," % (id, sc) 225. self . conn_client [cli] .send (msg) 226. time . sleep (. 5) # pour mieux separer les messages 227. self .verrou. release () 228. 229. def a jouter_canon (self , id): 230. "instancier un canon et un pupitre de nom dans 2 dictionn." 231. # on alternera ceux des 2 camps : 232. n = len (self . guns) 233. if n %2 ==0: 234. sens = -1 235. else: 236. sens = 1 237. x, y = self . coord_aleat (sens) 238. coul =('dark blue', 'dark red', 'dark green', 'purple', 239. 'dark cyan', 'red', 'cyan', 'orange', 'blue', 'violet') [n] 240. self . guns [id] = Canon (self . jeu, id, x, y, sens, coul) 241. self .pupi [id] = Pupitre (self , self . guns [id] ) 242. self .pupi [id] . inactiver () 243. return (x, y, sens, coul) 244. 245. def enlever_canon (self , id): 246. "retirer le canon et le pupitre dont 1 ' identif iant est " 247. if self. active ==0: # la fenetre a ete refermee 248. return 249. self .guns [id] .ef facer () 250. del self .guns [id] 251. self .pupi [id] . destroy () 252. del self .pupi [id] 253. 254. def orienter_canon (self , id, angle): Gerard Swinnen : Apprendre a programmer avec Python 295. 255 . "regler la hausse du canon a la valeur " 256 . self . guns [id] . orienter (angle) 257 . self .pupi [id] . reglage (angle) o c o 258 . 259 . def tir_canon (self , id): 260 . "declencher le tir du canon " 261 . self .guns [id] .feu() 262 . 263 . def enregistrer_connexion (self , conn, it): "Memoriser la connexion dans un dictionnaire" 264 . 265 . self . conn_client [it] = conn 266 . 267 . def af f icher (self , txt) : 268 . "afficher un message dans la zone de texte" 269 . self . avis . insert (END, txt) 270 . 271 . def fermer_threads (self , evt) : 272 . "couper les connexions existantes et fermer les threads" 273 . # couper les connexions etablies avec tous les clients : 274 . for id in self . conn_client : 275 . self . conn_client [id] . send ( ' fin ' ) 276 . # forcer la terminaison du thread serveur qui attend les requetes Oil £.11. if self.accueil != None: 278. self . accueil . _Thread stop ( ) 279. self. active =0 # empecher acces ulterieurs a Tk 280. 281. if name == ' main ' : 282 . AppServeur (host, port, largeur, hauteur) .mainloop () Commentaires : • Ligne 173 : II vous arrivera de temps a autre de vouloir « intercepter » l'ordre de fermeture de l'application que l'utilisateur declenche en quittant votre programme, par exemple parce que vous voulez forcer la sauvegarde de donnees importantes dans un fichier, ou fermer aussi d'autres fenetres, etc. II suffit pour ce faire de detecter l'evenement , comme nous le faisons ici pour forcer la terminaison de tous les threads actifs. • Lignes 179 a 186 : Au passage, voici comment vous pouvez associer une barre de defilement {widget Scrollbar) a un widget Text (vous pouvez faire de meme avec un widget Canvas), sans faire appel a la bibliotheque Pmw 80 . • Ligne 190 : Instanciation de l'obet « verrou » permettant de synchroniser les threads. • Lignes 202, 203 : Instanciation de l'objet thread qui attendra en permanence les demandes de connexion des clients potentiels. • Lignes 205 a213,215a 227 : Ces methodes surchargent les methodes de meme nom heritees de leur classe parente. Elles commencent par invoquer celles-ci pour effectuer le meme travail (lignes 207, 217), puis ajoutent leur fonctionnalite propre, laquelle consiste a signaler a tout le monde ce qui vient de se passer. • Lignes 229 a 243 : Cette methode instancie un nouveau poste de tir, chaque fois qu'un nouveau client se connecte. Les canons sont places alternativement dans le camp de droite et dans celui de gauche, procedure qui pourrait bien evidemment etre amelioree. La liste des couleurs prevues limite le nombre de clients a 10, ce qui devrait suffire. 80 Voir : Python Mega Widgets, page 207. Gerard Swinnen : Apprendre a programmer avec Python 296. 18.7.6 Programme client Le script correspondant au logiciel client est reproduit ci-apres. Comme celui qui correspond au serveur, il est relativement court, parce qu'il utilise lui aussi l'importation de modules et l'heritage de classes. Le script serveur doit avoir ete sauvegarde dans un fichier-module nomme canon_serveur.py. Ce fichier doit etre place dans le repertoire courant, de meme que les fichiers- modules canon03.py et canon04.py qu'il utilise lui-meme. De ces modules ainsi importes, le present script utilise les classes Canon() et Pupitre() a l'identique, ainsi qu'une forme derivee de la classe AppServeur(). Dans cette derniere, de nombreuses methodes ont ete surchargees, afin d'adapter leur fonctionnalite. Considerez par exemple les methodes goal() et depl_aleat_canon(), dont la variante surchargee ne fait plus rien du tout (instruction pass), parce que le calcul des scores et le repositionnement des canons apres chaque tir ne peuvent etre effectues que sur le serveur seulement. C'est dans la methode run() de la classe ThreadSocket() (lignes 86 a 126) que se trouve le code traitant les messages echanges avec le serveur. Nous y avons d'ailleurs laisse une instruction print (a la ligne 88) afin que les messages recus du serveur apparaissent sur la sortie standard. Si vous realisez vous-meme une forme plus definitive de ce jeu, vous pourrez bien evidemment supprimer cette instruction. 1 . ####################################################### 2 . # Jeu des bombardes - partie cliente # 3. # (C) Gerard Swinnen, Liege (Belgique) - Juillet 2004 # 4. # Licence : GPL # 5. # Avant d'executer ce script, verifiez que l'adresse, # 6. # le numero de port et les dimensions de l'espace de # 7 . # jeu indiquees ci-dessous correspondent exactement # 8. # a ce qui a ete defini pour le serveur. # 9 . ####################################################### 10. 11. from Tkinter import * 12. import socket, sys, threading, time 13. from canon_serveur import Canon, Pupitre, AppServeur 14. 15. host, port = '192.168.0.235', 35000 16. largeur, hauteur = 700, 400 # dimensions de l'espace de jeu 17. 18. class AppClient (AppServeur) : 19. def init (self, host, port, larg_c, haut_c) : 20. AppServeur. init (self, host, port, larg_c, haut_c) 21. 22. def specif icites (self ) : 23. "preparer les objets specif iques de la partie client" 24. self .master, title (' <<< Jeu des bombardes >»') 25. self.connex =ThreadSocket (self , self. host, self .port) 26. self .connex. start () 27. self. id =None 28. 29. def a jouter_canon (self , id, x, y, sens, coul) : 30. "instancier 1 canon et 1 pupitre de nom dans 2 dictionnaires " 31. self . guns [id] = Canon (self . jeu, id, int (x) , int (y) , int (sens) , coul) 32. self .pupi [id] = Pupitre (self , self . guns [id] ) 33. self .pupi [id] . inactiver () 34. 35. def activer_pupitre_personnel (self , id): 36. self. id =id # identifiant recu du serveur 37. self .pupi [id] .activer () 38 . 39. def tir_canon (self , id): 40. r = self . guns [id] . feu () # renvoie False si enraye 41. if r and id == self. id: 42. self . connex . signaler_tir ( ) Gerard Swinnen : Apprendre a programmer avec Python 297. 43. 44. def imposer_score (self , id, sc) : 45. self .pupi [id] . valeur_score (int (sc) ) 46. 47. def deplacer_canon (self , id, x, y) : 48. "note: les valeurs de x et y sont recues en tant que chaines" 49. self .guns [id] .deplacer (int (x) , int (y) ) 50. 51. def orienter_canon (self , id, angle): 52. "regler la hausse du canon a la valeur " 53. self . guns [id] . orienter (angle) 54. if id == self. id: 55 . self . connex . signaler_angle (angle) 56. else: 57. self .pupi [id] . reglage (angle) 58. 59. def fermer_threads (self , evt) : 60. "couper les connexions existantes et refermer les threads" 61. self . connex . terminer () 62. self. active =0 # empecher acces ulterieurs a Tk 63. 64. def depl_aleat_canon (self , id): 65 . pass # => methode inoperante 66. 67. def goal (self, a, b) : 68 . pass # => methode inoperante 69. 70. 71. class ThreadSocket (threading. Thread) : 72. """objet thread gerant l'echange de messages avec le serveur""" 73. def init (self, boss, host, port): 74 . threading. Thread. init (self) 75. self.app = boss # ref. de la fenetre application 76. # Mise en place du socket - connexion avec le serveur : 77. self . connexion = socket . socket (socket .AF_INET, socket . SOCK_STREAM) 78. try: 79. self .connexion. connect ( (host, port)) 80 . except socket . error : 81. print "La connexion a echoue . " 82. sys.exit() 83. print "Connexion etablie avec le serveur." 84. 85 . def run (self ) : 86. while 1: 87. msg_recu = self . connexion . recv (1024) 88. print "*%s*" % msg_recu 89. # le message recu est d'abord convert! en une liste : 90. t =msg_recu . split (',' ) 91. if t[0] =="" or t[0] =="fin": 92 . # f ermer le present thread : 93 . break 94. elif t[0] ==" serveur OK": 95. self . connexion . send ( "client OK") 96. elif t[0] =="canons": 97. self . connexion . send ( "OK" ) # accuse de reception 98 . # eliminons le ler et le dernier element de la liste . 99. # ceux qui restent sont eux-memes des listes : 100. lc = t[l:-l] 101. # chacune est la description complete d'un canon : 102. for g in lc: 103. s = g.splitC ; ') 104. self . app . a jouter_canon (s [0] , s[l], s[2], s[3], s[4]) 105. elif t[0] =="nouveau_canon" : 106. self .app.ajouter_canon(t[l] , t[2], t[3], t[4], t[5]) 107. if len(t) >6: 108 . self . app . activer_pupitre_personnel (t [1] ) 109. elif t[0] =='angle': 110. # il se peut que l'on ait recu plusieurs infos regroupees . 111. # on ne considere alors que la premiere : 112. self . app . orienter_canon (t [1] , t[2]) 298. Gerard Swinnen : Apprendre a programmer avec Python 113. elif t[0] =="tir_de": 114. self .app. tir_canon (t [1] ) 115. elif t[0] =="scores": 116. # eliminons le ler et le dernier element de la liste. 117 . # ceux qui restent sont eux-memes des listes : 118. Ic = t[l:-l] 119. # chaque element est la description d'un score : 120. for g in lc: 121. s = g. split ('; ') 122. self .app. imposer_score (s [0] , s[l]) 123. elif t[0] =="mouvement_de" : 124 . self .app. deplacer_canon (t[l],t[2],t[3]) 125. elif t[0] =="depart_de" : 126. self . app . enlever_canon (t [1] ) 127. 128. # Le thread se termine ici . 129. print "Client arrete. Connexion interrompue . " 130. self . connexion . close () 131. 132. def signaler_tir (self ) : 133. self . connexion . send (' feu ' ) 134. 135. def signaler_angle (self , angle): 136. self . connexion . send (' orienter, %s, ' % angle) 137. 138. def terminer (self ) : 139. self . connexion . send (' fin ' ) 140. 141. # Programme principal 142. if name == ' main ' : 143. AppClient (host, port, largeur, hauteur) .mainloop ( ) 144. Commentaires : • Lignes 15, 16 : Vous pouvez vous-meme perfectionner ce script en lui ajoutant un formulaire qui demandera ces valeurs a l'utilisateur au cours du demarrage. • Lignes 19 a 27: Le constructeur de la classe parente se termine en invoquant la methode specificites(). On peut done placer dans celle-ci ce qui doit etre construit differemment dans le serveur et dans les clients. (Le serveur instancie notamment un widget text qui n'est pas repris dans les clients ; l'un et l'autre demarrent des objets threads differents pour gerer les connexions). • Lignes 39 a 42 : Cette methode est invoquee chaque fois que l'utilisateur enfonce le bouton de tir. Le canon ne peut cependant pas effectuer des tirs en rafale. Par consequent, aucun nouveau tir ne peut etre accepte tant que l'obus precedent n'a pas termine sa trajectoire. C'est la valeur « vraie » ou « fausse » renvoyee par la methode feu() de l'objet canon qui indique si le tir a ete accepte ou non. On utilise cette valeur pour ne signaler au serveur (et done aux autres clients) que les tirs qui ont effectivement eu lieu. • Lignes 105 a 108 : Un nouveau canon doit etre ajoute dans l'espace de jeu de chacun (e'est-a-dire dans le canevas du serveur, et dans le canevas de tous les clients connectes), chaque fois qu'un nouveau client se connecte. Le serveur envoie done a ce moment un meme message a tous les clients pour les informer de la presence de ce nouveau partenaire. Mais le message envoye a celui-ci en particulier comporte un champ supplemental (lequel contient simplement la chaine « le votre »), afin que ce partenaire sache que ce message concerne son propre canon, et qu'il puisse done activer le pupitre correspondant, tout en memorisant l'identifiant qui lui a ete attribue par le serveur (voir egalement les lignes 35 a 37). Gerard Swinnen : Apprendre a programmer avec Python 299. Conclusions et perspectives : Cette application vous a ete presentee dans un but didactique. Nous y avons deliberement simplifie un certain nombre de problemes. Par exemple, si vous testez vous-meme ces logiciels, vous constaterez que les messages echanges sont souvent rassembles en « paquets », ce qui necessiterait d'affiner les algorithmes mis en place pour les interpreter. De meme, nous avons a peine esquisse le mecanisme fondamental du jeu : repartition des joueurs dans les deux camps, destruction des canons touches, obstacles divers, etc. II vous reste bien des pistes a explorer ! (18) Exercices : 18.1. Simplifiez le script correspondant au client de « chat » decrit a la page 283, en supprimant l'un des deux objets threads. Arrangez-vous par exemple pour traiter remission de messages au niveau du thread principal. 18.2. Modifiez le jeu des bombardes (version monoposte) du chapitre 15 (voir pages 227 et suivantes), en ne gardant qu'un seul canon et un seul pupitre de pointage. Ajoutez-y une cible mobile, dont le mouvement sera gere par un objet thread independant (de maniere a bien separer les portions de code qui controlent l'animation de la cible et celle du boulet). 18.8 Utilisation de threads pour optimiser les animations. Le dernier exercice propose a la fin de la section precedente nous suggere une methodologie de developpements d'applications qui peut se reveler particulierement interessante, dans le cas de jeux video impliquant plusieurs animations simultanees. En effet : si vous programmez les differents elements animes d'un jeu comme des objets independants fonctionnant chacun sur son propre thread, alors non seulement vous vous simplifiez la tache et vous ameliorez la lisibilite de votre script, mais encore vous augmentez la vitesse d' execution et done la fluidite de ces animations. Pour arriver a ce resultat, vous devrez abandonner la technique de temporisation que vous avez exploitee jusqu'ici, mais celle que vous allez utiliser a sa place est finalement plus simple ! 18.8.1 Temporisation des animations a I'aide de after() Dans toutes les animations que nous avons decrites jusqu'a present, le « moteur » etait constitute a chaque fois par une fonction contenant la methode after(), laquelle est associee d'office a tous les widgets Tkinter. Vous savez que cette methode permet d'introduire une temporisation dans le deroulement de votre programme : un chronometre interne est active, de telle sorte qu'apres un intervalle de temps convenu, le systeme invoque automatiquement une fonction quelconque. En general, e'est la fonction contenant after() qui est elle-meme invoquee : on realise ainsi une boucle recursive, dans laquelle il reste a programmer les deplacements des divers objets graphiques. Vous devez bien comprendre que pendant l'ecoulement de l'intervalle de temps programme a I'aide de la methode after(), votre application n'est pas du tout « figee ». Vous pouvez par exemple pendant ce temps : cliquer sur un bouton, redimensionner la fenetre, effectuer une entree clavier, etc. Comment cela est-il rendu possible ? Nous avons mentionne deja a plusieurs reprises le fait que les applications graphiques modernes comportent toujours une sorte de moteur qui « tourne » continuellement en tache de fond : ce dispositif se met en route lorsque vous activez la methode mainloop() de votre fenetre principale. Comme son nom l'indique fort bien, cette methode met en oeuvre une boucle repetitive perpetuelle, du meme type que les boucles while que vous connaissez bien. De nombreux mecanismes sont integres a ce « moteur ». L'un d'entre eux consiste a receptionner tous les evenements qui se 300. Gerard Swinnen : Apprendre a programmer avec Python produisent, et a les signaler ensuite a l'aide de messages appropries aux programmes qui en font la demande (voir : Programmes pilotes par des evenements, page 85), d'autres controlent les actions a effectuer au niveau de l'affichage, etc. Lorsque vous faites appel a la methode after() d'un widget, vous utilisez en fait un mecanisme de chronometrage qui est integre lui aussi a mainloop(), et c'est done ce gestionnaire central qui declenche l'appel de fonction que vous souhaitez, apres un certain intervalle de temps. La technique d'animation utilisant la methode after() est la seule possible pour une application fonctionnant toute entiere sur un seul thread, parce que c'est la boucle mainloop() qui dirige l'ensemble du comportement d'une telle application de maniere absolue. C'est notamment elle qui se charge de redessiner tout ou partie de la fenetre chaque fois que cela s'avere necessaire. Pour cette raison, vous ne pouvez pas imaginer de construire un moteur d'animation qui redefinirait les coordonnees d'un objet graphique a l'interieur d'une simple boucle while, par exemple, parce que pendant tout ce temps l'execution de mainloop() resterait suspendue, ce qui aurait pour consequence que pendant tout ce temps aucun objet graphique ne serait redessine (en particulier celui que vous souhaitez mettre en mouvement !). En fait, toute l'application apparaitrait figee, aussi longtemps que la boucle while ne serait pas interrompue. Puisqu'elle est la seule possible, c'est done cette technique que nous avons utilisee jusqu'a present dans tous nos exemples d'applications mono-thread. Elle comporte cependant un inconvenient genant : du fait du grand nombre d'operations prises en charge a chaque iteration de la boucle mainloop(), la temporisation que Ton peut programmer a l'aide de after() ne peut pas etre tres courte. Par exemple, elle ne peut guere descendre en dessous de 15 ms sur un PC typique (processeur de type Pentium IV, f = 1,5 GHz). Vous devez tenir compte de cette limitation si vous souhaitez developper des animations rapides. Un autre inconvenient lie a l'utilisation de la methode after() reside dans la structure de la boucle d'animation (a savoir une fonction ou une methode « recursive », e'est-a-dire qui s'appelle elle- meme) : il n'est pas toujours simple en effet de bien maitriser ce genre de construction logique, en particulier si Ton souhaite programmer l'animation de plusieurs objets graphiques independants, dont le nombre ou les mouvements doivent varier au cours du temps. 18.8.2 Temporisation des animations a l'aide de time.sleepQ Vous pouvez ignorer les limitations de la methode after() evoquees ci-dessus, si vous en confiez l'animation de vos objets graphiques a des threads independants. En procedant ainsi, vous vous liberez de la tutelle de mainloop(), et il vous est permis alors de construire des procedures d'animation sur la base de structures de boucles plus « classiques », utilisant l'instruction while ou l'instruction for par exemple. Au coeur de chacune de ces boucles, vous devez cependant toujours veiller a inserer une temporisation pendant laquelle vous « rendez la main » au systeme d' exploitation (afin qu'il puisse s'occuper des autres threads). Pour ce faire, vous ferez appel a la fonction sleep() du module time. Cette fonction permet de suspendre l'execution du thread courant pendant un certain intervalle de temps, pendant lequel les autres threads et applications continuent a fonctionner. La temporisation ainsi produite ne depend pas de mainloop(), et par consequent, elle peut etre beaucoup plus courte que celle que vous autorise la methode after(). Attention : cela ne signifie pas que le rafraichissem*nt de l'ecran sera lui-meme plus rapide, car ce rafraichissem*nt continue a etre assure par mainloop(). Vous pourrez cependant accelerer fortement les differents mecanismes que vous installez vous-meme dans vos procedures d'animation. Dans un logiciel de jeu, par exemple, il est frequent d'avoir a comparer periodiquement les positions de deux mobiles (tels qu' un projectile et une cible), afin de pouvoir entreprendre une Gerard Swinnen : Apprendre a programmer avec Python 301. action lorsqu'ils se rejoignent (explosion, ajout de points a un score, etc.)- Avec la technique d'animation decrite ici, vous pouvez effectuer beaucoup plus souvent ces comparaisons et done esperer un resultat plus precis. De meme, vous pouvez augmenter le nombre de points pris en consideration pour le calcul d'une trajectoire en temps reel, et done affiner celle-ci. Remarque : Lorsque vous utilisez la methode afterQ, vous devez lui indiquer la temporisation souhaitee en millisecondes, sous la forme d'un argument entier. Lorsque vous faites appel a la fonction sleep(), par contre, I'argument que vous transmettez doit etre exprime en secondes, sous la forme d'un reel (float). Vous pouvez cependant utiliser des tres petites valeurs (0.0003 par ex.). 18.8.3 Exemple concret Le petit script reproduit ci-dessous illustre la mise en oeuvre de cette technique, dans un exemple volontairement minimaliste. II s'agit d'une petite application graphique dans laquelle une figure se deplace en cercle a l'interieur d'un canevas. Son « moteur » mainloop() est lance comme d'habitude sur le thread principal. Le constructeur de l'application instancie un canevas contenant le dessin d'un cercle, un bouton et un objet thread. C'est cet objet thread qui assure l'animation du dessin, mais sans faire appel a la methode after() d'un widget. II utilise plutot une simple boucle while tres classique, installee dans sa methode run(). 1 . from Tkinter import * 2 . from math import sin, cos 3. import time, threading 4 . 5. class App (Frame): 6. def init (self): 7. Frame. init (self) 8. self. pack () 9. can =Canvas (self , width =400, height =400, 10. bg ='ivory', bd =3, relief =SUNKEN) 11. can .pack (padx =5, pady =5) 302. Gerard Swinnen : Apprendre a programmer avec Python 12. cercle = can . create_oval (185, 355, 215, 385, fill ='red') 13. tb = Thread_balle (can, cercle) 14. Button(self, text ='Marche', command =tb. start) .pack (side =LEFT) 15. # Button(self, text ='Arret', command =tb. stop) .pack (side =RIGHT) 16. # arreter 1' autre thread si l'on ferme la fenetre : 17. self .bind ( '' , tb.stop) 18. 19. class Thread_balle (threading . Thread) : 20. def init (self, canevas, dessin) : 21 . threading. Thread. init (self) 22. self. can, self. dessin = canevas, dessin 23. self.anim =1 24. 25. def run (self) : 26. a = 0.0 27. while self.anim == 1: 28. a += .01 29. x, y = 200 + 170*sin(a), 200 +170*cos (a) 30. self .can. coords (self .dessin, x-15, y-15, x+15, y+15) 31. time . sleep (0 . 010) 32. 33. def stop (self, evt =0) : 34. self.anim =0 35. 36. App () .mainloop () Commentaires : • Lignes 13 & 14 : Afin de simplifier notre exemple au maximum, nous creons l'objet thread charge de l'animation, directement dans le constructeur de Implication principale. Cet objet thread ne demarrera cependant que lorsque l'utilisateur aura clique sur le bouton « Marche », qui active sa methode start() (rappelons ici que c'est cette methode integree qui lancera elle-meme la methode run() ou nous avons installe notre boucle d'animation). • Ligne 15 : Vous ne pouvez par redemarrer un thread qui s'est termine. De ce fait, vous ne pouvez lancer cette animation qu'une seule fois (tout au mo ins sous la forme presentee ici). Pour vous en convaincre, activez la ligne n° 15 en enlevant le caractere # situe au debut (et qui fait que Python considere qu'il s'agit d'un simple commentaire) : lorsque l'animation est lancee, un clic de souris sur le bouton ainsi mis en place provoque la sortie de la boucle while des lignes 27-3 1 , ce qui termine la methode run(). L'animation s'arrete, mais le thread qui la gerait s'est termine lui aussi. Si vous essayez de le relancer a l'aide du bouton « Marche », vous n'obtenez rien d'autre qu'un message d'erreur. • Lignes 26 a 3 1 : Pour simuler un mouvement circulaire uniforme, il suffit de faire varier continuellement la valeur d'un angle a. Le sinus et le cosinus de cet angle permettent alors de calculer les coordonnees x et y du point de la circonference qui correspond a cet angle 81 . A chaque iteration, Tangle ne varie que d'un centieme de radian seulement (environ 0,6°), et il faudra done 628 iterations pour que le mobile effectue un tour complet. La temporisation choisie pour ces iterations se trouve a la ligne 31 : 10 millisecondes. Vous pouvez accelerer le mouvement en diminuant cette valeur, mais vous ne pourrez guere descendre en dessous de 1 milliseconde (0.001 s), ce qui n'est deja pas si mal. 81 Vous pouvez trouver quelques explications complementaires a ce sujet, a la page 230. Gerard Swinnen : Apprendre a programmer avec Python 303. Chapitre 19 : Annexes 19.1 Installation de Python Si vous souhaitez essayer Python sur votre ordinateur personnel, n'hesitez pas : ['installation est tres facile (et parfaitement reversible). 19.2 Sous Windows Sur le site web officiel de Python : http://www.python.org , vous trouverez dans la section « Download » des logiciels d'installation automatique pour les differentes versions de Python. Vous pouvez en confiance choisir la derniere version « de production ». Par exemple, au 30/9/03, il s'agit de la version 2.3. 1 - Fichier a telecharger : Python-2 .3.1. exe Copiez ce fichier dans un repertoire temporaire de votre machine, et executez-le. Python s'installera par defaut dans un repertoire nomme « Python** » (** indiquant les deux premiers chiffres du n° de version), et des icones de lancement seront mises en place automatiquement. Lorsque Installation est terminee, vous pouvez effacer le contenu du repertoire temporaire. 19.3 Sous Linux Vous avez probablement installe votre systeme Linux a l'aide d'une distribution commerciale telle que SuSE, RedHat ou Mandrake. Installez simplement les paquetages Python qui en font partie, en n'omettant pas Tkinter (parfois installe en meme temps que la Python imaging library). 19.4 Sous MacOS Vous trouverez differentes versions de Python pour MacOS 9 et Mac OS X sur le site web de Jack Jansen : http://homepages.cwi.nl/~jack/macpython Remarque importante concernant les versions recentes de Python Depuis l'apparition de la version 2.3, il est vivement recommande aux francophones que nous sommes d'inclure l'un des pseudo-commentaires suivant au debut de tous nos scripts Python (a la l e ou 2 e ligne) : # -*- coding :Latin-l -*- Ou bien : # -*- coding :Utf-8 -*- Vous trouverez l'explication de cette necessite a la page 40. 19.5 Installation de SciTE (Scintilla Text Editor) SciTE est un excellent logiciel editeur, capable d'effectuer la coloration syntaxique, l'auto- completion et surtout le repliement de code (code folding), c'est a dire le masquage a volonte de differents blocs d'instructions (contenu d'une classe, d'une fonction, d'une boucle, etc.) : cette fonctionnalite se revele extremement pratique lorsque vos scripts commencent a s'allonger ... II integre egalement une fenetre de terminal ainsi qu'un raccourci pour lancement des scripts. Cet editeur est disponible pour Windows et pour Linux. Veuillez consulter le site web : http://www.scintilla.org/SciTE.html 304. Gerard Swinnen : Apprendre a programmer avec Python 19.5.1 Installation sous Linux : L'editeur Scintilla fait dorenavant partie des paquetages fournis d'office avec les distributions recentes de Linux. Sinon, telechargez-le au depart du site web mentionne ci-dessus. Ensuite : • Telecharger l'archive gscite***.tgz puis l'extraire avec tar. • Installer 1' executable SciTE dans /usr/local/bin • Installer tout le reste (fichiers * .properties) dans /usr/share/scite (et non /usr/share/gscite !) 19.5.2 Installation sous Windows : • Telecharger l'archive wscite***.zip puis l'extraire dans \Program files • Installer une icone de lancement pour l'executable SciTe.exe 19.5.3 Pour les deux versions : On peut personnaliser beaucoup de choses (polices, etc.) en editant le fichier des proprietes globales (Menu Options — > Open global options file) Par exemple, pour activer de jolis symboles pour replier/deplier, dans la marge de gauche : fold. symbols =2 # pour de belles icones + et - cerclees fold. on. open =1 # ainsi tout est plie au depart margin . width =0 # pour supprimer la marge inutile Pour forcer le remplacement automatique des tabulations par des groupes de 4 espaces : tabsize = 4 indent. size = 4 use. tabs = 0 19.6 Installation des Python mega-widgets Visitez le site web : http://pmw.sourceforge.net et cliquez sur le lien : Download Pmwl2tar.gz pour telecharger le fichier correspondant. Decomprimez ce fichier archive dans un repertoire temporaire, a l'aide d'un logiciel de decompression tel que tar, Winzip, Info-Zip, unzip .... Recopiez l'integralite du sous-repertoire Pmw qui s'est cree automatiquement, dans le repertoire ou se trouve deja l'essentiel de votre installation de Python. Sous Windows, il s'agira par exemple de c : \Python23 Sous Linux, il s'agira vraisemblablement de /usr/lib/python Gerard Swinnen : Apprendre a programmer avec Python 305. 19.7 Installation de Gadfly (systeme de bases de donnees) Depuis le site http://sourceforge.net/projects/gadfly . telecharger le paquetage gadfly-l.O.O.tar.gz II s'agit d'un fichier archive comprime. Copiez ce fichier dans un repertoire temporaire. 19.8 Sous Windows : Dans un repertoire temporaire quelconque, decomprimez le fichier archive a l'aide d'un logiciel tel que Winzip. Ouvrez une fenetre DOS et entrez dans le sous-repertoire qui s'est cree automatiquement. Lancez la commande : python setup. py install C'esttout. Vous pouvez eventuellement ameliorer les performances, en ajoutant l'operation suivante : Dans le sous-repertoire qui s'est cree, ouvrez le sous-repertoire kjbuckets, puis le sous-repertoire qui correspond a votre version de Python. Recopiez le fichier *.pyd qui s'y trouve dans le repertoire racine de votre installation de Python. Lorsque tout est termine, effacez le contenu de votre repertoire temporaire. 19.8.1 Sous Linux : En tant qu'administrateur (root), choisissez un repertoire temporaire quelconque et decomprimez-y le fichier archive a l'aide de l'utilitaire tar, qui fait certainement partie de votre distribution. Entrez simplement la commande : tar -xvzf gadfly- 1 .0.0. tar . gz Entrez dans le sous-repertoire qui s'est cree automatiquement : cd gadfly- 1 . 0 . 0 Lancez la commande : python setup. py install C'esttout. Si votre systeme Linux comporte un compilateur C, vous pouvez ameliorer les performances de Gadfly en recompilant la bibliotheque kjbuckets. Pour ce faire, entrez encore les deux commandes suivantes : cd kjbuckets python setup. py install Lorsque tout est termine, effacez tout le contenu du repertoire temporaire. 306. Gerard Swinnen : Apprendre a programmer avec Python 19.9 Solutions a ux exercices Pour quelques exercices, nous ne fournissons pas de solution. Efforcez-vous de les trouver sans aide, meme si cela vous semble difficile. C'est en effet en vous acharnant sur de tels problemes que vous apprendrez le mieux. Exercice 4.2 : »> c = 0 »> while c < 20: c = c +1 print c, "x 7 =", c*7 ou encore : »> c = 1 »> while c <= 20: print c, "x 7 =", c*7 c = c +1 Exercice 4.3 : »> s = l »> while s <= 16384: print s, "euro(s) =", s *1.65, "dollar (s) " s = s *2 Exercice 4.4 : »> a, c = 1, 1 »> while c < 13: print a, ... a, c = a *3, c+1 Exercice 4.6 : # Le nombre de secondes est fourni au depart : # (un grand nombre s ' impose ! ) nsd = 12345678912 # Nombre de secondes dans une journee : nspj = 3600 * 24 # Nombre de secondes dans un an (soit 365 jours - # on ne tiendra pas compte des annees bissextiles) : nspa = nspj * 365 # Nombre de secondes dans un mois (en admettant # pour chague mois une duree identique de 30 jours) : nspm = nspj * 30 # Nombre d' annees contenues dans la duree fournie : na = nsd / nspa # division nsr = nsd % nspa # n. de sec. restantes # Nombre de mois restants : nmo = nsr / nspm # division nsr = nsr % nspm # n. de sec. restantes # Nombre de jours restants : nj = nsr / nspj # division nsr = nsr % nspj # n. de sec. restantes # Nombre d'heures restantes : nh = nsr / 3600 # division nsr = nsr % 3600 # n. de sec. restantes # Nombre de minutes restantes : Gerard Swinnen : Apprendre a programmer avec Python 307. nmi = nsr /60 # division nsr = nsr % 60 # n. de sec. restantes print "Nombre de secondes a convertir : " , nsd print "Cette duree correspond a", na, "annees de 365 jours, plus" print nmo, "mois de 30 jours,", print nj, "jours,", print nh, "heures,", print nmi, "minutes et", print nsr, "secondes." Exercice 4.7 : # affichage des 20 premiers termes de la table par 7, # avec signalement des multiples de 3 : i = 1 # compteur : prendra successivement les valeurs de 1 a 20 while i < 21: # calcul du terme a afficher : t = i * 7 # affichage sans saut a la ligne (utilisation de la virgule) : print t, # ce terme est-il un multiple de 3 ? (utilisation de l'operateur modulo) : if t % 3 == 0: print "*", # affichage d'une asterisque dans ce cas i = i + 1 # incrementation du compteur dans tous les cas Exercice 5.1 : # Conversion degres -> radians # Rappel : un angle de 1 radian est un angle qui correspond a une portion # de circonference de longueur egale a celle du rayon. # Puisque la circonference vaut 2 pi R, un angle de 1 radian correspond # a 360° / 2 pi , ou encore a 180° / pi # Angle fourni au depart en degres, minutes, secondes : deg, min, sec = 32, 13, 49 # Conversion des secondes en une fraction de minute : # (le point decimal force la conversion du resultat en un nombre reel) fm = sec/ 60 . # Conversion des minutes en une fraction de degre : fd = (min + fm) /60 # Valeur de 1' angle en degres "decimalises" : ang = deg + fd # Valeur de pi : pi = 3.14159265359 # Valeur d'un radian en degres : rad = 180 / pi # Conversion de 1 ' angle en radians : arad = ang / rad # Affichage : print deg, min, " "' , sec, "' =', arad, "radian (s) " 308. Gerard Swinnen : Apprendre a programmer avec Python Exercice 5.3 : # Conversion °Fahrenheit <-> °Celsius # A) Temperature fournie en °C : tempC =25 # Conversion en °Fahrenheit : tempF = tempC * 1.8 + 32 # Affichage : print tempC, "°C =", tempF, "°F" # B) Temperature fournie en °F : tempF = 25 # Conversion en °Celsius : tempC = (tempF - 32) / 1.8 # Affichage : print tempF, "°F =", tempC, "°C" Exercice 5.5 »> a, b = 1, 1 »> while b<65: print b, a a,b = a*2, b+1 # variante b = 1. Exercice 5.6 : # Recherche d'un caractere particulier dans une chaine # Chaine fournie au depart : ch = "Monty python flying circus" # Caractere a rechercher : cr = "e" # Recherche proprement dite : lc = len(ch) # nombre de caracteres a tester i = 0 # indice du caractere en cours d'examen t = 0 # "drapeau" a lever si le caractere recherche est present while i < lc: if ch[i] == cr: t = 1 i = i + 1 # Affichage : print "Le caractere", cr, if t == 1: print "est present", else : print "est inrouvable", print "dans la chaine", ch Exercice 5.8 : # Insertion d'un caractere d'espacement dans une chaine # Chaine fournie au depart : ch = "Gaston" # Caractere a inserer : cr = "*" # Le nombre de caracteres a inserer est inferieur d'une unite au Gerard Swinnen : Apprendre a programmer avec Python 309. # nombre de caracteres de la chaine . On traitera done celle-ci a # partir de son second caractere (en omettant le premier) . lc = len(ch) # nombre de caracteres total 1=1 # indice du premier caractere a examiner (le second, en fait) nch = ch[0] # nouvelle chaine a construire (contient deja le premier car.) while i < lc: nch = nch + cr + ch [ i ] i = i + 1 # Affichage : print nch Exercice 5.9 : # Inversion d'une chaine de caracteres # Chaine fournie au depart : ch = "zorglub" lc = len(ch) # nombre de caracteres total i = lc - 1 # le traitement commencera a partir du dernier caractere nch = "" # nouvelle chaine a construire (vide au depart) while i >= 0 : nch = nch + ch [ i ] i = i - 1 # Affichage : print nch Exercice 5.11 : # Combinaison de deux listes en une seule # Listes fournies au depart : tl = [31,28,31,30,31,30,31,31,30,31,30,31] t2 = [ ' Janvier ' , ' Fevrier ' , ' Mars ' , ' Avril ' , ' Mai ' , ' Juin ' , ' Juillet ' , ' Aout ' , ' Septembre ' , ' Octobre ' , ' Novembre ' , ' Decembre ' ] # Nouvelle liste a construire (vide au depart) : t3 = [] # Boucle de traitement : i = 0 while i < len(tl) : t3. append (t2 [i] ) t3. append (tl [i] ) i = i + 1 # Affichage : print t3 Exercice 5.12 : # Affichage des elements d'une liste # Liste fournie au depart : t2 = [ ' Janvier ' , ' Fevrier ' , ' Mars ' , ' Avril ' , ' Mai ' , ' Juin ' , ' Juillet ' , ' Aout ' , ' Septembre ' , ' Octobre ' , ' Novembre ' , ' Decembre ' ] # Affichage : i = 0 while i < len(t2): print t2 [i] , i = i + 1 310. Gerard Swinnen : Apprendre a programmer avec Python Exercice 5.13 : # Recherche du plus grand element d'une liste # Liste fournie au depart : tt = [32, 5, 12, 8, 3, 75, 2, 15] # Au fur et a mesure du traitement de la liste, on memo riser a dans # la variable ci-dessous la valeur du plus grand element deja trouve : max = 0 # Examen de tous les elements : i = 0 while i < len(tt) : if tt [i] > max : max = tt[i] # memorisation d'un nouveau maximum i = i + 1 # Affichage : print "Le plus grand element de cette liste a la valeur", max Exercice 5.14 : # Separation des nombres pairs et impairs # Liste fournie au depart : tt = [32, 5, 12, 8, 3, 75, 2, 15] pairs = [] impairs = [] # Examen de tous les elements : i = 0 while i < len(tt) : if tt[i] % 2 == 0: pairs . append (tt [i] ) else : impairs . append ( tt [ i ] ) i = i + 1 # Affichage : print "Nombres pairs : " , pairs print "Nombres impairs : " , impairs Exercice 6.1 : # Conversion de miles/heure en km/h et m/s print "Veuillez entrer le nombre de miles parcourus en une heure : ", ch = raw_input ( ) # en general preferable a input ( ) mph = float (ch) # conversion de la chaine entree en nombre reel mps = mph * 1609 / 3600 # conversion en metres par seconde kmph = mph * 1.609 # conversion en km/h # affichage : print mph, "miles/heure =", kmph, "km/h, ou encore", mps, "m/s" Exercice 6.2 : # Perimetre et Aire d'un triangle quelconque from math import sqrt print "Veuillez entrer le cote a : " a = float (raw_input () ) print "Veuillez entrer le cote b : " b = float (raw_input () ) Gerard Swinnen : Apprendre a programmer avec Python 311. print "Veuillez entrer le cote c : " c = float (raw_input () ) d = (a + b + c)/2 # demi s = sqrt (d* (d-a) * (d-b) * (d-c) ) # aire print "Longueur des cotes =", a, b, c print "Perimetre =", d*2, "Aire =", s Exercice 6.4 : # Entree d' elements dans une liste tt = [] # Liste a completer (vide au depart) ch = "start" # valeur quelconque (mais non nulle) while ch ! = " " : print "Veuillez entrer une valeur : " ch = raw_input ( ) if ch != "": tt . append (float (ch) ) # variante : tt . append (ch) # affichage de la liste : print tt -perimetre (suivant formule) Exercice 6.8 : # Traitement de nombres entiers compris entre deux limites print "Veuillez entrer la limite inf erieure : " , a = input () print "Veuillez entrer la limite superieure : " , b = input () s = 0 # some recherchee (nulle au depart) # Parcours de la serie des nombres compris entre a et b : n = a # nombre en cours de traitement while n <= b: if n % 3 ==0 and n % 5 ==0: # variante : 'or' au lieu de 'and' s = s + n n = n + 1 print "La somme recherchee vaut", s Exercice 6.9 : # Annees bissextiles print "Veuillez entrer 1 ' annee a tester : " , a = input ( ) if a % 4 != 0: # a n'est pas divisible par 4 -> annee non bissextile bs = 0 else : if a % 400 ==0: # a divisible par 400 -> annee bissextile bs = 1 elif a % 100 ==0: # a divisible par 100 -> annee non bissextile bs = 0 else : 312. Gerard Swinnen : Apprendre a programmer avec Python # autres cas ou a est divisible par 4 -> annee bissextile bs = 1 if bs ==1: ch = "est" else : ch = "n'est pas" print "L' annee", a, ch, "bissextile." Variante (proposee par Alex Misbah ) : a=input ( ' entree une annee : ' ) if (a%4==0) and ((a%100!=0) or (a%400==0) ) : print a, "est une annee bissextile" else : print a, "n'est pas une annee bissextile" Exercice 6.11 : Calculs de triangles from sys import exit # module contenant des fonctions systeme print """ Veuillez entrer les longueurs des 3 cotes (en separant ces valeurs a l'aide de virgules) a, b, c = input () # II n'est possible de construire un triangle que si chaque cote # a une longueur inferieure a la somme des deux autres : if a < (b+c) and b < (a+c) and c < (a+b) : print "Ces trois longueurs determinent bien un triangle." else : print "II est impossible de construire un tel triangle !" exit() # ainsi 1 ' on n ' ira pas plus loin. f = 0 if a == b and b == c : print "Ce triangle est equilateral." f = 1 elif a == b or b == c or c == a : print "Ce triangle est isocele . " f = 1 if a*a + b*b == c*c or b*b + c*c == a*a or c*c + a*a == b*b : print "Ce triangle est rectangle." f = 1 if f == 0 : print "Ce triangle est quelconque." Gerard Swinnen : Apprendre a programmer avec Python 313. Exercice 6.15 : # Notes de travaux scolaires notes = [] # liste a construire n = 2 # valeur positive quelconque pour initier la boucle while n >= 0 : print "Entrez la note suivante, s.v.p. : ", n = f loat (raw_input ( ) ) # conversion de 1' entree en un nombre reel if n < 0 : print "OK. Termine . " else : notes . append (n) # ajout d'une note a la liste # Calculs divers sur les notes deja entrees : # valeurs minimale et maximale + total de toutes les notes . min = 500 # valeur superieure a toute note max, tot, i = 0, 0, 0 nn = len (notes) # nombre de notes deja entrees while i < nn : if notes [i] > max: max = notes [i] if notes [i] < min: min = notes [i] tot = tot + notes [i] moy = tot/nn i = i + 1 print nn, "notes entrees. Max =", max, "Min =", min, "Moy =", moy Exercice 7.3 : from math import pi def surfCercle (r) : "Surface d'un cercle de rayon r" return pi * r**2 # test : print surfCercle (2 . 5) Exercice 7.4 : def volBoite(xl, x2, x3) : "Volume d'une boite parallelipipedique" return xl * x2 * x3 # test : print volBoite (5 . 2, 7.7, 3.3) 314. Gerard Swinnen : Apprendre a programmer avec Python Exercice 7.5 : def maximum (nl , n2, n3) : "Renvoie le plus grand de trois nombres" if nl >= n2 and nl >= n3 : return nl elif n2 >= nl and n2 >= n3: return n2 else : return n3 # test : print maximum (4. 5, 5.7, 3.9) Exercice 7.9 : def compteCar (ca, ch) : "Renvoie le nombre de caracteres ca trouves dans la chaine ch" i, tot =0, 0 while i < len(ch) : if ch[i] == ca: tot = tot + 1 i = i + 1 return tot # test : print compteCar ("e" , "Cette chaine est un exemple") Exercice 7.10 : def indexMax(tt) : "renvoie 1 ' indice du plus grand element de la liste tt" i, max =0, 0 while i < len(tt) : if tt[i] > max : max , imax = tt [ i ] , i i = i + 1 return imax # test : serie = [5, 8, 2, 1, 9, 3, 6, 4] print indexMax (serie) Exercice 7.11 : def nomMois (n) : "renvoie le nom du n-ieme mois de l'annee" mois = ['Janvier,', 'Fevrier', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Aout', 'Septembre', 'Octobre', 'Novembre', 'Decembre'] return mois [n -1] # les indices sont numerotes a partir de zero # test : print nomMois (4) Gerard Swinnen : Apprendre a programmer avec Python 315. Exercice 7.14 : def volBoite(xl =10, x2 =10, x3 =10): "Volume d'une boite parallelipipedique" return xl * x2 * x3 # test : print volBoite() print volBoite (5 . 2) print volBoite (5 . 2, 3) Exercice 7.15 : def volBoite (xl =-1, x2 "Volume d'une boite if xl == -1 : return xl elif x2 == -1 : return xl**3 elif x3 == -1 : return xl*xl*x2 else : return xl*x2*x3 # test : print volBoite () print volBoite (5 . 2) print volBoite (5 . 2, 3) print volBoite (5 . 2, 3, 7.4) Exercice 7.16 : def changeCar (ch, cal, ca2, debut =0, fin =-1): "Remplace tous les caracteres cal par des ca2 dans la chaine ch" if fin == -1: fin = len(ch) nch, i = "", 0 # nch : nouvelle chaine a construire while i < len(ch) : if i >= debut and i <= fin and ch[i] == cal: nch = nch + ca2 else : nch = nch + ch[i] i = i + 1 return nch # test : print changeCar ( "Ceci est une print changeCar ("Ceci est une print changeCar ( "Ceci est une =-1, x3 =-1) : parallelipipedique" # aucun argument n ' a ete f ourni # un seul argument -> boite cubique # deux arguments -> boite prismatique toute petite phrase", " ", "*") toute petite phrase", " ", "*", 8, 12) toute petite phrase", " ", "*", 12) 316. Gerard Swinnen : Apprendre a programmer avec Python Exercice 7.17 : def eleMax(lst, debut =0, fin =-1): "renvoie le plus grand element de la liste 1st" if fin == -1: fin = len(lst) max, i = 0, 0 while i < len(lst) : if i >= debut and i <= fin and lst[i] > max: max = lst[i] i = i + 1 return max # test : serie = [9, 3, 6, 1, 7, 5, 4, 8, 2] print eleMax (serie) print eleMax (serie, 2) print eleMax (serie, 2, 5) Exercice 8.7 : from Tkinter import * # Coordonnees X,Y des 5 anneaux : coord = [[20,30], [120,30], [220, 30], [70,80], [170,80]] # Couleurs des 5 anneaux : coul = ["red", "yellow", "blue", "green", "black"] base = Tk() can = Canvas (base, width =335, height =200, bg ="white") can. pack () bou = Button (base, text =" Quitter", command =base.quit) bou. pack (side = RIGHT) # Dessin des 5 anneaux : i = 0 while i < 5 : xl, yl = coord [i] [0], coord [i] [1] can . create_oval (xl, yl, xl+100, yl +100, width =2, outline =coul[i]) i = i +1 base . mainloop ( ) Variante : from Tkinter import * # Dessin des 5 anneaux : def dessineCercle (i) : xl, yl = coord [i] [0], coord [i] [1] can . create_oval (xl, yl, xl+100, yl +100, width =2, outline =coul[i]) def al () : dessineCercle (0) def a2 ( ) : dessineCercle (1) def a3 () : dessineCercle (2) def a4 () : dessineCercle (3) Gerard Swinnen : Apprendre a programmer avec Python 317. def a5 () : dessineCercle (4) # Coordonnees X,Y des 5 anneaux : coord = [[20,30], [120,30], [220, 30], [70,80], [170,80]] # Couleurs des 5 anneaux : coul = ["red", "yellow", "blue", "green", "black"] base = Tk() can = Canvas (base, width =335, height =200, bg ="white") can . pack ( ) bou = Button (base, text ="Quitter", command =base.quit) bou .pack (side = RIGHT) # Installation des 5 boutons : Button (base, text='l', command = al) Button (base, text='2', command = a2) Button (base, text='3', command = a3) Button (base, text='4', command = a4) Button (base, text='5', command = a5) base . mainloop ( ) .pack (side =LEFT) .pack (side =LEFT) .pack (side =LEFT) .pack (side =LEFT) .pack (side =LEFT) 318. Gerard Swinnen : Apprendre a programmer avec Python Exercices 8.9 et 8.10 : # Dessin d'un damier, avec placement de pions au hasard from Tkinter import * from random import randrange def damier ( ) : "dessiner dix lignes de carres y = 0 while y < 10: if y % 2 == 0: x = 0 else : x = 1 ligne_de_carres (x*c, y*c) y += 1 def ligne_de_carres (x, y) : "dessiner une ligne de carres, i = 0 while i < 10: can . create_rectangle (x, y, i += 1 x += c*2 # generateur de nombres aleatoires avec decalage alterne" # une fois sur deux, on # commencera la ligne de # carres avec un decalage # de la taille d'un carre en part ant de x, y" x+c, y+c, fill='navy') # espacer les carres def cercle (x, y, r, coul) : "dessiner un cercle de centre x,y et de rayon r" can . create_oval (x-r, y-r, x+r, y+r, fill=coul) def a jouter_pion () : "dessiner un pion au hasard sur le damier" # tirer au hasard les coordonnees du pion : x = c/2 + randrange (10) * c y = c/2 + randrange (10) * c cercle (x, y, c/3, 'red') ##### Programme principal : ############ # Tachez de bien "parametrer" vos programmes, comme nous 1 ' avons # fait dans ce script . Celui-ci peut en ef fet tracer des damiers # de n ' importe quelle taille en changeant seulement la valeur # d'une seule variable, a savoir la dimension des carres : c = 30 # taille des carres fen = Tk() can = Canvas (fen, width =c*10, height =c*10, bg =' ivory') can. pack (side =TOP, padx =5, pady =5) bl = Button (fen, text =' damier', command =damier) bl. pack (side =LEFT, padx =3, pady =3) b2 = Button (fen, text = 'pions', command =a jouter_pion) b2. pack (side =RIGHT, padx =3, pady =3) fen . mainloop ( ) # Gerard Swinnen : Apprendre a programmer avec Python 319. Exercice 8.12 : # Simulation du phenomene de gravitation universelle from Tkinter import * from math import sqrt def distance (xl, yl, x2, y2) : "distance separant les points xl,yl et x2,y2" d = sqrt ( (x2-xl) **2 + (y2-yl)**2) # theoreme de Pythagore return d def forceG(ml, m2, di) : "force de gravitation s ' exergant entre ml et m2 pour une distance di" return ml*m2*6 . 67e-ll/di**2 # loi de Newton def avance (n, gd, hb) : "deplacement de l'astre n, de gauche a droite ou de haut en bas" global x, y, step # nouvelles coordonnees : x[n], y[n] = x[n] +gd, y[n] +hb # deplacement du dessin dans le canevas : can . coords (astre [n] , x[n]-10, y[n]-10, x[n]+10, y[n]+10) # calcul de la nouvelle interdistance : di = distance (x[0] , y[0], x[l], y[l]) # conversion de la distance "ecran" en distance "astronomique" : diA = di*le9 # (1 pixel => 1 million de km) # calcul de la force de gravitation correspondante : f = f orceG (ml , m2 , diA) # affichage des nouvelles valeurs de distance et force : valDis . configure (text="Distance = " +str(diA) +" m") valFor. configure (text=" Force = " +str(f) +" N") # adaptation du "pas" de deplacement en fonction de la distance : step = di/10 def gauchel () : avance (0, -step, 0) def droitel () : avance (0, step, 0) def hautl () : avance (0, 0, -step) def basl () : avance (0, 0, step) def gauche2 () : avance (1, -step, 0) def droite2 () : avance (1, step, 0) def haut2 () : avance (1, 0, -step) def bas2 () : avance (1, 0, step) # Masses des deux astres : ml = 6e24 # (valeur de la masse de la terre, en kg) m2 = 6e24 # 320. Gerard Swinnen : Apprendre a programmer avec Python astre = [0]*2 x =[50. , 350. ] y =[100., 100.] step =10 # liste servant a memoriser les references des dessins # liste des coord. X de chaque astre (a l'ecran) # liste des coord. Y de chaque astre # "pas" de deplacement initial +str(ml) +" kg") +str(m2) +" kg") # Construction de la fenetre : fen = Tk() fen.title(' Gravitation universelle suivant Newton') # Libelles : valMl = Label (fen, text="Ml = valMl . grid (row =1, column =0) valM2 = Label (fen, text="M2 = valM2 . grid (row =1, column =1) valDis = Label (fen, text="Distance" ) valDis . grid (row =3, column =0) valFor = Label (fen, text="Force" ) valFor . grid (row =3, column =1) # Canevas avec le dessin des 2 astres: can = Canvas (fen, bg ="light yellow", width =400, height =200) can. grid (row =2, column =0, columnspan =2) astre[0] = can. create_oval (x [0] -10, y[0]-10, x[0]+10, y[0]+10, fill ="red", width =1) astre[l] = can. create_oval (x [1] -10, y[l]-10, x[l]+10, y[l]+10, fill ="blue", width =1) # 2 groupes de 4 boutons, chacun installe dans un cadre (frame) : fral = Frame (fen) fral . grid (row =4, column =0, sticky =W, padx =10) Button(fral, text="<-", fg =' red ', command =gauchel) .pack (side =LEFT) Button(fral, text="->", fg ='red', command =droitel) . pack (side =LEFT) Button(fral, text=" A ", fg ='red', command =hautl) .pack (side =LEFT) Button(fral, text="v", fg ='red', command =basl) .pack (side =LEFT) fra2 = Frame (fen) fra2 . grid (row =4, column =1, sticky =E, padx =10) Button(fra2, text="<-", fg ='blue', command =gauche2) .pack (side =LEFT) Button(fra2, text="->", fg ='blue', command =droite2) .pack (side =LEFT) Button(fra2, text=" A ", fg ='blue', command =haut2) .pack (side =LEFT) Button(fra2, text="v", fg ='blue', command =bas2) .pack (side =LEFT) fen . mainloop ( ) Gravitation universelle suivant Newton M1 = Ge+024 kg M2 = 6e+024 kg Distance = 56232995801 .7 m < l I I 'I Force = 7.5935681 0742e+01 7 N <- Gerard Swinnen : Apprendre a programmer avec Python 321. Exercice 8.16 : # Conversions de temperatures Fahrenheit <=> Celsius from Tkinter import * def convFar (event) : "valeur de cette temperature, exprimee en degres Fahrenheit" tF = eval ( champTC . get ( ) ) varTF.set (str (tF*l . 8 +32)) def convCel (event) : "valeur de cette temperature, exprimee en degres Celsius" tC = eval (champTF . get ( ) ) varTC . set (str ( (tC-32) /l . 8) ) fen = Tk() fen . title ( ' Fahrenheit/Celsius ' ) Label(fen, text='Temp. Celsius :' # "variable Tkinter" associee au > # assure 1 ' interface entre TCL et varTC =StringVar() champTC = Entry (fen, textvariable champTC . bind ( " " , convFar ) champTC . grid (row =0, column =1) # Initialisation du contenu de la varTC. set ("100.0") .grid (row =0, column =0) ihamp d' entree. Cet "ob jet -variable" Python (voir notes, page 165) : =varTC) variable Tkinter : Label(fen, text= ' Temp . Fahrenheit :').grid(row =1, column =0) varTF =StringVar() champTF = Entry (fen, textvariable =varTF) champTF .bind("" , convCel) champTF . grid (row =1, column =1) varTF.set ("212.0") fen . mainloop ( ) Fahrenheit/Celsius Temp. Celsius : 25.0 Temp. Fahrenheit : 1 77 0 njx] Exercice 8.18 a 8.20 : # Cercles et courbes de Lissajous from Tkinter import * from math import sin, cos def move ( ) : global ang, x, y # on memorise les coord, precedentes avant de calculer les nouvelles : xp, yp = x, y # rotation d ' un angle de 0.1 radian : ang = ang + . 1 # sinus et cosinus de cet angle => coord, d'un point du cercle trigone x, y = sin (ang), cos (ang) # Variante determinant une courbe de Lissajous avec fl/f2 = 2/3 : # x, y = sin(2*ang), cos(3*ang) # mise a l'echelle (120 = rayon du cercle, (150,150) = centre du canevas) 322. Gerard Swinnen : Apprendre a programmer avec Python x, y = x*120 + 150, y*120 + 150 can . coords (balle, x-10, y-10, x+10, y+10) can . create_line (xp, yp, x, y, fill ="blue") ang, x, y = 0., 150., 270. fen = Tk() f en. title ( 'Courbes de Lissajous') can = Canvas (fen, width =300, height=300, bg="white") can . pack ( ) balle = can. create_oval (x-10, y-10, x+10, y+10, fill='red') Button (fen, text='Go', command =move) . pack ( ) fen . mainloop ( ) Courbes de Lissajous Go Exercice 8.27 : # Chutes et rebonds from Tkinter import * def move ( ) : global x, y, v, dx, dv, flag xp, yp = x, y # memorisation des coord, precedentes # deplacement horizontal : if x > 385 or x < 15 : # rebond sur les parois laterales : dx = -dx # on inverse le deplacement x = x + dx # variation de la vitesse verticale (toujours vers le bas) : v = v + dv Gerard Swinnen : Apprendre a programmer avec Python 323. # deplacement vertical (proportionnel a la vitesse) y = y + v if y > 240: # niveau du sol a 240 pixels : y = 240 # defense d'aller + loin ! v = -v # rebond : la vitesse s ' inverse # on repositionne la balle : can . coords (balle, x-10, y-10, x+10, y+10) # on trace un bout de trajectoire : can . create_line (xp, yp, x, y, fill =' light grey') # ... et on remet 9a jusqu'a plus soif : if flag > 0 : fen . after (50, move) def start () : global flag flag = flag +1 if flag == 1: move ( ) def stop ( ) : global flag flag =0 # initialisation des coordonnees, des vitesses et du temoin d' animation : x, y, v, dx, dv, flag = 15, 15, 0, 6, 5, 0 fen = Tk() fen.title(' Chutes et rebonds ' ) can = Canvas (fen, width =400, height=250, bg="white") can . pack ( ) balle = can. create_oval (x-10, y-10, x+10, y+10, fill='red') Button(fen, text= ' Start ' , command =start) .pack (side =LEFT, padx =10) Button(fen, text='Stop', command =stop) .pack (side =LEFT) Button(fen, text= ' Quitter ' , command =f en . quit) .pack (side =RIGHT, padx =10) fen . mainloop ( ) 324. Gerard Swinnen : Apprendre a programmer avec Python Exercice 9.1 (editeur simple, pour lire et ecrire dans un fichier 'texte') : def sansDC (ch) : "cette fonction renvoie la chaine ch amputee de son dernier caractere" nouv = i, j = 0, len(ch) -1 while i < j : nouv = nouv + ch [ i ] i = i + 1 return nouv def ecrireDansFichier ( ) : of = open(nomF, 'a') while 1 : ligne = raw_input ( "entrez une ligne de texte (ou ) : ") if ligne == ' ' : break else : of .write (ligne + '\n') of . close () def lireDansFichier () : of = open(nomF, 'r') while 1 : ligne = of . readline () if ligne == " " : break # afficher en omettant le dernier caractere (= fin de ligne) : print sansDC (ligne) of . close () nomF = raw_input ( ' Nom du fichier a traiter : ') choix = raw_input (' Entrez "e" pour ecrire, "c" pour consulter les donnees : ') if choix == ' e ' : ecrireDansFichier ( ) else : lireDansFichier ( ) Exercice 9.3 (generation des tables de multiplication de 2 a 30) : def tableMulti (n) : # Fonction generant la table de multiplication par n (20 termes) # La table sera renvoyee sous forme d'une chaine de caracteres : i, ch = 0, "" while i < 20: i = i + 1 ch = ch + str(i * n) + " " return ch NomF = raw_input ( "Nom du fichier a creer : " ) fichier = open (NomF, 'w') # Generation des tables de 2 a 30 : table = 2 while table < 31 : fichier. write (tableMulti (table) + '\n') table = table + 1 fichier . close ( ) Gerard Swinnen : Apprendre a programmer avec Python 325. Exercice 9.4 : # Triplement des espaces dans un f ichier texte . # Ce script montre egalement comment modifier le contenu d'un f ichier # en le transferant d'abord tout entier dans une liste, puis en # reenregistrant celle-ci apres modifications def triplerEspaces (ch) : "fonction qui triple les espaces entre mots dans la chaine ch" i, nouv =0, "" while i < len(ch) : if ch[i] == " " : nouv = nouv + " " else : nouv = nouv + ch [ i ] i = i +1 return nouv NomF = raw_input ( "Norn du f ichier : ") f ichier = open (NomF, ' r+ ' ) # ' r+ ' = mode read/write lignes = f ichier . readlines () # lire toutes les lignes n=0 while n < len (lignes) : lignes [n] = triplerEspaces (lignes [n] ) n =n+l f ichier . seek (0) # retour au debut du fichier f ichier . writelines (lignes) # reenregistrement fichier . close ( ) Exercice 9.5 : # Mise en forme de donnees numeriques . # Le fichier traite est un fichier texte dont chaque ligne contient un nombre # reel (sans exposants et encode sous la forme d'une chaine de caracteres) def valArrondie (ch) : "representation arrondie du nombre presente dans la chaine ch" f = float (ch) # conversion de la chaine en un nombre reel e = int(f + .5) # conversion en entier (On ajoute d'abord # 0.5 au reel pour 1 ' arrondir correctement) return str(e) # reconversion en chaine de caracteres fiSource = raw_input ( "Nom du fichier a traiter : ") fiDest = raw_input ( "Nom du fichier destinataire : ") fs = open (fiSource, 'r') fd = open (fiDest, 'w') while 1 : ligne = f s . readline () # lecture d'une ligne du fichier if ligne == "" or ligne == "\n": break ligne = valArrondie (ligne) fd. write (ligne +"\n") fd. close () fs . close () 326. Gerard Swinnen : Apprendre a programmer avec Python Exercice 9.6 : # Comparaison de deux fichiers, caractere par caractere : fichl = raw_input ( "Nom du premier fichier : ") fich2 = raw_input ( "Nom du second fichier : ") fil = open (fichl, 'r') fi2 = open(fich2, 'r' ) c, f = 0, 0 while 1 : c = c + 1 carl = fil. read (1) car2 = fi2.read(l) if carl =="" or car2 break if carl != car2 : f = 1 break fil . close () fi2 .close () print "Ces 2 fichiers", if f ==1: print "different a partir du caractere n°", c else : print "sont identiques . " Exercice 9.7 : # Combinaison de deux fichiers texte pour en faire un nouveau fichA = raw_input ( "Nom du premier fichier : ") fichB = raw_input ( "Nom du second fichier : ") fichC = raw_input ( "Nom du fichier destinataire : ") fiA = open (fichA, 'r') fiB = open (fichB, 'r') fiC = open(fichC, 'w') while 1 : ligneA = fiA. readline () ligneB = fiB. readline () if ligneA =="" and ligneB =="": break # On est arrive a la fin des 2 fichiers if ligneA != "" : fiC .write (ligneA) if ligneB != "" : fiC .write (ligneB) fiA. close () fiB. close () fiC. close () # compteur de caracteres et "drapeau" # lecture d'un caractere dans chacun # des deux fichiers # difference trouvee Gerard Swinnen : Apprendre a programmer avec Python 327. Exercice 9.8 : # Enregistrer les coordonnees des membres d'un club def encodage ( ) : "renvoie la liste des valeurs entrees, ou une liste vide" print "*** Veuillez entrer les donnees (ou pour terminer) :" while 1 : nom = raw_input ( "Norn : ") if nom == "" : return [ ] prenom = raw_input ( "Prenom : ") rueNum = raw_input ( "Adresse (N° et rue) : ") cPost = raw_input ("Code postal : ") local = raw_input ( "Localite : ") tel = raw_input ( "N° de telephone : ") print nom, prenom, rueNum, cPost, local, tel ver = raw_input ( "Entrez si c'est correct, sinon ") if ver == "" : break return [nom, prenom, rueNum, cPost, local, tel] def enregistrer (liste) : "enregistre les donnees de la liste en les separant par des <#>" i = 0 while i < len (liste) : of .write (liste [i] + "#") i = i + 1 of .write ("\n") # caractere de fin de ligne nomF = raw_input ( ' Nom du fichier destinataire : ') of = open (nomF, 'a') while 1 : tt = encodage ( ) if tt == [] : break enregistrer (tt) of .close () 328. Gerard Swinnen : Apprendre a programmer avec Python Exercice 9.9 : # Ajouter des informations dans le fichier du club def traduire(ch) : " convert ir une ligne du dn = tt = [] i = 0 while i < len(ch) : if ch[i] == "#": tt . append (dn) dn ="" else : dn = dn + ch [ i ] i = i + 1 return tt fichier source en liste de donnees" # chaine temporaire pour extraire les donnees # la liste a produire # on ajoute la donnee a la liste, et # on reinitialise la chaine temporaire def encodage (tt) : "renvoyer la liste tt, completee avec la date de naissance et le sexe" print "*** Veuillez entrer les donnees (ou pour terminer) : " # Affichage des donnees deja presentes dans la liste : i = 0 while i < len(tt) : print tt [i] , i = i +1 print while 1 : daNai = raw_input ( "Date de naissance : ") sexe = raw_input ( "Sexe (m ou f) : ") print daNai , sexe ver = raw_input ( "Entrez si c'est correct, sinon ") if ver == " " : break tt . append (daNai) tt . append ( sexe ) return tt def enregistrer (tt) : "enregistrer les donnees de la liste tt en les separant par des <#>" i = 0 while i < len(tt) : fd. write (tt [i] + "#") i = i + 1 fd. write ("\n" ) # caractere de fin de ligne f Source = raw_input ( ' Norn du fichier source : ') fDest = raw_input ( ' Nom du fichier destinataire : ') fs = open (f Source, 'r') f d = open ( fDest , ' w ' ) while 1 : ligne = f s . readline () # lire une ligne du fichier source if ligne =="" or ligne =="\n": break liste = traduire (ligne) # la convertir en une liste liste = encodage (liste) # y ajouter les donnees supplementaires enregistrer (liste) # sauvegarder dans fichier dest . fd. close () f s . close () Gerard Swinnen : Apprendre a programmer avec Python 329. Exercice 9.10 : # Recherche de lignes particulieres dans un fichier texte def chercheCP (ch) : "recherche dans ch i, f, ns = 0, 0, 0 cc = "" while i < len(ch) : if ch[i] =="#": ns = ns +1 if ns ==3 : f = 1 elif ns ==4 : break elif f ==1: cc = cc + ch[i] i = i +1 return cc la portion # de chaine contenant le code postal" ns est un compteur de codes # chaine a construire le CP se trouve apres le 3e code # variable "drapeau" (flag) inutile de lire apres le 4e code # le caractere lu fait partie CP recherche -> on memorise du nomF = raw_input ( "Norn du fichier a traiter : ") codeP = raw_input ( "Code postal a rechercher : ") fi = open (nomF, 'r') while 1 : ligne = f i . readline ( ) if ligne =="": break if chercheCP (ligne) == codeP : print ligne fi. close () Exercice 10.2 (decoupage d'une chaine en fragments) : def decoupe (ch, n) : "decoupage de la chaine ch en une liste de fragments de n caracteres" d, f = 0, n tt = [] while d < len(ch) : if f > len(ch) : f = len(ch) fr = ch[d:f] tt . append (fr) d, f = f, f +n return tt # indices de debut et de fin de fragment # liste a construire # on ne peut pas decouper au-dela de la fin # decoupage d'un fragment # ajout du fragment a la liste # indices suivants def inverse(tt): "rassemble les elements de la liste tt dans l'ordre inverse" ch = "" # chaine a construire i = len(tt) # on commence par la fin de la liste while i > 0 : i = i - 1 # le dernier element possede 1 ' indice n -1 ch = ch + tt[i] return ch # Test : ch = " abcde f ghi j klmnopqr st uvwxy z 123456789" liste = decoupe (ch, 5) print liste print inverse (liste) 330. Gerard Swinnen : Apprendre a programmer avec Python Exercice 10.3 : # Rechercher l'indice d'un caractere dans une chaine def trouve (ch, car, deb=0) : "trouve 1 ' indice du caractere car dans la chaine ch" i = deb while i < len(ch) : if ch[i] == car: return i # le caractere est trouve -> on termine i = i + 1 return -1 # toute la chaine a ete scannee sans succes # Tests : print trouve ( "Coucou c'est moi", "z") print trouve ("Juliette & Romeo", "&") print trouve ("Cesar & Cleopatre", "r", 5) Exercice 10.6 : prefixes, suffixe = " JKLMNOP " , "ack" for p in prefixes : print p + suffixe Exercice 10.7 : def compteMots (ch) : "comptage du nombre if len(ch) ==0: return 0 nm = 1 for c in ch: if c == " " : nm = nm + 1 return nm # Test : print compteMots ("Les petit* ruisseaux font les grandes rivieres") Exercice 10.8 : def ma juscule (car) : "renvoie si car est une majuscule" if car in "ABCDEFGHI JKLMNOPQRSTUVWXYZ" : return 1 else : return 0 de mots dans la chaine ch" # la chaine comporte au moins un mot # il suffit de compter les espaces Gerard Swinnen : Apprendre a programmer avec Python 331. Exercice 10.10 : def chaineListe (ch) : "convertit la chaine ch en une liste de mots" liste, ct = [], "" # ct est une chaine temporaire for c in ch: if c == " " : liste . append (ct) # ajouter la ch. temporaire a la liste ct = "" # re-initialiser la ch. temporaire else : ct = ct + c if ct != "": liste . append (ct) # ne pas oublier le dernier mot return liste # Test : print chaineListe ( "Une hirondelle ne fait pas le printemps") print chaineListe ("" ) Exercice 10.11 (utilise les deux fonctions definies dans les exercices precedents) : txt = "Le nom de ce Monsieur est Alphonse" 1st = chaineListe (txt) # convertir la phrase en une liste de mots for mot in 1st: # analyser chacun des mots de la liste if ma juscule (mot [0] ) : # tester le premier caractere du mot print mot Exercice 10.12 : def majuscule (car) : "renvoie si car est une majuscule" if car >= "A" and car <= "Z" : return 1 else : return 0 def minuscule (car) : "renvoie si car est une minuscule" if car >= "a" and car <= "z": return 1 else : return 0 def alphab(car): "renvoie si car est un caractere alphabet ique" if ma juscule (car) or minuscule (car) : return 1 else : return 0 332. Gerard Swinnen : Apprendre a programmer avec Python Exercice 10.15 (utilise deux fonctions definies dans les exercices precedents) : def compteMa j (ch) : "comptage des mots debutant par une majuscule dans la chaine ch" c = 0 1st = chaineListe (ch) # convertir la phrase en une liste de mots for mot in 1st : # analyser chacun des mots de la liste if ma juscule (mot [0] ) : # tester le premier caractere du mot c = c +1 return c # Test : print compteMa j ("Les filles Tidgout se nomment Justine et Corinne") Exercice 10.16 (table des codes ASCII) : c = 32 # Premier code ASCII while c < 128 : # caracteres non accentues seulement print "Code", c, chr(c), " ", c = c + 1 Exercice 10.18 (Convertir majuscules -> minuscules et inversem*nt) : def convMa jMin (ch) : "echange les majuscules et les minuscules dans la chaine ch" nouvC = " " # chaine a construire for car in ch: code = ord(car) if car >= "A" and car <= "Z": code = code + 32 elif car >= "a" and car <= "z": code = code - 32 nouvC = nouvC + chr(code) return nouvC # Test : print convMa jMin ("Ferdinand-Charles de CAMARET") Exercice 10.20 (Comptage de voyelles) : def voyelle (car) : "teste si car est une voyelle" if car in "AEIOUYaeiouy" : return 1 else : return 0 def compteVoyelles (ch) : "compte les voyelles presentes dans la chaine ch" n = 0 for c in ch: if voyelle (c) : n = n + 1 return n # Test : print compteVoyelles ( "Monty Python Flying Circus") Gerard Swinnen : Apprendre a programmer avec Python 333. Exercice 10.22 : # Comptage du nombre de mots dans un texte fiSource = raw_input ( "Nom du fichier a traiter : ") fs = open (fiSource, 'r') n = 0 # variable compteur while 1 : ch = f s . readline ( ) if ch == "" : break # conversion de la chaine lue en une liste de mots : li = ch. split () # totalisation des mots : n = n + len(li) fs .close () print "Ce fichier texte contient un total de %s mots" % (n) Exercice 10.23 : # Conversion en majuscule du premier caractere de chaque ligne fiSource = raw_input ( "Nom du fichier a traiter : ") fiDest = raw_input ( "Nom du fichier destinataire : ") fs = open (fiSource, 'r') fd = open (fiDest, 'w') while 1 : ch = f s . readline ( ) if ch == " " : break if ch[0] >= "A" and ch[0] <= "Z": # le premier car. est une majuscule. On passe, pass else : # Reconstruction de la chaine: pc = ch[0] .upper () # Premier caractere convert! rc = ch[l:] # toute le reste de la chaine ch = pc + rc # fusion # variante utilisant une methode encore plus integree : # ch = ch. capitalize () # Transcription : fd. write (ch) fd. close () fs .close () Exercice 10.24 : # Fusion de lignes pour former des phrases fiSource = raw_input ( "Nom du fichier a traiter : ") fiDest = raw_input ( "Nom du fichier destinataire : ") fs = open (fiSource, 'r') fd = open (fiDest, 'w') 334. Gerard Swinnen : Apprendre a programmer avec Python # On lit d ' abord la premiere ligne : chl = f s . readline () # On lit ensuite les suivantes, en les fusionnant si necessaire : while 1 : ch2 = f s . readline ( ) if ch2 == "" : break # Si la chaine lue commence par une majuscule, on transcrit # la precedente dans le fichier destinataire, et on la # remplace par celle que 1 ' on vient de lire : if ch2[0] >= "A" and ch2[0] <= "Z" : fd. write (chl) chl = ch2 # Sinon, on la fusionne avec la precedente : else : chl = chl [ :-l] + " " + ch2 # (veiller a enlever de chl le caractere de fin de ligne) fd. write (chl) # ne pas oublier de transcrire la derniere ! fd. close () f s . close () Exercice 10.25 (caracteristiques de spheres) : # Le fichier de depart est un fichier dont chaque ligne contient # un nombre reel (encode sous la forme d'une chaine de caracteres) from math import pi def caractSphere (d) : "renvoie les caracteristiques d'une sphere de diametre d" d = f loat (d) # conversion de 1' argument (=chaine) en reel r = d/2 # rayon ss = pi*r**2 # surface de section se = 4*pi*r**2 # surface exterieure v = 4./3*pi*r**3 # volume (! la le division doit etre reelle !) # Le marqueur de conversion %8.2f utilise ci-dessous formate le nombre # affiche de maniere a occuper 8 caracteres au total, en arrondissant # de maniere a conserver deux chiffres apres la virgule : ch = "Diam. %6.2f cm Section = %8.2f cm 2 " % (d, ss) ch = ch +"Surf. = %8.2f cm 2 . Vol. = %9.2f cm 3 " % (se, v) return ch fiSource = raw_input ( "Nom du fichier a traiter : ") fiDest = raw_input ( "Nom du fichier destinataire : ") fs = open (fiSource, 'r') fd = open (fiDest, 'w') while 1 : diam = f s . readline ( ) if diam == "" or diam == "\n": break fd. write (caractSphere (diam) + "\n") # enregistrement fd. close () f s . close () Gerard Swinnen : Apprendre a programmer avec Python 335. Exercice 10.26 : # Mise en forme de donnees numeriques # Le fichier traite est un fichier dont chaque ligne contient un nombre # reel (sans exposants et encode sous la forme d'une chaine de caracteres) def arrondir (reel) : "representation arrondie a . 0 ou .5 d'un nombre reel" ent = int(reel) # partie entiere du nombre fra = reel - ent # partie fractionnaire if fra < .25 : fra = 0 elif fra < .75 : fra = .5 else : fra = 1 return ent + fra fiSource = raw_input ( "Nom du fichier a traiter : ") fiDest = raw_input ( "Nom du fichier destinataire : ") fs = open (fiSource, 'r') fd = open (fiDest, 'w') while 1 : ligne = f s . readline () if ligne == "" or ligne == "\n": break n = arrondir (float (ligne) ) # conversion en , puis arrondi fd. write (str (n) + "\n") # enregistrement fd. close () fs .close () Exercice 10.29 : # Affichage de tables de multiplication nt = [2, 3, 5, 7, 9, 11, 13, 17, 19] def tableMulti (m, n) : "renvoie n termes de la table de multiplication par m" ch for i in range (n) : v = m * (i+1) # calcul d'un des termes ch = ch + "%4d" % (v) # formatage a 4 caracteres return ch for a in nt : print tableMulti (a, 15) # 15 premiers termes seulement Exercice 10.30 (simple parcours d'une liste) : 1st = ['Jean-Michel', 'Marc', 'Vanessa', 'Anne', 'Maximilien ' , ' Alexandre-Benoit ' , 'Louise'] for e in 1st : print "%s : %s caracteres" % (e, len(e)) 336. Gerard Swinnen : Apprendre a programmer avec Python Exercice 10.31 : # Elimination de doublons 1st = [9, 12, 40, 5, 12, 3, 27, 5, 9, 3, 8, 22, 40, 3, 2, 4, 6, 25] lst2 = [] for el in 1st : if el not in lst2 : lst2 . append (el) lst2 . sort () print lst2 Exercice 10.33 (afficher tous les jours d'une annee) : ## Cette variante utilise une liste de listes ## ## (que l'on pourrait aisem*nt remplacer par deux listes distinctes) # La liste ci-dessous contient deux elements qui sont eux-memes des listes . # 1' element 0 contient les nombres de jours de chaque mois, tandis que # 1' element 1 contient les noms des douze mois : mois = [[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31], ['Janvier', 'Fevrier', 'Mars', 'Avril', 'Mai', ' Juin ' , 'Juillet', 'Aout', 'Septembre', 'Octobre', 'Novembre', 'Decembre']] jour = [ ' Dimanche ' , ' Lundi ' , ' Mardi ' , ' Mercredi ' , ' Jeudi ' , ' Vendredi ' , ' Samedi ' ] ja, jm, js, m = 0, 0, 0, 0 while ja <365: ja, jm = ja +1, jm +1 # ja = jour dans 1' annee, jm = jour dans le mois js = (ja +3) % 7 # js = jour de la semaine. Le decalage ajoute # permet de choisir le jour de depart if jm > mois[0] [m] : # element m de 1' element 0 de la liste jm, m = 1, m+1 print jourfjs], jm, mois[l] [m] # element m de l'element 1 de la liste Exercice 10.36 : # Insertion de nouveaux elements dans une liste existante tl = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] t2 = [ ' Janvier ' , ' Fevrier ' , ' Mars ' , ' Avril ' , ' Mai ' , ' Juin ' , ' Juillet ' , ' Aout ' , ' Septembre ' , ' Octobre ' , ' Novembre ' , ' Decembre ' ] c, d = 1, 0 while d < 12 : t2[c:c] = [tl [d] ] # ! l'element insere doit etre une liste c, d = c+2, d+1 Gerard Swinnen : Apprendre a programmer avec Python 337. Exercice 10.40 : # Crible d ' Eratosthene pour rechercher les nombres premiers de 1 a 999 # Creer une liste de 1000 elements 1 (leurs indices vont de 0 a 999) : 1st = [1]*1000 # Parcourir la liste a partir de 1 ' element d'indice 2: for i in range (2, 1000) : # Mettre a zero les elements suivants dans la liste, # dont les indices sont des multiples de i : for j in range(i*2, 1000, i) : lst[j] = 0 # Afficher les indices des elements restes a 1 (on ignore 1' element 0) : for i in range (1, 1000) : if 1st [i] : print i, Exercice 10.43 (Test du generateur de nombres aleatoires, page 141) : from random import random # tire au hasard un reel entre 0 et 1 n = raw_input ( "Nombre de valeurs a tirer au hasard (defaut = 1000) : ") if n == "" : nVal =1000 else : nVal = int(n) n = raw_input ( "Nombre de fractions dans l'intervalle 0-1 (entre 2 et " + str(nVal/10) + ", defaut =5) : ") if n == "" : nFra =5 else : nFra = int (n) if nFra < 2 : nFra =2 elif nFra > nVal/10: nFra = nVal/10 print "Tirage au sort des", nVal, "valeurs ..." listVal = [0]*nVal # creer une liste de zeros for i in range (nVal) : # puis modifier chaque element listVal[i] = random () print "Comptage des valeurs dans chacune des", nFra, "fractions ..." listCompt = [0]*nFra # creer une liste de compteurs # parcourir la liste des valeurs : for valeur in listVal : # trouver 1 ' index de la fraction qui contient la valeur : index = int (valeur*nFra) # incrementer le compteur correspondant : listCompt [index] = listCompt [index] +1 # afficher 1 ' etat des compteurs : for compt in listCompt : print compt , 338. Gerard Swinnen : Apprendre a programmer avec Python Exercice 10.44 : tirage de cartes from random import randrange couleurs = ['Pique', ' Tref le ' , 'Carreau', 'Coeur'] valeurs = [2, 3, 4, 5, 6, 7, 8, 9, 10, 'valet', 'dame', ' roi ' , 'as'] # Construction de la liste des 52 cartes : carte = [ ] for coul in couleurs : for val in valeurs : carte. append ("%s de %s" % (str(val), coul)) # Tirage au hasard : while 1 : k = raw_input ( "Frappez pour tirer une carte, pour terminer ") if k =="" : break r = randrange (52) print carte [r] Exercice 10.45 : Creation et consultation d'un dictionnaire def consultation () : while 1 : nom = raw_input ( "Entrez le nom (ou pour terminer) : ") if nom == "" : break if dico . has_key (nom) : # le nom est-il repertorie ? item = dico [nom] # consultaion proprement dite age, taille = item[0] , itemfl] print "Nom : %s - age : %s ans - taille : %s m."\ % (nom, age, taille) else : print "*** nom inconnu ! ***" def remplissage () : while 1 : nom = raw_input ( "Entrez le nom (ou pour terminer) : ") if nom == " " : break age = int (raw_input ("Entrez l'age (nombre entier !) : ")) taille = float (raw_input ( "Entrez la taille (en metres) : ")) dico [nom] = (age, taille) dico ={} while 1 : choix = raw_input ( "Choisissez : (R) emplir - (C)onsulter - (T)erminer : ") if choix . upper ( ) == 'T': break elif choix . upper ( ) == 'R' : remplissage () elif choix . upper ( ) == 'C: consultation ( ) Gerard Swinnen : Apprendre a programmer avec Python 339. Exercice 10.46 : echange des cles et des valeurs dans un dictionnaire def inverse (dico) : "Construction d'un nouveau dico, pas a pas" dic_inv = { } for cle in dico: item = dico [cle] dic_inv [item] = cle return dic_inv # programme test : dico = { ' Computer ' : ' Ordinateur ' , ' Mouse ' : ' Souris ' , ' Keyboard ' : ' Clavier ' , 'Hard disk' : 'Disque dur' , ' Screen ' : ' Ecran ' } print dico print inverse (dico) Exercice 10.47 : histogramme nFich = raw_input ( ' Nom du f ichier : ' ) fi = open (nFich, 'r') texte = fi.read() # conversion du f ichier en une chaine de caracteres fi. close () print texte dico ={} for c in texte: c = cupper () # conversion de toutes les lettres en majuscules dico[c] = dico. get (c, 0) +1 liste = dico. items () liste . sort () print liste Exercice 10.48 : nFich = raw_input ( ' Nom du f ichier a traiter : ' ) fi = open (nFich, 'r') texte = fi.read() fi. close () # afin de pouvoir aisem*nt separer les mots du texte, on commence # par convertir tous les caracteres non-alphabetiques en espaces : alpha = "abcdefghi jklmnopqrstuvwxyzeeaucaeiouaeiou" lettres = ' ' # nouvelle chaine a construire for c in texte: c = c. lower () # conversion de chaque caractere en minuscule if c in alpha: lettres = lettres + c else : lettres = lettres + ' ' 340. Gerard Swinnen : Apprendre a programmer avec Python # conversion de la chaine resultante en une liste de mots mots = lettres . split () # construction de 1 ' histogramme : dico ={} for m in mots : dico[m] = dico. get (m, 0) +1 liste = dico. items () # tri de la liste resultante : liste . sort () # affichage en clair : for item in liste : print item[0], item[l] Exercice 10.49 : # encodage d'un texte dans un dictionnaire nFich = raw_input ( ' Nom du f ichier a traiter : ' ) fi = open (nFich, 'r') texte = f i . read ( ) f i . close () # On considere que les mots sont des suites de caracteres faisant partie # de la chaine ci-dessous. Tous les autres sont des separateurs : alpha = "abcdefghi jklmnopqrstuvwxyzeeaucaeiouaeiou" # construction du dictionnaire : dico ={} # parcours de tous les caracteres du texte : i =0 # indice du caractere en cours de lecture mot ="" # variable de travail : mot en cours de lecture for c in texte: c = c. lower () # conversion de chaque caractere en minuscule if c in alpha: # car. alphab. => on est a 1 ' interieur d'un mot mot = mot + c else : # car . non-alphabetique => fin de mot if mot != "": # afin d'ignorer les car. non-alphab. successifs # pour chaque mot, on construit une liste d' indices : if dico. has_key (mot) : # mot deja repertorie : dico [mot] . append (i) # ajout d'un indice a la liste else: # mot rencontre pour la le fois : dico [mot] =[i] # creation de la liste d' indices mot ="" # preparer la lecture du mot suivant i = i+1 # indice du caractere suivant # Affichage du dictionnaire, en clair : for clef, valeur in dico. items () : print clef, ":", valeur Gerard Swinnen : Apprendre a programmer avec Python 341. Exercice 10.50 : Sauvegarde d'un dictionnaire (complement de l'ex. 10.45). def enregistrement () : fich = raw_input ( "Entrez le nom du fichier de sauvegarde : ") ofi = open (fich, "w") # parcours du dictionnaire entier, converti au prealable en une liste : for cle, valeur in dico . items () : # utilisation du f ormatage des chaines pour creer 1 ' enregistrement : ofi.write("%s@%s#%s\n" % (cle, valeur[0], valeur[l])) ofi . close () def lectureFichier () : fich = raw_input ( "Entrez le nom du fichier de sauvegarde : ") try: ofi = open (fich, "r") except : print "*** fichier inexistant ***" return while 1 : ligne = of i . readline () if ligne == ' ' : break enreg = ligne . split ( "@") cle = enreg [0] valeur = enreg [1] [:— 1] data = valeur. split ("#") age, taille = int(data[0]) dico [cle] = (age, taille) ofi . close () # detection de la fin de fichier # restitution d'une liste [cle, valeur] # elimination # restitution float (data[l] ) # reconstitution du caractere de fin de ligne d'une liste [age, taille] du dictionnaire Ces deux fonctions peuvent etre appelees respectivement a la fin et au debut du programme principal, comme dans l'exemple ci-dessous : dico ={} lectureFichier ( ) while 1 : choix = raw_input ( "Choisissez : (R) emplir - (C)onsulter - (T)erminer : ") if choix . upper ( ) == 'T': break elif choix . upper ( ) == 'R': remplissage () elif choix . upper ( ) == 'C: consultation ( ) enregistrement () Exercice 10.51 : Controle du flux d'execution a l'aide d'un dictionnaire Cet exercice complete le precedent. On ajoute encore deux petites fonctions, et on reecrit le corps principal du programme pour diriger le flux d'execution en se servant d'un dictionnaire : def sortie () : print "*** Job termine ***" return 1 # afin de provoquer la sortie de la boucle 342. Gerard Swinnen : Apprendre a programmer avec Python def autre () : print "Veuillez f rapper R, A, C, S ou T, svp." dico ={} fonc ={ "R" : lectureFichier, "A" : remplissage, "C" : consultation, "S" : enregistrement, "T" :sortie} while 1 : choix = raw_input ( "Choisissez :\n" +\ "(R)ecuperer un dictionnaire preexistant sauvegarde dans un fichier\n" +\ " (A) jouter des donnees au dictionnaire courant\n" +\ "(C)onsulter le dictionnaire courant\n" +\ " (S) auvegarder le dictionnaire courant dans un fichier\n" +\ " (T) erminer : ") # 1 ' instruction ci-dessous appelle une fonction differente pour # chaque choix, par 1 ' intermediaire du dictionnaire : if fonc . get (choix, autre) () : break # Rem : toutes les fonctions appelees ici renvoient par defaut, # sauf la fonction sortie () qui renvoie 1 => sortie de la boucle Exercice 12.1 : class Domino: def init (self, pa, pb) : self .pa, self.pb = pa, pb def af fiche_points (self) : print "face A :", self .pa, print "face B :", self.pb def valeur (self ) : return self. pa + self.pb # Programme de test : dl = Domino (2, 6) d2 = Domino (4,3) dl . af f iche_points ( ) d2 . af f iche_points ( ) print "total des points :", dl. valeur () + d2. valeur () liste_dominos = [ ] for i in range (7): liste_dominos . append (Domino (6, i) ) vt =0 for i in range (7): liste_dominos [i] . af f iche_points () vt = vt + liste_dominos [i] .valeur () print "valeur totale des points", vt Gerard Swinnen : Apprendre a programmer avec Python 343. Exercice 12.3 : class Voiture : def init (self, marque = 'Ford', couleur = 'rouge'): self.couleur = couleur self .marque = marque self.pilote = 'personne' self. vitesse = 0 def accelerer (self , taux, duree) : if self.pilote == 'personne ' : print "Cette voiture n'a pas de conducteur !" else : self. vitesse = self. vitesse + taux * duree def choix_conducteur (self , nom) : self.pilote = nom def af f iche_tout (self ) : print "%s %s pilotee par %s, vitesse = %s m/s" % \ (self .marque, self.couleur, self.pilote, self .vitesse) al = Voiture ( ' Peugeot ' , ' bleue ' ) a2 = Voiture (couleur = 'verte') a3 = Voiture ( 'Mercedes ' ) al . choix_conducteur ( ' Romeo ' ) a2 . choix_conducteur ( ' Juliette ' ) a2 .accelerer (1 . 8, 12) a3 .accelerer (1 . 9, 11) a2 . af f iche_tout ( ) a3 . af f iche_tout ( ) Exercice 12.4 : class Satellite: def init (self, nom, masse =100, vitesse =0) : self. nom, self. masse, self. vitesse = nom, masse, vitesse def impulsion (self , force, duree): self. vitesse = self. vitesse + force * duree / self .masse def energie (self ) : return self .masse * self . vitesse**2 / 2 def af f iche_vitesse (self ) : print "Vitesse du satellite %s = %s m/s" \ % (self. nom, self . vitesse) # Programme de test : si = Satellite (' Zoe ' , masse =250, vitesse =10) si . impulsion (500, 15) si . af f iche_vitesse ( ) print s 1 . energie ( ) si . impulsion (500 , 15) si . af f iche_vitesse ( ) print s 1 . energie ( ) 344. Gerard Swinnen : Apprendre a programmer avec Python Exercices 12.5-12.6 (classes de cylindres et de cones) : # Classes derivees - polymorphisms class Cercle: def init (self, rayon) : self. rayon = rayon def surface (self ) : return 3.1416 * self . rayon**2 class Cylindre (Cercle) : def init (self, rayon, hauteur) : Cercle. init (self, rayon) self. hauteur = hauteur def volume (self ) : return self . surface ( ) *self . hauteur # la methode surface () est heritee de la classe parente class Cone (Cylindre) : def init (self, rayon, hauteur) : Cylindre. init (self, rayon, hauteur) def volume (self ) : return Cylindre . volume (self) /3 # cette nouvelle methode volume () remplace celle que # l'on a heritee de la classe parente (exemple de polymorphisme) cyl = Cylindre (5, 7) print cyl . surface ( ) print cyl . volume ( ) co = Cone (5, 7) print co. surface () print co . volume ( ) Gerard Swinnen : Apprendre a programmer avec Python 345. Exercice 12.7 : # Tirage de cartes from random import randrange class JeuDeCartes : """Jeu de cartes""" # attributs de classe (communs a toutes les instances) : couleur = ('Pique', ' Tref le ' , ' Carreau ' , 'Coeur') valeur = (0, 0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 'valet', 'dame', ' roi ' , 'as') def init (self) : "Construction de la liste des 52 cartes" self. carte =[] for coul in range (4) : for val in range (13) : self .carte. append ( (val +2, coul)) # la valeur commence a 2 def nom_carte (self , c) : "Renvoi du nom de la carte c, en clair" return "%s de %s" % (self .valeur [c [0] ] , self . couleur [c [1] ] ) def battre(self) : "Melange des cartes" t = len (self . carte) # nombre de cartes restantes # pour melanger, on procede a un nombre d'echanges equivalent : for i in range (t) : # tirage au hasard de 2 emplacements dans la liste : hi, h2 = randrange (t) , randrange (t) # echange des cartes situees a ces emplacements : self . carte [hi] , self . carte [h2] = self .carte [h2] , self . carte [hi] def tirer(self): "Tirage de la premiere carte de t = len (self . carte) if t >0: carte = self .carte [0] del (self .carte [0] ) return carte else : return None la pile" # verifier qu'il reste des cartes # choisir la premiere carte du jeu # la retirer du jeu # en renvoyer copie au prog, appelant # facultatif ### Programme test : if name == ' main ' : jeu = JeuDeCartes ( ) jeu.battre () for n in range (53): c = jeu.tirer() if c == None: print ' Termine ! ' else : print jeu . nom_carte (c) # instanciation d'un objet # melange des cartes # tirage des 52 cartes : # il ne reste aucune carte # dans la liste # valeur et couleur de la carte 346. Gerard Swinnen : Apprendre a programmer avec Python Exercice 12.8 : (On supposera que l'exercice precedent a ete sauvegarde sous le nom cartes.py) # Bataille de de cartes from cartes import JeuDeCartes jeuA = JeuDeCartes () # instanciation du premier jeu jeuB = JeuDeCartes () # instanciation du second jeu jeuA.battre () # melange de chacun jeuB.battre () pA, pB = 0, 0 # compteurs de points des joueurs A et B # tirer 52 fois une carte de chaque jeu : for n in range (52) : cA, cB = jeuA. tirer () , jeuB. tirer () vA, vB = cA[0] , cB[0] # valeurs de ces cartes if vA > vB: pA += 1 elif vB > vA: pB +=1 # (rien ne se passe si vA = vB) # affichage des points successifs et des cartes tirees : print "%s * %s ==> %s * %s" % ( jeuA. nom_carte (cA) , jeuB . nom_carte (cB) , pA, pB) print "le joueur A obtient %s points, le joueur B en obtient %s." % (pA, pB) Exercice 13.6 : from Tkinter import * def cercle (can, x, y, r, coul ='white'): "dessin d'un cercle de rayon en dans le canevas " can . create_oval (x-r, y-r, x+r, y+r, fill =coul) class Application (Tk) : def init (self) : Tk. init (self) # constructeur de la classe parente self. can =Canvas (self , width =475, height =130, bg ="white") self . can. pack (side =TOP, padx =5, pady =5) Button(self, text ="Train", command =self. dessine) .pack (side =LEFT) Button(self, text ="Hello", command =self. coucou) .pack (side =LEFT) Button(self, text ="Ecl34", command =self . eclai34) .pack (side =LEFT) def dessine (self ) : "instanciation de 4 wagons dans le canevas" self.wl = Wagon (self. can, 10, 30) self.w2 = Wagon (self . can, 130, 30, self.w3 = Wagon (self. can, 250, 30, self.w4 = Wagon (self . can, 370, 30, ' dark green ' ) 'maroon ' ) 'purple ' ) def coucou (self ) : "apparition de personnages dans certaines fenetres' self.wl .perso (3) self . w3 .per so (1) self . w3 .perso (2) self .w4 .perso (1) # ler wagon, 3e fenetre # 3e wagon, le fenetre # 3e wagon, 2e fenetre # 4e wagon, le fenetre def eclai34 (self) Gerard Swinnen : Apprendre a programmer avec Python 347. "allumage de l'eclairage dans les wagons 3 & 4" self . w3 . allumer ( ) self . w4 . allumer ( ) class Wagon: def init (self, canev, x, y, coul ='navy'): "dessin d'un petit wagon en dans le canevas " # memorisation des parametres dans des variables d' instance : self. canev, self.x, self.y = canev, x, y # rectangle de base : 95x60 pixels : canev. create_rect angle (x, y, x+95, y+60, fill =coul) # 3 fenetres de 25x40 pixels, ecartees de 5 pixels : self. fen =[] # pour memoriser les ref. des fenetres for xf in range (x +5, x +90, 30) : self . fen. append (canev. create_rectangle (xf , y+5, xf+25, y+40, fill = 'black')) # 2 roues de rayon egal a 12 pixels : cercle (canev, x+18, y+73, 12, 'gray') cercle (canev, x+77, y+73, 12, 'gray') def perso(self, fen): "apparition d'un petit personnage a la fenetre " # calcul des coordonnees du centre de chaque fenetre : xf = self.x + fen*30 -12 yf = self.y + 25 cercle (self . canev, xf, yf, 10, "pink") # visage cercle (self .canev, xf-5, yf-3, 2) # oeil gauche cercle (self .canev, xf+5, yf-3, 2) # oeil droit cercle (self .canev, xf, yf+5, 3) # bouche def allumer (self ) : "declencher l'eclairage interne du wagon" for f in self. fen: self . canev . itemconf igure (f, fill =' yellow') Application ( ) . app . mainloop ( ) Exercice 13.21 : # Dictionnaire de couleurs Norn de la couleur : ocre ocre Existe deja ? |#FFCC00 Test Ajouter la couleur au dictionnaire Enregistrer le dictionnaire Restaurer le dictionnaire 348. Gerard Swinnen : Apprendre a programmer avec Python from Tkinter import * # Module donnant acces aux boites de dialogue standard pour # la recherche de fichiers sur disque : from tkFileDialog import asksaveasf ile, askopenfile class Application (Frame) : ' ' 'Fenetre d' application' ' ' def init (self) : Frame . init (self) self .master .title ("Creation d'un dictionnaire de couleurs") self.dico ={} # creation du dictionnaire # Les widgets sont regroupes dans deux cadres (Frames) : frSup =Frame(self) # cadre superieur contenant 6 widgets Label (frSup, text ="Nom de la couleur :", width =20) .grid(row =1, column =1) self.enNom =Entry (f rSup, width =25) # champ d' entree pour self . enNom. grid (row =1, column =2) # le nom de la couleur Button (frSup, text ="Existe deja ?", width =12, command =self . chercheCoul) . grid (row =1, column =3) Label (frSup, text ="Code hexa. corresp. :", width =20) .grid(row =2, column =1) self. enCode =Entry (f rSup, width =25) # champ d'entree pour self . enCode . grid (row =2, column =2) # le code hexa. Button (frSup, text ="Test", width =12, command =self . testeCoul) .grid (row =2, column =3) f r Sup .pack (padx =5, pady =5) frlnf =Frame (self) # cadre inferieur contenant le reste self. test = Label (frlnf, bg ="white", width =45, # zone de test height =7, relief = SUNKEN) self .test .pack (pady =5) Button (frlnf , text ="Ajouter la couleur au dictionnaire", command =self . a jouteCoul) .pack ( ) Button (frlnf , text ="Enregistrer le dictionnaire", width =25, command =self . enregistre) .pack (side = LEFT, pady =5) Button (frlnf , text ="Restaurer le dictionnaire", width =25, command =self . restaure) .pack (side =RIGHT, pady =5) frlnf .pack (padx =5, pady =5) self .pack () def a jouteCoul (self ) : "ajouter la couleur presente au dictionnaire" if self .testeCoul () ==0: # une couleur a-t-elle ete definie ? return nom = self . enNom. get ( ) if len (nom) >1 : # refuser les noms trop petit* self . dico [nom] =self .cHexa else : self . test . config (text ="%s : nom incorrect" % nom, bg ='white') def chercheCoul (self ) : "rechercher une couleur deja inscrite au dictionnaire" nom = self . enNom. get ( ) if self . dico . has_key (nom) : self . test . config (bg =self .dico [nom] , text ="") else : self . test . config (text ="%s : couleur inconnue" % nom, bg ='white') def testeCoul (self ) : Gerard Swinnen : Apprendre a programmer avec Python 349. "verifier la validite d'un code hexa. - afficher la couleur corresp." try: self.cHexa =self .enCode.get () self .test . config (bg =self.cHexa, text ="") return 1 except : self .test . config (text ="Codage de couleur incorrect", bg ='white') return 0 def enregistre (self ) : "enregistrer le dictionnaire dans un fichier texte" # Cette methode utilise une boite de dialogue standard pour la # selection d'un fichier sur disque. Tkinter fournit toute une serie # de fonctions associees a ces boites, dans le module tkFileDialog. # La fonction ci-dessous renvoie un ob jet-f ichier ouvert en ecriture : ofi =asksaveasfile (filetypes=[ ("Texte", " .txt") , ("Tous", "*")]) for clef, valeur in self . dico . items () : ofi. write ("%s %s\n" % (clef, valeur)) ofi . close () def restaure (self ) : "restaurer le dictionnaire a partir d'un fichier de memorisation" # La fonction ci-dessous renvoie un ob jet-f ichier ouvert en lecture : ofi =askopenfile(filetypes=[ ("Texte", " .txt") , ("Tous", "*") ] ) lignes = of i . readlines () for li in lignes : cv = li. split () # extraction de la cle et la valeur corresp. self .dico [cv[0] ] = cv[l] ofi . close () if name == ' main ' : Application ( ) .mainloopO Exercice 13.22 (variante 3) : from Tkinter import * from random import randrange from math import sin, cos, pi class FaceDom: def init (self, can, val, pos, taille =70) : self. can =can x, y, c = pos[0], pos[l], taille/2 self, carre = can . create_rectangle (x -c, y-c, x+c, y+c, fill =' ivory', width =2) d = taille/3 # disposition des points sur la face, pour chacun des 6 cas : self.pDispo = [((0,0),), ((-d,d), (d,-d)), ((-d,-d), (0,0), (d,d)), ( (-d, -d) , (-d, d) , (d, -d) , (d, d) ) , ( (-d, -d) , (-d, d) , (d, -d) , (d, d) , (0, 0) ) , ( (-d, -d) , (-d, d) , (d, -d) , (d, d) , (d, 0) , (-d, 0) ) ] self.x, self.y, self. dim = x, y, taille/15 self.pList =[] # liste contenant les points de cette face self . tracer_points (val) def tracer_points (self, val) : # creer les dessins de points correspondant a la valeur val : disp = self .pDispo [val -1] 350. Gerard Swinnen : Apprendre a programmer avec Python for p in disp: self . cercle (self . x +p[0], self.y +p[l], self. dim, 'red') self.val = val def cercle (self, x, y, r, coul) : self .pList . append (self . can . create_oval (x-r, y-r, x+r, y+r, fill=coul) ) def ef facer (self , flag =0): for p in self .pList : self . can . delete (p) if flag: self .can. delete (self .carre) class Pro jet (Frame) : def init (self, larg, haut) : Frame . init (self) self. larg, self. haut = larg, haut self. can = Canvas (self, bg='dark green', width =larg, height =haut) self .can. pack (padx =5, pady =5) # liste des boutons a installer, avec leur gestionnaire : bList = [("A", self.boutA), ("B", self.boutB), ("C", self.boutC), ("Quitter", self . boutQuit ) ] bList . reverse () # inverser 1 ' ordre de la liste for b in bList : Button(self, text =b[0], command =b [1] ) .pack (side =RIGHT, padx=3) self .pack () self. des =[] # liste qui contiendra les faces de des self . actu =0 # ref . du de actuellement selectionne def boutA(self) : if len (self .des) : return # car les dessins existent deja ! a, da = 0, 2*pi/13 for i in range (13): cx, cy = self.larg/2, self.haut/2 x = cx + cx*0 . 75*sin (a) # pour disposer en cercle, y = cy + cy*0 . 75*cos (a) # on utilise la trigono ! self .des. append (FaceDom (self .can, randrange (1, 7) , (x,y), 65)) a += da def boutB(self) : # incrementer la valeur du de selectionne. Passer au suivant : v = self .des [self .actu] .val v = v % 6 v += 1 self . des [self . actu] .effacer() self . des [ self . actu] . tracer_point s ( v) self. actu += 1 self. actu = self. actu % 13 def boutC(self) : for i in range (len (self . des) ) : self. des [i] .effacer(l) self. des =[] self. actu =0 def boutQuit (self ) : self . master . destroy ( ) Projet(600, 600) .mainloop () Gerard Swinnen : Apprendre a programmer avec Python 351. Exercice 16.1 (Creation de la base de donnees "musique") : import gadfly connex = gadf ly . gadfly ( ) connex . startup ( "musique" , "E : /Python/essais/gadf ly" ) cur = connex. cursor () requete = "create table compositeurs (comp varchar, a_naiss integer, \ a_mort integer) " cur . execute (requete) requete = "create table oeuvres (comp varchar, titre varchar, \ duree integer, interpr varchar) " cur . execute (requete) print "Entree des enregistrements , table des compositeurs :" while 1 : nm = raw_input ( "Nom du compositeur ( pour terminer) : ") if nm == ' ' : break an = raw_input ( "Annee de naissance : ") am = raw_input ( "Annee de mort : " ) requete ="insert into compositeurs (comp, a_naiss, a_mort) values \ ('%s', %s, %s)" % (nm, an, am) cur . execute (requete) # Affichage des donnees entrees, pour verification : cur .execute ("select * from compositeurs") print cur . pp ( ) print "Entree des enregistrements, table des oeuvres musicales :" while 1 : nom = raw_input ( "Nom du compositeur ( pour terminer) : ") if nom == ' ' : break tit = raw_input ( "Titre de l'oeuvre : ") dur = raw_input (" duree (minutes) : ") int = raw_input ( " interprete principal : " ) requete =" insert into oeuvres (comp, titre, duree, interpr) values \ ('%s', '%s', %s, '%s')" % (nom, tit, dur, int) cur . execute (requete) # Affichage des donnees entrees, pour verification : cur .execute ("select * from oeuvres") print cur . pp ( ) connex . commit ( ) Exercice 18.2 : ##################################### # Bombardement d'une cible mobile # # (C) G. Swinnen - Avril 2004 - GPL # ##################################### from Tkinter import * from math import sin, cos, pi from random import randrange from threading import Thread class Canon: """Petit canon graphique""" def init (self, boss, num, x, y, sens) : self .boss = boss # reference du canevas 352. Gerard Swinnen : Apprendre a programmer avec Python self.num = num # n° du canon dans la liste self.xl, self.yl = x, y # axe de rotation du canon self. sens = sens # sens de tir (-1: gauche, +l:droite) self.lbu =30 # longueur de la buse # dessiner la buse du canon (horizontale) : self.x2, self.y2 = x + self.lbu * sens, y self. buse = boss . create_line (self .xl, self.yl, self.x2, self.y2, width =10) # dessiner le corps du canon (cercle de couleur) : self.rc =15 # rayon du cercle self. corps = boss . create_oval (x -self.rc, y -self.rc, x +self.rc, y +self.rc, fill = 'black') # pre-dessiner un obus (au depart c'est un simple point) : self.obus = boss . create_oval (x, y, x, y, fill='red') self.anim = 0 # retrouver la largeur et la hauteur du canevas : self.xMax = int (boss . cget ( 'width ') ) self.yMax = int (boss . cget ( 'height ') ) def orienter (self , angle): "regler la hausse du canon" # rem : le parametre est recu en tant que chaine. # il faut done le traduire en reel, puis le convertir en radians : self. angle = float (angle) *2*pi/360 self.x2 = self.xl + self.lbu * cos (self . angle) * self. sens self.y2 = self.yl - self.lbu * sin (self . angle) self .boss . coords (self .buse, self.xl, self.yl, self.x2, self.y2) def feu (self ) : "declencher le tir d'un obus" # reference de 1 ' ob jet cible : self.cible = self .boss .master . cible if self.anim ==0: self.anim =1 # position de depart de l'obus (c'est la bouche du canon) : self.xo, self.yo = self.x2, self.y2 v = 20 # vitesse initiale # composantes verticale et horizontale de cette vitesse : self.vy = -v *sin (self . angle) self.vx = v *cos (self . angle) *self.sens self . animer_obus ( ) def animer_obus (self ) : "animer l'obus (trajectoire balistique) " # positionner l'obus, en re-def inissant ses coordonnees : self .boss . coords (self . obus, self.xo -3, self.yo -3, self.xo +3, self.yo +3) if self.anim >0 : # calculer la position suivante : self.xo += self.vx self.yo += self.vy self.vy += .5 self . test_obstacle () # a-t-on atteint un obstacle ? self .boss . after (1, self . animer_obus) else : # fin de 1 ' animation : self .boss . coords (self . obus, self.xl, self.yl, self.xl, self.yl) def test_obstacle (self ) : "evaluer si l'obus a atteint une cible ou les limites du jeu" if self.yo >self.yMax or self.xo <0 or self.xo >self.xMax: Gerard Swinnen : Apprendre a programmer avec Python 353. self.anim =0 return if self.yo > self.cible.y -3 and self.yo < self.cible.y +18 \ and self.xo > self.cible.x -3 and self.xo < self.cible.x +43: # dessiner 1' explosion de l'obus (cercle orange) : self.explo = self .boss . create_oval (self . xo -10, self.yo -10, self.xo +10, self.yo +10, fill =' orange', width =0) self .boss . after (150, self . fin_explosion) self.anim =0 def f in_explosion (self ) : "ef facer le cercle d' explosion - gerer le score" self .boss . delete (self . explo) # signaler le succes a la fenetre maitresse : self . boss . master . goal ( ) class Pupitre (Frame) : """Pupitre de pointage associe a un canon""" def init (self, boss, canon) : Frame. init (self, bd =3, relief =GROOVE) self. score =0 s =Scale(self, from_ =88, to =65, troughcolor = ' dark grey ' , command =canon . orienter) s. set (45) # angle initial de tir s. pack (side =LEFT) Label(self, text =' Hausse ') .pack (side =TOP, anchor =W, pady =5) Button (self, text ='Feu !', command =canon . f eu) . \ pack (side =BOTTOM, padx =5, pady =5) Label(self, text ="points") .pack () self .points =Label (self , text=' 0 ' , bg =' white') self . points . pack ( ) # positionner a gauche ou a droite suivant le sens du canon : gd = (LEFT, RIGHT) [canon. sens == -1] self .pack (padx =3, pady =5, side =gd) def attribuerPoint (self , p) : "incrementer ou decrementer le score" self. score += p self .points . config (text = ' %s ' % self. score) class Cible: """ob jet graphique servant de cible""" def init (self, can, x, y) : self. can = can # reference du canevas self.x, self.y = x, y self. cible = can . create_oval (x, y, x+40, y+15, fill =' purple') def deplacer (self , dx, dy) : "effectuer avec la cible un deplacement dx,dy" self .can. move (self .cible, dx, dy) self.x += dx self.y += dy return self.x, self.y class Thread_cible (Thread) : """ob jet thread gerant 1' animation de la cible""" def init (self, app, cible) : Thread. init (self) self. cible = cible # objet a deplacer 354. Gerard Swinnen : Apprendre a programmer avec Python self . app = app self.sx, self.sy = 6, 3 self.dt =300 # ref. de la fenetre d' application # increments d'espace et de # temps pour 1' animation (ms) def run (self) : "animation, tant que la fenetre d' application existe" x, y = self . cible .deplacer (self . sx, self.sy) if x > self. app. xm -50 or x < self. app. xm /5: self.sx = -self.sx if y < self. app. ym /2 or y > self. app. ym -20: self.sy = -self.sy if self. app != None: self . app. after (int (self . dt) , self . run) def stop (self ) : "fermer le thread si la fenetre d' application est refermee" self . app =None def accelere (self ) : "accelerer le mouvement" self.dt /= 1.5 class Application (Frame) : def init (self) : Frame . init (self) self .master .title (' «< Tir sur cible mobile »>') self .pack () self .xm, self .ym = 600, 500 self.jeu = Canvas (self, width =self.xm, height =self.ym, bg =' ivory', bd =3, relief =SUNKEN) self . jeu. pack (padx =4, pady =4, side =TOP) # Instanciation d'un canon et d'un pupitre de pointage : x, y = 30, self.ym -20 self. gun =Canon (self . jeu, 1, x, y, 1) self .pup =Pupitre (self , self. gun) # instanciation de la cible mobile : self. cible = Cible (self . jeu, self.xm/2, self.ym -25) # animation de la cible mobile, sur son propre thread : self.tc = Thread_cible (self , self. cible) self .tc. start () # arreter tous les threads lorsque 1 ' on ferme la fenetre : self .bind ( ' ' , self . fermer_threads) def goal (self) : "la cible a ete touchee" self . pup . att r ibuerPoint ( 1 ) self . tc . accelere ( ) def fermer_threads (self, evt) : "arreter le thread d' animation de la cible" self .tc. stop () if name == ' main ' : Application () .mainloopO Gerard Swinnen : Apprendre a programmer avec Python 355. 19.10 Annexes extraites de « How to think like a computer scientist » Suivant les termes de la GNU Free Documentation licence (voir p. 361), les annexes qui suivent doivent obligatoirement accompagner telles quelles toute distribution du texte original, que celui-ci ait ete modifie (traduit, par exemple) ou non. 19.10. 1 Contributor list by Jeffrey Elkner Perhaps the most exciting thing about a free content textbook is the possibility it creates for those using the book to collaborate in its development. I have been delighted by the many responses, suggestions, corrections, and words of encouragement I have received from people who have found this book to be useful, and who have taken the time to let me know about it. Unfortunately, as a busy high school teacher who is working on this project in my spare time (what little there is of it ;-), I have been neglectful in giving credit to those who have helped with the book. I always planned to add an "Acknowlegdements" sections upon completion of the first stable version of the book, but as time went on it became increasingly difficult to even track those who had contributed. Upon seeing the most recent version of Tony Kuphaldt's wonderful free text, "Lessons in Electric Circuits", I got the idea from him to create an ongoing "Contributor List" page which could be easily modified to include contributors as they come in. My only regret is that many earlier contributors might be left out. I will begin as soon as possible to go back through old emails to search out the many wonderful folks who have helped me in this endeavour. In the mean time, if you find yourself missing from this list, please accept my humble apologies and drop me an email atjeff@elkner.net to let me know about my oversight. And so, without further delay, here is a listing of the contributors: Lloyd Hugh Allen Lloyd sent in a correction to section 8.4. He can be reached at: lha2@columbia.edu Yvon Boulianne Yvon sent in a correction of a logical error in Chapter 5. She can be reached at: mystic@monuniverse.net Fred Bremmer Fred submitted a correction in section 2. 1. He can be reached at: Fred.Bremmer@ubc.cu Jonah Cohen Jonah wrote the Perl scripts to convert the LaTeX source for this book into beautiful HTML. His Web page isjonah.ticalc.org and his email is JonahCohen@aol.com Michael Conlon Michael sent in a grammer correction in Chapter 2 and an improvement in style in Chapter 1, and he initiated discussion on the technical aspects of interpreters. Michael can be reached at: michael.conlon@sru.edu Courtney Gleason Courtney and Katherine Smith created the first version of horsebet.py, which is used as the case study for the last chapters of the book. Courtney can be reached at: orionl558@aol.com Lee Harr Lee submitted corrections for sections 10.1 and 1 1.5. He can be reached at: missive@linuxfreemail.com James Kaylin James is a student using the text. He has submitted numerous corrections. James can be reached by email at: Jamarf@aol.com David Kershaw David fixed the broken catTwice function in section 3.10. He can be reached at: david_kershaw@merck.com Eddie Lam Eddie has sent in numerous corrections to Chapters 1, 2, and 3. He also fixed the Makefile so that it 356. Gerard Swinnen : Apprendre a programmer avec Python creates an index the first time it is run and helped us set up a versioning scheme. Eddie can be reached at: nautilus@yoyo.cc.monash.edu.au Man-Yong Lee Man-Yong sent in a correction to the example code in section 2.4. He can be reaced at: yong@linuxkorea.co.kr David Mayo While he didn't mean to hit us over the head with it, David Mayo pointed out that the word "unconsciously" in chapter 1 needed to be changed to "subconsciously". David can be reached at: bdbear44@netscape.net Chris McAIoon Chris sent in several corrections to sections 3.9 and 3.10. He can be reached at: cmcaloon@ou.edu Matthew J. Moelter Matthew has been a long-time contributor who sent in numerous corrections and suggestions to the book. He can be reached at: mmoelter@calpoly.edu Simon Dicon Montford Simon reported a missing function definition and several typos in Chapter 3. He also found errors in the increment function in Chapter 13. He can be reached at: dicon@bigfoot.com John Ouzts John sent in a correction to the "return value" definition in Chapter 3. He can be reached at: jouzts@bigfoot.com Kevin Parks Kevin sent in valuable comments and suggestions as to how to improve the distribution of the book. He can be reached at: cpsoct@lycos.com David Pool David sent in a typo in the glossary of chapter 1, as well as kind words of encouragement. He can be reached at: pooldavid@hotmail.com Michael Schmitt Michael sent in a correction to the chapter on files and exceptions. He can be reached at: ipv6_128@yahoo.com Paul Sleigh Paul found an error in Chapter 7 and a bug in Jonah Cohen's Perl script that generates HTML from LaTeX. He can be reached at: bat@atdot.dotat.org Christopher Smith Chris is a computer science teacher at the Blake School in Minnesota who teaches Python to his beginning students. He can be reached at: csmith@blakeschool.org or smiles@saysomething.com Katherine Smith Katherine and Courtney Gleason created the first version of horsebet.py, which is used as the case study for the last chapters of the book. Katherine can be reached at: kss_0326@yahoo.com Craig T. Snydal Craig is testing the text in a course at Drew University. He has contributed several valuable suggestions and corrections, and can be reached at: csnydal@drew.edu Ian Thomas Ian and his students are using the text in a programming course. They are the first ones to test the chapters in the latter half of the book, and they have make numerous corrections and suggestions. Ian can be reached at: ithomas@sd70.bc.ca Keith Verheyden Keith made correction in Section 3.11 and can be reached at: kverheyd@glam.ac.uk Chris Wrobel Chris made corrections to the code in the chapter on file I/O and exceptions. He can be reached at: Gerard Swinnen : Apprendre a programmer avec Python 357. ferz9 80@yahoo. com Moshe Zadka Moshe has made invaluable contributions to this project. In addition to writing the first draft of the chapter on Dictionaries, he provided continual guidance in the early stages of the book. He can be reached at: moshez@math.huji.ac.il 19.10.2 Preface by J. Elkner This book owes its existance to the collaboration made possible by the internet and the free software movement. Its three authors, a college professor, a high school teacher, and a professional programmer, have yet to meet face to face, but we have been able to work closely together, and have been aided by many wonderful folks who have donated their time and energy to helping make it better. What excites me most about it is that it is a testament to both the benefits and future possibilities of this kind of collaboration, the framework for which has been put in place by Richard Stallman and the Free Software Foundation. a) How and why I came to use Python In 1999, the College Board's Advanced Placement Computer Science exam was given in C++ for the first time. As in many high schools throughout the country, the decision to change languages had a direct impact on the computer science curriculum where I teach at Yorktown High School, in Arlington, Virginia. Up to this point, Pascal was the language of instruction in both our first year and AP courses. In keeping with past practice of giving students two years of exposure to the same language, we made the decision to switch to C++ in the first year course for the 1997-98 school year, so that we would be in step with the College Board's change for the AP course the following year. Two years later, I was convinced that C++ was a poor choice to use for introducing students to computer science. While it is certainly a very powerful programming language, it is also an extremely difficult language to learn and teach. I found myself constantly fighting with C++'s difficult syntax and multiple ways of doing things, and I was losing many students unnecessarily as a result. Convinced there had to be a better language choice for our first year class, I went looking for an alternative to C++. A discussion on the High School Linux Users' Group mailing list provided a solution. A thread emerged during the latter part of January, 1999 concerning the best programming language for use with first time high school computer science students. In a posting on January 30th, Brendon Ranking wrote: I believe that Python is the best choice for any entry-level programming class. It teaches proper programming principles while being incredibly easy to learn. It is also designed to be object oriented from its inception so it doesn't have the add-on pain that both Perl and C++ suffer from It is also *very* widely supported and very much web-centric, as well. I had first heard of Python a few years earlier at a Linux Install Fest, when an enthusiastic Michael McLay told me about Python's many merits. He and Brendon had now convinced me that I needed to look into Python. Matt Ahrens, one of Yorktown's gifted students, jumped at the chance to try out Python, and in the final two months of the 1998-99 school year he not only learned the language but wrote an application called pyTicket which enabled our staff to report technology problems via the web. I knew that Matt could not have finished an application of that scale in so short a time in C++, and this accomplishment combined with Matt's positive assessment of Python suggested Python was the solution I was looking for. b) Finding a text book Having decided to use Python in both my introductory computer science classes the following year, the most pressing problem was the lack of an available text book. Free content came to the rescue. Earlier in the year Richard Stallman had introduced me to Allen Downey. Both of us had written to Richard expressing an interest in developing free educational content. 358. Gerard Swinnen : Apprendre a programmer avec Python Allen had already written a first year computer science text book titled, How to think like a computer scientist. When I read this book I knew immediately that I wanted to use it in my class. It was the clearest and most helpful computer science text I had seen. It emphasized the processes of thought involved in programming, rather than the features of a particular language. Reading it immediately made me a better teacher. Not only was How to think like a computer scientist an excellent book, but it was also released under a GNU public license, which meant it could be used freely and modified to meet the needs of its user. Once I decided to use Python, it occurred to me that I could translate Allen's original Java version into the new language. While I would not have been able to write a text book on my own, having Allen's book to work from made it possible for me to do so, at the same time demonstrating that the cooperative development model used so well in software could also work for educational content. Working on this book for the last two years has been rewarding for both me and my students, and the students played a big part in the process. Since I could make instant changes whenever someone found a spelling error or difficult passage, I encouraged them to look for errors in the book by giving them a bonus point every time they found or suggested something that resulted in a change in the text. This had the double benefit of encouraging them to read the text more carefully, and of getting the text thoroughly reviewed by its most important critics, students using it to learn computer science. For the second half of the book on object oriented programming, I knew that someone with more real programming experience than I had would be needed to do it right. The book actually sat in an unfinished state for the better part of a year until two things happened that led to its completion. I received an email from Chris Meyers expressing interest in the book. Chris is a professional programmer who started teaching a programming course last year using Python at Lane Community College in Eugene Oregon. The prospect of teaching the course had led Chris to the book, and he started helping out with it immediately. By the end of the school year he had created a companion project on our web site at http://www.ibiblio.org/obp called Python for Fun and was working with some of my most advanced students as a master teacher, guiding them beyond the places I could take them. c) Introducing programming with Python The process of translating and using How to think like a computer scientist for the past two years has confirmed Python's suitability to teaching beginning students. Python greatly simplifies programming examples and makes important programming ideas easier to teach. The first example from the text dramatically illustrates this point. It is the traditional "hello, world" program, which in the C++ version of the book looks like this: #include void main O { cout « "Hello, world." « endl; } in the Python version it becomes: print "Hello, World!" Even though this is a trivial example, the advantages to Python stand out. There are no prerequisites to Yorktown's Computer Science I course, so many of the students seeing this example are looking at their first program. Some of them are undoubtedly a little nervous, having heard that computer programming is difficult to learn. The C++ version has always forced me to choose between two unsatisfying options: either to explain the #include, void main(), {, and } statements, and risk confusing or intimidating some of the students right at the start, or to tell them "just don't worry about all of that stuff now, we will talk about it later" and risk the same thing. The educational objectives at this point in the course are to introduce students to the idea of a programming statement and to get them to make their first program, thereby introducing them to the programming environment. The Python program has exactly what is needed to do these things, and nothing more. Gerard Swinnen : Apprendre a programmer avec Python 359. Comparing Section 1.5 of each version of the book, where this first program is located, further illustrates what this means to the beginning student. There are thirteen paragraphs of explanation of "Hello, world" in the C++ version, in the Python version there are only two. More importantly, the missing eleven paragraphs do not deal with the "big ideas" in computer programming, but with the minutia of C++ syntax. I found this same thing happening throughout the book. Whole paragraphs simply disappear from the Python version of the text because Python's much clearer syntax renders them unnecessary. Using a very high level language like Python allows a teacher to postpone talking about low level details of the machine until students have the background that they need to better make sense of the details. It thus creates the ability to put "first things first" pedagogically. One of the best examples of this is the way in which Python handles variables. In C++ a variable is a name for a place which holds a thing. Variables have to be declared with types at least in part because the size of the place to which they refer needs to be predetermined. Thus the idea of a variable is bound up with the hardware of the machine. The powerful and fundamental concept of a variable is already difficult enough for beginning students (in both Computer Science and Algebra). Bytes and addresses do not help the matter. In Python a variable is a name which refers to a thing. This is a far more intuitive concept for beginning students, and one which is much closer to the meaning of variable that they learned in their math class. I had much less difficulty teaching variables this year than I did in the past, and I spent less time helping students with problems using them. Another example of how Python aides in the teaching and learning of programming is in its syntax for functions. My students have always had a great deal of difficulty understanding functions. The main problem centers around the difference between a function definition and a function call, and the related distinction between a parameter and an argument. Python comes to the rescue with syntax that is nothing short of beautiful. Function definitions begin with the key word def, so I simply tell my students, "when you define a function, begin with def, followed by the name of the function that you are defining, when you call a function, simply call (type) out its name." Parameters go with definitions, arguments go with calls. There are no return types or parameter types or reference and value parameters to get in the way, so I am now able to teach functions in less then half the time that it previously took me, with better comprehension. Using Python has improved the effectiveness of our computer science program for all students. I see a higher general level of success and a lower level of frustration than I experienced during the two years using C++. I move faster with better results. More students leave the course with the ability to create meaningful programs, and with the positive attitude toward the experience of programming that this engenders. d) Building a community I have received email every continent on the globe and from as far away as Korea from people using this book to learn or to teach programming. A user community has begun to emerge and increasing numbers of people have been contributing to the project by sending in materials for the companion web site at http://www.ibiblio.org/obp. With the publication of the book in print form, I expect the growth in the user community to continue and accelerate. It is the emergence of this user community and the possibility it suggests for similar collaboration among educators that has been the most exciting thing for me about working on the project. By working together we can both increase the quality of materials available for our use and save valuable time. I invite you to join our community and look forward to hearing from you. Jeffrey Elkner Yorktown High School Arlington, Virginia 360. Gerard Swinnen : Apprendre a programmer avec Python 19.10.3 GNU Free Documentation License Version 1.1, March 2000 Copyright © 2000 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The purpose of this License is to make a manual, textbook, or other written document "free" in the sense of freedom: to assure everyone the effective freedom to copy and redistribute it, with or without modifying it, either commercially or noncommercially. Secondarily, this License preserves for the author and publisher a way to get credit for their work, while not being considered responsible for modifications made by others. This License is a kind of "copyleft," which means that derivative works of the document must themselves be free in the same sense. It complements the GNU General Public License, which is a copyleft license designed for free software. We have designed this License in order to use it for manuals for free software, because free software needs free documentation: a free program should come with manuals providing the same freedoms that the software does. But this License is not limited to software manuals; it can be used for any textual work, regardless of subject matter or whether it is published as a printed book. We recommend this License principally for works whose purpose is instruction or reference. 19.10.3. a.l Applicability and Definitions This License applies to any manual or other work that contains a notice placed by the copyright holder saying it can be distributed under the terms of this License. The "Document," below, refers to any such manual or work. Any member of the public is a licensee, and is addressed as "you." A "Modified Version" of the Document means any work containing the Document or a portion of it, either copied verbatim, or with modifications and/or translated into another language. A "Secondary Section" is a named appendix or a front-matter section of the Document that deals exclusively with the relationship of the publishers or authors of the Document to the Document's overall subject (or to related matters) and contains nothing that could fall directly within that overall subject. (For example, if the Document is in part a textbook of mathematics, a Secondary Section may not explain any mathematics.) The relationship could be a matter of historical connection with the subject or with related matters, or of legal, commercial, philosophical, ethical, or political position regarding them. The "Invariant Sections" are certain Secondary Sections whose titles are designated, as being those of Invariant Sections, in the notice that says that the Document is released under this License. The "Cover Texts" are certain short passages of text that are listed, as Front-Cover Texts or Back-Cover Texts, in the notice that says that the Document is released under this License. A "Transparent" copy of the Document means a machine-readable copy, represented in a format whose specification is available to the general public, whose contents can be viewed and edited directly and straightforwardly with generic text editors or (for images composed of pixels) generic paint programs or (for drawings) some widely available drawing editor, and that is suitable for input to text formatters or for automatic translation to a variety of formats suitable for input to text formatters. A copy made in an otherwise Transparent file format whose markup has been designed to thwart or discourage subsequent modification by readers is not Transparent. A copy that is not "Transparent" is called "Opaque." Examples of suitable formats for Transparent copies include plain ASCII without markup, Texinfo input format, \LaTeX~input format, SGML or XML using a publicly available DTD, and standard-conforming simple HTML designed for human modification. Opaque formats include PostScript, PDF, proprietary formats that can be read and edited only by proprietary word processors, SGML or XML for which the DTD and/or processing tools are not generally available, and the machine-generated HTML produced by some word processors for output purposes only. The "Title Page" means, for a printed book, the title page itself, plus such following pages as are needed Gerard Swinnen : Apprendre a programmer avec Python 361. to hold, legibly, the material this License requires to appear in the title page. For works in formats which do not have any title page as such, "Title Page" means the text near the most prominent appearance of the work's title, preceding the beginning of the body of the text. 19.10.3.a.2 Verbatim Copying You may copy and distribute the Document in any medium, either commercially or noncommercially, provided that this License, the copyright notices, and the license notice saying this License applies to the Document are reproduced in all copies, and that you add no other conditions whatsoever to those of this License. You may not use technical measures to obstruct or control the reading or further copying of the copies you make or distribute. However, you may accept compensation in exchange for copies. If you distribute a large enough number of copies you must also follow the conditions in Section 3. You may also lend copies, under the same conditions stated above, and you may publicly display copies. 19.10.3.a.3 Copying in Quantity If you publish printed copies of the Document numbering more than 100, and the Document's license notice requires Cover Texts, you must enclose the copies in covers that carry, clearly and legibly, all these Cover Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on the back cover. Both covers must also clearly and legibly identify you as the publisher of these copies. The front cover must present the full title with all words of the title equally prominent and visible. You may add other material on the covers in addition. Copying with changes limited to the covers, as long as they preserve the title of the Document and satisfy these conditions, can be treated as verbatim copying in other respects. If the required texts for either cover are too voluminous to fit legibly, you should put the first ones listed (as many as fit reasonably) on the actual cover, and continue the rest onto adjacent pages. If you publish or distribute Opaque copies of the Document numbering more than 100, you must either include a machine-readable Transparent copy along with each Opaque copy, or state in or with each Opaque copy a publicly accessible computer-network location containing a complete Transparent copy of the Document, free of added material, which the general network-using public has access to download anonymously at no charge using public-standard network protocols. If you use the latter option, you must take reasonably prudent steps, when you begin distribution of Opaque copies in quantity, to ensure that this Transparent copy will remain thus accessible at the stated location until at least one year after the last time you distribute an Opaque copy (directly or through your agents or retailers) of that edition to the public. It is requested, but not required, that you contact the authors of the Document well before redistributing any large number of copies, to give them a chance to provide you with an updated version of the Document. 19.10.3.a.4 Modifications You may copy and distribute a Modified Version of the Document under the conditions of Sections 2 and 3 above, provided that you release the Modified Version under precisely this License, with the Modified Version filling the role of the Document, thus licensing distribution and modification of the Modified Version to whoever possesses a copy of it. In addition, you must do these things in the Modified Version: • Use in the Title Page (and on the covers, if any) a title distinct from that of the Document, and from those of previous versions (which should, if there were any, be listed in the History section of the Document). You may use the same title as a previous version if the original publisher of that version gives permission. • List on the Title Page, as authors, one or more persons or entities responsible for authorship of the modifications in the Modified Version, together with at least five of the principal authors of the Document (all of its principal authors, if it has less than five). • State on the Title page the name of the publisher of the Modified Version, as the publisher. • Preserve all the copyright notices of the Document. • Add an appropriate copyright notice for your modifications adjacent to the other copyright notices. • Include, immediately after the copyright notices, a license notice giving the public permission to use the Modified Version under the terms of this License, in the form shown in the Addendum below. 362. Gerard Swinnen : Apprendre a programmer avec Python • Preserve in that license notice the full lists of Invariant Sections and required Cover Texts given in the Document's license notice. • Include an unaltered copy of this License. • Preserve the section entitled "History," and its title, and add to it an item stating at least the title, year, new authors, and publisher of the Modified Version as given on the Title Page. If there is no section entitled "History" in the Document, create one stating the title, year, authors, and publisher of the Document as given on its Title Page, then add an item describing the Modified Version as stated in the previous sentence. • Preserve the network location, if any, given in the Document for public access to a Transparent copy of the Document, and likewise the network locations given in the Document for previous versions it was based on. These may be placed in the "History" section. You may omit a network location for a work that was published at least four years before the Document itself, or if the original publisher of the version it refers to gives permission. • In any section entitled "Acknowledgements" or "Dedications," preserve the section's title, and preserve in the section all the substance and tone of each of the contributor acknowledgements and/or dedications given therein. • Preserve all the Invariant Sections of the Document, unaltered in their text and in their titles. Section numbers or the equivalent are not considered part of the section titles. • Delete any section entitled "Endorsem*nts." Such a section may not be included in the Modified Version. • Do not retitle any existing section as "Endorsem*nts" or to conflict in title with any Invariant Section. If the Modified Version includes new front-matter sections or appendices that qualify as Secondary Sections and contain no material copied from the Document, you may at your option designate some or all of these sections as invariant. To do this, add their titles to the list of Invariant Sections in the Modified Version's license notice. These titles must be distinct from any other section titles. You may add a section entitled "Endorsem*nts," provided it contains nothing but endorsem*nts of your Modified Version by various parties — for example, statements of peer review or that the text has been approved by an organization as the authoritative definition of a standard. You may add a passage of up to five words as a Front-Cover Text, and a passage of up to 25 words as a Back-Cover Text, to the end of the list of Cover Texts in the Modified Version. Only one passage of Front- Cover Text and one of Back-Cover Text may be added by (or through arrangements made by) any one entity. If the Document already includes a cover text for the same cover, previously added by you or by arrangement made by the same entity you are acting on behalf of, you may not add another; but you may replace the old one, on explicit permission from the previous publisher that added the old one. The author(s) and publisher(s) of the Document do not by this License give permission to use their names for publicity for or to assert or imply endorsem*nt of any Modified Version. 19.10.3. a.5 Combining Documents You may combine the Document with other documents released under this License, under the terms defined in Section 4 above for modified versions, provided that you include in the combination all of the Invariant Sections of all of the original documents, unmodified, and list them all as Invariant Sections of your combined work in its license notice. The combined work need only contain one copy of this License, and multiple identical Invariant Sections may be replaced with a single copy. If there are multiple Invariant Sections with the same name but different contents, make the title of each such section unique by adding at the end of it, in parentheses, the name of the original author or publisher of that section if known, or else a unique number. Make the same adjustment to the section titles in the list of Invariant Sections in the license notice of the combined work. In the combination, you must combine any sections entitled "History" in the various original documents, forming one section entitled "History"; likewise combine any sections entitled "Acknowledgements," and any sections entitled "Dedications." You must delete all sections entitled "Endorsem*nts." 19.10.3. a.6 Collections of Documents You may make a collection consisting of the Document and other documents released under this License, Gerard Swinnen : Apprendre a programmer avec Python 363. and replace the individual copies of this License in the various documents with a single copy that is included in the collection, provided that you follow the rules of this License for verbatim copying of each of the documents in all other respects. You may extract a single document from such a collection, and distribute it individually under this License, provided you insert a copy of this License into the extracted document, and follow this License in all other respects regarding verbatim copying of that document. 19.10.3.a.7 Aggregation with Independent Works A compilation of the Document or its derivatives with other separate and independent documents or works, in or on a volume of a storage or distribution medium, does not as a whole count as a Modified Version of the Document, provided no compilation copyright is claimed for the compilation. Such a compilation is called an "aggregate," and this License does not apply to the other self-contained works thus compiled with the Document, on account of their being thus compiled, if they are not themselves derivative works of the Document. If the Cover Text requirement of Section 3 is applicable to these copies of the Document, then if the Document is less than one quarter of the entire aggregate, the Document's Cover Texts may be placed on covers that surround only the Document within the aggregate. Otherwise they must appear on covers around the whole aggregate. 19.10.3.a.8 Translation Translation is considered a kind of modification, so you may distribute translations of the Document under the terms of Section 4. Replacing Invariant Sections with translations requires special permission from their copyright holders, but you may include translations of some or all Invariant Sections in addition to the original versions of these Invariant Sections. You may include a translation of this License provided that you also include the original English version of this License. In case of a disagreement between the translation and the original English version of this License, the original English version will prevail. 19.10.3.a.9 Termination You may not copy, modify, sublicense, or distribute the Document except as expressly provided for under this License. Any other attempt to copy, modify, sublicense, or distribute the Document is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 19.10.3.a.l0 Future Revisions of This License The Free Software Foundation may publish new, revised versions of the GNU Free Documentation License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. See http:///www.gnu.org/copyleft/. Each version of the License is given a distinguishing version number. If the Document specifies that a particular numbered version of this License "or any later version" applies to it, you have the option of following the terms and conditions either of that specified version or of any later version that has been published (not as a draft) by the Free Software Foundation. If the Document does not specify a version number of this License, you may choose any version ever published (not as a draft) by the Free Software Foundation. 364. Gerard Swinnen : Apprendre a programmer avec Python Table des matieres Introduction 4 Choix d'un premier langage de programmation 5 Presentation du langage Python, par Stefane Fermigier 6 Plusieurs versions differentes ? 7 Distribution de Python - Bibliographie 8 Pour le professeur qui souhaite utiliser cet ouvrage comme support de cours 9 Exemples du livre 10 Remerciements 10 Chapitre 1 : Penser comme un programmeur 11 1.1 La demarche du programmeur 11 1.2 Langage machine, langage de programmation 11 1 .3 Compilation et interpretation 13 1.4 Mise au point d'un programme - Recherche des erreurs (« debug ») 15 1 .4. 1 Erreurs de syntaxe 15 1.4.2 Erreurs semantiques 15 1.4.3 Erreurs a l'execution 16 1.5 Recherche des erreurs et experimentation 16 1.6 Langages naturels et langages formels 17 Chapitre 2 : Premiers pas 19 2. 1 Calculer avec Python 19 2.2 Donnees et variables 21 2.3 Noms de variables et mots reserves 22 2.4 Affectation (ou assignation) 23 2.5 Afficher la valeur dune variable 24 2.6 Typage des variables 24 2.7 Affectations multiples 25 2.8 Operateurs et expressions 26 2.9 Priorite des operations 27 2.10 Composition 28 Chapitre 3 : Controle du flux d'instructions 29 3.1 Sequence d'instructions 29 3.2 Selection ou execution conditionnelle 29 3.3 Operateurs de comparaison 31 3.4 Instructions composees - Blocs d'instructions 31 3.5 Instructions imbriquees 32 3.6 Quelques regies de syntaxe Python 32 3.6.1 Les limites des instructions et des blocs sont definies par la mise en page 32 3.6.2 Instruction composee = En-tete , double point , bloc d'instructions indente 33 3.6.3 Les espaces et les commentaires sont normalement ignores 33 Chapitre 4 : Instructions repetitives 34 4.1 Re-affectation 34 Gerard Swinnen : Apprendre a programmer avec Python 365. 4.2 Repetitions en boucle - l'instruction while 35 4.3 Elaboration de tables 37 4.4 Construction d'une suite mathematique 37 4.5 Premiers scripts, ou : Comment conserver nos programmes ? 38 4.6 Remarque concernant les caracteres accentues et speciaux : 40 Chapitre 5 : Principaux types de donnees 42 5.1 Les donnees numeriques 42 5.1.1 Les types « integer » et « long » 42 5.1.2 Le type « float » 44 5.2 Les donnees alphanumeriques 46 5.2.1 Le type « string » (chaine de caracteres) 46 5.2.2 Acces aux caracteres individuels d'une chaine 47 5.2.3 Operations elementaires sur les chaines 48 5.3 Les listes (premiere approche) 50 Chapitre 6 : Fonctions predefinies 53 6.1 Interaction avec l'utilisateur : la fonction input() 53 6.2 Importer un module de fonctions 54 6.3 Un peu de detente avec le module turtle 56 6.4 Veracite/faussete d'une expression 57 6.5 Revision 58 6.5.1 Controle du flux - Utilisation d'une liste simple 58 6.5.2 Boucle while - Instructions imbriquees 59 Chapitre 7 : Fonctions originales 62 7.1 Definir une fonction 62 7.1.1 Fonction simple sans parametres 63 7.1.2 Fonction avec parametre 65 7.1.3 Utilisation d'une variable comme argument 66 7.1.4 Fonction avec plusieurs parametres 67 7.2 Variables locales, variables globales 68 7.3 « Vraies » fonctions et procedures 70 7.4 Utilisation des fonctions dans un script 72 7.5 Modules de fonctions 73 7.6 Typage des parametres 78 7.7 Valeurs par defaut pour les parametres 78 7.8 Arguments avec etiquettes 79 Chapitre 8 : Utilisation de fenetres et de graphismes 81 8.1 Interfaces graphiques (GUI) 81 8.2 Premiers pas avec Tkinter 81 8.3 Programmes pilotes par des evenements 85 8.3.1 Exemple graphique : trace de lignes dans un canevas 87 8.3.2 Exemple graphique : deux dessins alternes 90 8.3.3 Exemple graphique : calculatrice minimaliste 92 8.3.4 Exemple graphique : detection et positionnement dun clic de souris 94 366. Gerard Swinnen : Apprendre a programmer avec Python 8.4 Les classes de widgets Tkinter 95 8.5 Utilisation de la methode grid() pour controler la disposition des widgets 96 8.6 Composition d'instructions pour ecrire un code plus compact 100 8.7 Modification des proprietes d'un objet - Animation 102 8.8 Animation automatique - Recurs ivite 105 Chapitre 9 : Les fichiers 108 9.1 Utilite des fichiers 108 9.2 Travailler avec des fichiers 109 9.3 Noms de fichiers - Repertoire courant 110 9.4 Les deux formes d'importation 110 9.5 Ecriture sequentielle dans un fichier 112 9.6 Lecture sequentielle d'un fichier 113 9.7 L'instruction break pour sortir d'une boucle 114 9.8 Fichiers texte 115 9.9 Enregistrement et restitution de variables diverses 117 9.10 Gestion des exceptions. Les instructions try - except - else 118 Chapitre 10 : Approfondir les structures de donnees 121 10.1 Le point sur les chaines de caracteres 121 10.1.1 Concatenation, Repetition 121 10.1.2 Indicage, extraction, longueur 121 10.1.3 Parcours d'une sequence. L'instruction for ... in 123 10.1.4 Appartenance d'un element a une sequence. L'instruction in utilisee seule 124 10.1.5 Les chaines sont des sequences non modifiables 125 10.1.6 Les chaines sont comparables 126 10.1.7 Classem*nt des caracteres 126 10.1.8 Les chaines sont des objets 128 10.1.9 Formatage des chaines de caracteres 130 10.2 Le point sur les listes 132 10.2.1 Definition d'une liste - Acces a ses elements 132 10.2.2 Les listes sont modifiables 133 10.2.3 Les listes sont des objets 133 10.2.4 Techniques de « slicing » avance pour modifier une liste 135 10.2.5 Creation d'une liste de nombres a l'aide de la fonction rangef) 136 10.2.6 Parcours d'une liste a l'aide de for, rangef) et len() 136 10.2.7 Une consequence du typage dynamique 137 10.2.8 Operations sur les listes 137 10.2.9 Test d'appartenance 137 10.2.10 Copie d'une liste 138 10.2.1 1 Nombres aleatoires - Histogrammes 140 10.3 Les tuples 143 10.4 Les dictionnaires 144 10.4.1 Creation d'un dictionnaire 1 44 10.4.2 Operations sur les dictionnaires 145 Gerard Swinnen : Apprendre a programmer avec Python 367 '. 10.4.3 Les dictionnaires sont des objets 145 10.4.4 Parcours dun dictionnaire 146 10.4.5 Les cles ne sont pas necessairement des chaines de caracteres 147 10.4.6 Les dictionnaires ne sont pas des sequences 148 10.4.7 Construction d'un histogramme a l'aide d'un dictionnaire 149 10.4.8 Controle du flux d'execution a l'aide d'un dictionnaire 150 Chapitre 11 : Classes, objets, attributs 152 11.1 Utilite des classes 152 11.2 Definition d'une classe elementaire 153 1 1.3 Attributs (ou variables) d'instance 154 1 1.4 Passage d' objets comme arguments lors de l'appel d'une fonction 155 11.5 Similitude et unicite 155 11.6 Objets composes d'objets 156 1 1.7 Objets comme valeurs de retour d'une fonction 157 1 1.8 Les objets sont modifiables 158 Chapitre 12 : Classes, methodes, heritage 159 12.1 Definition d'une methode 159 12.2 La methode « constructeur » 161 12.3 Espaces de noms des classes et instances 165 12.4 Heritage 166 12.5 Heritage et polymorphisme 167 12.6 Modules contenant des bibliotheques de classes 171 Chapitre 13 : Classes & Interfaces graphiques 174 13.1 « Code des couleurs » : un petit projet bien encapsule 174 13.2 « Petit train » : heritage, echange d'informations entre classes 178 13.3 « OscilloGraphe » : un widget personnalise 181 13.4 « Curseurs » : un widget composite 186 13.4.1 Presentation du widget « Scale » 186 13.4.2 Construction d'un panneau de controle a trois curseurs 187 13.5 Integration de widgets composites dans une application synthese 191 Chapitre 14 : Et pour quelques widgets de plus 198 14.1 Les « boutons radio » 198 14.2 Utilisation des cadres (frames) pour la composition d'une fenetre 200 14.3 Comment deplacer des dessins a l'aide de la souris 202 14.4 Python Mega Widgets 205 14.4.1 « Combo Box » 205 14.4.2 Remarque concernant l'entree de caracteres accentues 206 14.4.3 « Scrolled Text » 207 14.4.4 « Scrolled Canvas » 210 14.4.5 Barres d'outils avec bulles d'aide - expressions lambda 213 14.5 Fenetres avec menus 216 14.5.1 Premiere ebauche du programme : 217 14.5.2 Ajout de la rubrique « Musiciens » 219 368. Gerard Swinnen : Apprendre a programmer avec Python 14.5.3 Ajout de la rubrique « Peintres » : 221 14.5.4 Ajout de la rubrique « Options » : 222 Chapitre 15 : Analyse de programmes concrets 227 15.1 Jeu des bombardes 227 15.1.1 Prototypage d'une classe « Canon » 229 15.1.2 Ajout de methodes au prototype 232 15.1.3 Developpement de l'application 234 15.1.4 Developpements complementaires 239 15.2 Jeu de Ping 243 15.2.1 Principe 243 15.2.2 Programmation 244 Chapitre 16 : Gestion d'une base de donnees 249 16.1 Les bases de donnees 249 16.1.1 SGBDR - Le modele client/serveur 249 16.1.2 Le langage SQL - Gadfly 250 16.2 Mise en oeuvre d'une base de donnees simple avec Gadfly 251 16.2.1 Creation de la base de donnees 251 16.2.2 Connexion a une base de donnees existante 252 16.2.3 Recherches dans une base de donnees 253 16.2.4 La requete select 255 16.3 Ebauche dun logiciel client pour MySQL 256 16.3.1 Decrire la base de donnees dans un dictionnaire d'application 256 16.3.2 Definir une classe d'objets-interfaces 258 16.3.3 Construire un generateur de formulaires 261 16.3.4 Le corps de l'application 262 Chapitre 17 : Applications web 264 17.1 Pages web interactives 264 17.2 L'interface CGI 265 17.2.1 Une interaction CGI rudimentaire 265 17.2.2 Un formulaire HTML pour l'acquisition des donnees 267 17.2.3 Un script CGI pour le traitement des donnees 268 17.3 Un serveur web en pur Python ! 269 17.3.1 Installation de Karrigell 270 17.3.2 Demarrage du serveur : 270 17.3.3 Ebauche de site web 271 17.3.4 Prise en charge des sessions 273 17.3.5 Autres developpements 277 Chapitre 18 : Communications a travers un reseau 278 18.1 Les sockets 278 18.2 Construction dun serveur elementaire 279 18.3 Construction dun client rudimentaire 281 18.4 Gestion de plusieurs taches en parallele a l'aide des threads 282 18.5 Client gerant remission et la reception simultanees 283 Gerard Swinnen : Apprendre a programmer avec Python 369. 18.6 Serveur gerant les connexions de plusieurs clients en parallele 285 18.7 Jeu des bombardes, version reseau 287 18.7.1 Programme serveur : vue d'ensemble 288 18.7.2 Protocole de communication 288 18.7.3 Programme serveur : premiere partie 290 18.7.4 Synchronisation de threads concurrents a l'aide de « verrous » (thread locks) 293 18.7.5 Programme serveur : suite et fin 294 18.7.6 Programme client 297 18.8 Utilisation de threads pour optimiser les animations 300 18.8.1 Temporisation des animations a l'aide de after() 300 18.8.2 Temporisation des animations a l'aide de time.sleep() 301 18.8.3 Exemple concret 302 Chapitre 19 : Annexes 304 19.1 Installation de Python 304 19.2 Sous Windows 304 19.3 Sous Linux 304 19.4 SousMacOS 304 19.5 Installation de SciTE (Scintilla Text Editor) 304 19.5.1 Installation sous Linux : 305 19.5.2 Installation sous Windows : 305 19.5.3 Pour les deux versions : 305 19.6 Installation des Python mega-widgets 305 19.7 Installation de Gadfly (systeme de bases de donnees) 306 19.8 Sous Windows : 306 19.8.1 Sous Linux : 306 19.9 Solutions aux exercices 307 19.10 Annexes extraites de « How to think like a computer scientist » 356 19.10.1 Contributor list 356 19.10.2 Preface 358 19.10.3 GNU Free Documentation License 361 370. Gerard Swinnen : Apprendre a programmer avec Python

Exercice avec Python 

3 . (2024)

References

Top Articles
The best-performing stock on the S&P 500 is a ‘unicorn’ that joined the index less than a month ago and is beating Nvidia
Cdb Arlington Ky
Cintas Pay Bill
Windcrest Little League Baseball
³µ¿Â«»ÍÀÇ Ã¢½ÃÀÚ À̸¸±¸ ¸íÀÎ, ¹Ì±¹ Ķ¸®Æ÷´Ï¾Æ ÁøÃâ - ¿ù°£ÆÄ¿öÄÚ¸®¾Æ
Craigslist Parsippany Nj Rooms For Rent
Sprague Brook Park Camping Reservations
Lenscrafters Westchester Mall
Parks in Wien gesperrt
Lycoming County Docket Sheets
Mylife Cvs Login
PGA of America leaving Palm Beach Gardens for Frisco, Texas
Osrs Blessed Axe
Hca Florida Middleburg Emergency Reviews
Arboristsite Forum Chainsaw
Teenleaks Discord
Mals Crazy Crab
Rondom Ajax: ME grijpt in tijdens protest Ajax-fans bij hoofdbureau politie
Allentown Craigslist Heavy Equipment
Homeaccess.stopandshop
The Largest Banks - ​​How to Transfer Money With Only Card Number and CVV (2024)
Dewalt vs Milwaukee: Comparing Top Power Tool Brands - EXTOL
Amelia Chase Bank Murder
1145 Barnett Drive
Mynahealthcare Login
49S Results Coral
Kiddie Jungle Parma
3473372961
Diggy Battlefield Of Gods
La Qua Brothers Funeral Home
Microsoftlicentiespecialist.nl - Microcenter - ICT voor het MKB
Save on Games, Flamingo, Toys Games & Novelties
Metro 72 Hour Extension 2022
Terrier Hockey Blog
Final Exam Schedule Liberty University
Aveda Caramel Toner Formula
Soulstone Survivors Igg
How To Upgrade Stamina In Blox Fruits
Amc.santa Anita
Winta Zesu Net Worth
Penny Paws San Antonio Photos
Best Conjuration Spell In Skyrim
Babykeilani
Unblocked Games - Gun Mayhem
Bridgeport Police Blotter Today
Whitney Wisconsin 2022
Greatpeople.me Login Schedule
Runescape Death Guard
Razor Edge Gotti Pitbull Price
Appsanywhere Mst
Subdomain Finer
Supervisor-Managing Your Teams Risk – 3455 questions with correct answers
Latest Posts
Article information

Author: Eusebia Nader

Last Updated:

Views: 6038

Rating: 5 / 5 (80 voted)

Reviews: 95% of readers found this page helpful

Author information

Name: Eusebia Nader

Birthday: 1994-11-11

Address: Apt. 721 977 Ebert Meadows, Jereville, GA 73618-6603

Phone: +2316203969400

Job: International Farming Consultant

Hobby: Reading, Photography, Shooting, Singing, Magic, Kayaking, Mushroom hunting

Introduction: My name is Eusebia Nader, I am a encouraging, brainy, lively, nice, famous, healthy, clever person who loves writing and wants to share my knowledge and understanding with you.