Gerard Swinnen Apprendre d P 9 CIV6C Python Objet • Multithreading • Bases de donnees • Evenements Programmation web • Programmation reseau • Unicode. EYROLLES Apprendre a programmer ave< DANS LA MEME COLLECTION H. Bersini, I. Wellesz. - La programmation orientee objet. Cours et exercices en UML 2 avec Java 5, C# 2, C++, Python et PHP 5. N12441, 4 e edition, 2009, 602 pages (collection Noire). C. Delannoy. - Programmer en Java. Java 5 et 6. N°12232, 5 C edition, 2007, 800 pages avec CD-Rom. J.-B. Boichat. Apprendre Java et C++ en parallele. N° 12403, 4 e edition, 2008, 600 pages avec CD-Rom. A. Tasso. - Le livre de Java premier langage. Avec 80 exercices corriges. N°12376, 5 e edition, 2008, 520 pages avec CD-Rom. C. Dabancourt. - Apprendre a programmer. Algorithmes et conception objet - BTS, Deng, IUT, licence N° 12350, 2= edition, 2008, 296. P. Roques. - UML 2 par la pratique. Etude de cas et exercices corriges. N°12322, 6= edition, 2008, 368. A. Tasso. Apprendre a programmer en ActionScript 3. N°12199, 2008, 438 pages. A. Brillant. - XML. Cours et exercices. N°12151, 2007, 282 pages. X Blanc, I. Mounier. - UML 2 pour les developpeurs. N° 12029, 2006, 202 pages H. Sutter (trad. T. Petillon). - Mieux programmer en C++. N°09224, 2001, 215 pages. CHEZ LE MEME EDITEUR T. Ziade. Programmation Python. N° 11677, 2006, 530 pages (Collection Blanche). B. Meyer. - Conception et programmation orientees objet. N°12270, 2008, 1222 pages (Collection Blanche). R. Goetter. - CSS 2 : pratique du design web. N° 12461, 3 e edition, 2009, 340 pages. A. Boucher. - Ergonomie web. Pour des sites web efficaces. N°12158, 2007, 426 pages. V. Messager Rota. - Gestion de projet. Vers les methodes agiles. N°12165, 2007, 258 pages (collection Architecte logiciel). J.-L. Benard, L. Bossavit , R.Medina , D. Williams. - L'Extreme Programming, avec deux etudes de cas. N°11051, 2002, 300 pages. P. Roques. - UML 2. Modeliser une application web. N°11770, 2006, 236 pages (coll. Cahiers du programmeur). E. Puybaret. - Swing. N°12019, 2007, 500 pages (coll. Cahiers du programmeur) E. Puybaret. - Java 1.4 et 5.0. N°11916, 3 C edition 2006, 400 pages (coll. Cahiers du programmeur) S Powers. Debuter en JavaScript. N°12093, 2007, 386 pages T. Templier, A. Gougeon. - JavaScript pour le Web 2.0. N° 12009, 2007, 492 pages X. Briffault, S. Ducasse. - Programmation Squeak. N°11023, 2001, 328 pages. P. Rigaux, A. Rochfeld. - Traite de modelisation objet. N° 11035, 2002, 308 pages. Gerard Swinnen Apprendre a programmer avec Python Avec plus de 40 pages de corriges d'exercues ! Objet • Multithreading • Evenements • Bases de donnees Programmation web • Programmation reseau • Unicode... EYROLLES • EDITIONS EYROLLES 61, bd Saint-Germain 75240 Paris Cedex 05 www.editions-eyrolles.com Une version numerique de ce texte peut etre telechargee librement a partir du site : http://www.ulg. ac. be/cifen/inforef/swi Une petite partie de cet ouvrage est adaptee de : How to think like a computer scientist de Allen B. Downey, Jeffrey Elkner & Chris Meyers disponible sur : http://thinkpython.com ou http://www.openbookproject.net/thinkCSpy Le code de la propriete intellectuelle du l el juillet 1992 interdit en effet expressement la photocopie a usage collectif sans autorisation des ayants droit. Or, cette pratique s'est generalisee notamment dans les etablissements d'enseignement, provoquant une baisse brutale des achats de livres, au point que la possibilite meme pour les auteurs de creer des ceuvres nouvelles et de les faire editer correctement est aujourd'hui menacee. En application de la loi du 11 mars 1957, il est interdit de reproduire integralement ou partiellement le present ouvrage, sur quelque support que ce soit, sans autorisation de Fediteur ou du Centre Francais d' Exploitation du Droit de Copie, 20, rue des Grands-Augustins, 75006 Paris. © Groupe Eyrolles, 2009, ISBN : 978-2-212-12474-3 DANGER PHOTOCOPILLAGE TUELELIVRE Grace Hopper, inventeur du compilateur : « Pour moi, la programmation est plus qu'un art applique important. C'est aussi une ambitieuse quete menee dans les trefonds de la connaissance. » A Maximilien, Elise, Lucille, Augustin et Alexane. Colophon Choisie deliberement hors propos, cette illustration d'ouverture 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 pen- dant pres d'un demi-siecle. Preface En tant que professeur ayant pratique l'enseignement de la programmation en parallele avec d'autres disciplines, je crois pouvoir affirmer qu'il s'agit la d'une forme d'apprentissage extremement enrichis- sante pour la formation intellectuelle d'un jeune et dont la valeur formative est au moins egale, sinon superieure, a celle de branches plus classiques telles que le latin. Excellente idee done, que celle de proposer cet apprentissage dans certaines filieres, y compris de l'en- seignement secondaire. Comprenons-nous bien : il ne s'agit pas de former trop precocement de futurs programmeurs professionnels. Nous sommes simplement convaincus que l'apprentissage de la pro- grammation a sa place dans la formation generale des jeunes (ou au moins d'une partie d'entre eux), car e'est une extraordinaire ecole de logique, de rigueur, et meme de courage. A l'origine, le present ouvrage a ete redige 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. II nous a semble par la suite que ce cours pouvait egalement tres bien convenir a toute personne n'ayant encore jamais programme, mais souhaitant s'initier a cette discipline en autodidacte. Nous y proposons 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'ap- prendre (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 se veut generaliste : nous souhaitons mettre en evidence les inva- riants de la programmation et de l'informatique, sans nous laisser entrainer vers une specialisation quelconque, ni supposer que le lecteur dispose de capacites intellectuelles hors du commun. • Les outils utilises au cours de l'apprentissage doivent etre modernes et performants, mais il faut aussi que le lecteur puisse se les procurer en toute legalite a tres bas prix pour son usage personnel. Notre texte s'adresse en effet en priorite a des etudiants, et toute notre demarche d'apprentissage vise a leur donner la possibilite de mettre en chantier le plus tot possible des realisations person- nelles qu'il pourront developper et exploiter a leur guise. • 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 cette program- mation presente des defis qui apparaissent plus concrets aux yeux d'un programmeur debutant. D'autre part, 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 au lecteur de se lancer tres tot VIII Apprendre a programmer avec Python dans de petits projets personnels attrayants, par lesquels ils puisse se sentir valorise. En revanche, nous laisserons deliberement de cote les environnements de programmation sophistiques qui ecrivent automatiquement de nombreuses lignes de code, parce que nous ne voulons pas non plus masquer la complexite sous-jacente. Certains nous reprocheront que notre demarche n'est pas suffisamment centree sur ralgorithmique pure et dure. Nous pensons que celle-ci 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 pre- coce que possible de l'apprenant avec des objets et des bibliotheques de classes preexistants. Ainsi il ap- prend 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 c'est la reflexion sur les donnees qui doit rester la colonne vertebrale de tout developpement logiciel. Choix d'un premier langage de programmation II existe un tres grand nombre de langages de programmation, chacun avec ses avantages et ses incon- venients. II faut bien en choisir un. Lorsque nous avons commence a reflechir a cette question, durant notre preparation d'un curriculum pour la nouvelle option Sciences & Informatique, nous avions per- sonnellement accumule une assez longue experience de la programmation sous Visual Basic (Microsoft) et sous Clarion (Topspeed). Nous avions egalement experimente quelque peu sous Delphi (Borland). II etait done naturel que nous pensions d'abord exploiter l'un ou l'autre de ces langages. Si nous souhai- tions les utiliser comme outils de base pour un apprentissage general de la programmation, ces langages presentaient toutefois deux gros inconvenients : • Ils sont lies a des environnements de programmation (e'est-a-dire des logiciels) proprietaires. Cela signifiait 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 risquait 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'acquerir eux aussi des licences, ce que nous ne pouvions pas accepter. • Ce sont des langages specifiquement lies au seul systeme d'exploitation Windows. Ils ne sont pas « portables » sur d'autres systemes (Unix, Mac OS, etc.). Cela ne cadrait pas avec notre projet peda- gogique 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 rinformatique libre 1 . Ce que nous avons trouve nous a enthousiasmes : non seule- ment il existe dans le monde de I'Open Source des interpreters et des compilateurs gratuits pour toute une serie de langages, mais surtout ces langages sont modernes, performants, portables (e'est-a-dire uti- lisables sur differents systemes d'exploitation tels que Windows, Linux, Mac OS ...), et fort bien docu- ments. 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 malheureusement tres rebarbatif et compli- 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. Preface IX que, 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) D'autre part, la pratique moderne de ce langage fait abondamment appel a des generateurs d'applica- tions 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 experi- mentes, 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 lan- gage 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. Apres avoir successivement examine et experimente 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 Ce texte de Stefane Fermigier est extrait d'un article paru dans le magazine Programmez! en decembre 1998. II est egalement disponible sur http://www.Hnux- center.org/articles/9812/python.html. Stefane Fermigier est le co-fondateur de I'AFUL (Association Francophone des Utilisateurs de Linux et des logiciels libres). Python est un langage portable, dynamique, extensible, gratuit, qui permet (sans l'imposer) une ap- proche 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 precisement, du langage et de ses deux implantations actuelles: • Python est portable, non seulement sur les differentes variantes d'Unix, mais aussi sur les OS pro- prietaires : Mac OS, 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 rutiliser 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, dic- tionnaires...), 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 main- tenance largement accrue. • Python gere ses ressources (memoire, descripteurs de fichiers...) sans intervention du program- meur, par un mecanisme de comptage de references (proche, mais different, d'un garbage collec- tor). • II n'y a pas de pointeurs explicites en Python. • Python est (optionnellement) multi-threade. X Apprendre a programmer avec Python • 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 per- mettent de simplifier considerablement la gestion des erreurs. • Python est dynamique (Tinterpreteur peut evaluer des chaines de caracteres representant des ex- pressions 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 pro- grammeur 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 pro- grammes 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 ge- nere 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 (fi- chiers, 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 enthou- siastes et responsables, dont la plupart sont des supporters du logiciel lib re. Parallelement a Tinter- preteur 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. 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 me- thodes, 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 autonomic Preface XI Ce sont ces considerations qui nous ont amenes a developper une structure de cours que certains trou- veront peut-etre un peu chaotique. Le debut s'inspire d'un texte americain disponible sous licence libre : « How to think like a computer scientist », par Allen Downey, Jeff Elkner et Chris Meyers (voir : http://greenteapress.com/thinkpython/thinkCSpy/), mais nous l'avons progressivement eclate pour y inse- rer toute une serie d'elements concernant la gestion des entrees/ sorties, et en particulier l'interface gra- phique Tkinter. Nous souhaiterions en effet que les eleves puissent deja realiser une petite application grapbique 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 com- pris 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 ac- quises, en les approfondissant. Les algorithmes seront davantage decortiques et commentes. Les pro- jets, cahiers des charges et methodes d'analyse seront discutes en concertation. On exigera la tenue re- guliere 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 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 cher- chons en aucune maniere a former des specialistes d'un certain langage ou d'un certain domaine tech- nique : 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. Versions du langage Python continue a evoluer, mais cette evolution ne vise qu'a ameliorer ou perfectionner le produit. Vous n'aurez pas a 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 les versions 2.4 et 2.5 apparues depuis, 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 ! Distribution de Python et bibliographie Les differentes versions de Python (pour Windows, Unix, etc.), son tutoriel original, son manuel de re- ference, la documentation des bibliotheques de fonctions, etc. sont disponibles en telechargement gratuit depuis Internet, a partir du site web officiel : http://www.python.org II existe egalement de tres bons ouvrages imprimes concernant Python. En langue francaise, vous pour- rez tres profitablement consulter les manuels ci- apres : XII Apprendre a programmer avec Python • Programmation Python, par Tarek Ziade, Editions Eyrolles, Paris, 2006, 538 p., ISBN 978-2-212- 11677-9. C'est l'un des premiers ouvrages edites directement en langue francaise sur le langage Python. Excellent. Une mine de renseignements essentielle si vous voulez acquerir les meilleures pratiques et vous demarquer des debutants. • Au coeur de Python, volumes 1 et 2, par Wesley J. Chun, traduction de Python core programming, 2d edition (Prentice Hall) par Marie-Cecile Baland, Anne Bohy et Luc Carite, Editions Campus- Press, Paris, 2007, respectivement 645 et 385 p., ISBN 978-2-7440-2148-0 et 978-2-7440-2195-4. C'est un ouvrage de reference indispensable, tres bien ecrit. D'autres excellents ouvrages en francais etaient proposes par la succursale francaise de la maison d'edi- tions O'Reilly, laquelle a malheureusement disparu. En langue anglaise, le choix est evidemment beau- coup plus vaste. Nous apprecions personnellement beaucoup Python : How to program, par Deitel, Liperi & Wiedemann, 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, 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 utile - ment 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 Py- thon (second edition) de Mark Lutz, Editions O'Reilly, 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 souhaitez 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, 2002, 575 p., ISBN 0-596-00167-3 , dont les recettes sont savou- reuses. 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 ou bien directement a cette adresse : http://main.pythomium.net/download/cours_python.zip Preface XIII Remerciements Ce livre est pour une partie le resultat d'un travail personnel, mais pour une autre — bien plus impor- tante - 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 malheureusement 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 ceuvrent au developpement de Python, de ses accessoires et de sa do- cumentation, a commencer par Guido van Rossum, bien sur, mais sans oublier non plus tous les autres ((mal)heureusement 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 Chris - tophe 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 encore a mes partenaires actuels chez Eyrolles, Muriel Shan Sei Fan et Matthieu Montaudouin, qui ont efficacement pris en charge cette nouvelle edi- tion. Merci enfin a mon epouse Suzel, pour sa patience et sa comprehension. Table des matieres 1. PENSER COMME UN PROGRAMMEUR 1 La demarche du programmeur • 1 Langage machine, langage de programmation • 2 Compilation et interpretation • 3 Mise au point d'un programme - Recherche des erreurs (debug) • 4 Erreurs de syntaxe • 4 Erreurs semantiques • 5 Erreurs a l'execution • 5 Recherche des erreurs et experimentation • 5 Langages naturels et langages formels • 6 2. Premiers pas 9 Calculer avec Python • 9 Donnees et variables • 11 Noms de variables et mots reserves • 11 Affectation (ou assignation) • 12 Afficher la valeur d'une variable • 13 Typage des variables • 13 Affectations multiples • 14 Operateurs et expressions • 15 Priorite des operations • 15 Composition • 16 3. CoNTROLE DU FLUX d'eXECUTION 17 Sequence d'instructions • 17 Selection ou execution conditionnelle • 18 Operateurs de comparaison • 19 Instructions composees — blocs d'instructions • 19 Instructions imbriquees • 20 Quelques regies de syntaxe Python • 20 Les limites des instructions et des blocs sont definies par la mise en page • 20 Instruction composee : en-tete, double point, bloc d'instructions indente • 21 Les espaces et les commentaires sont normalement ignores • 21 4. Instructions repetitives 23 Re-affectation • 23 Repetitions en boucle - l'instruction while • 24 Commentaires • 24 Remarques • 25 Elaboration de tables • 25 Construction d'une suite mathematique • 25 Premiers scripts, ou comment conserver nos programmes • 27 Remarque concernant les caracteres accentues • 29 5. Principaux types de donnees 31 Les donnees numeriques • 31 Les types integer et long • 31 Le type float • 33 Les donnees alphanumeriques • 34 Le type string • 34 Remarques • 35 Triple quotes • 35 Acces aux caracteres individuels d'une chaine • 35 Limitations du type string • 36 Operations elementaires sur les chaines • 38 Les listes (premiere approche) • 39 6. Fonctions PREDEFINES 43 Interaction avec l'utilisateur : la fonction inputQ • 43 Remarques importantes • 43 Importer un module de fonctions • 44 Un peu de detente avec le module turtle • 45 Veracite/faussete d'une expression • 46 Revision • 47 Controle du flux - utilisation d'une liste simple • 47 Boucle while - instructions imbriquees • 48 7. Fonctions originales 51 Definir une fonction • 51 Fonction simple sans parametres • 52 Fonction avec parametre • 53 Utilisation d'une variable comme argument • 54 Remarque importante • 54 Fonction avec plusieurs parametres • 54 Notes • 55 Variables locales, variables globales • 55 Vraies fonctions et procedures • 57 Notes • 58 Utilisation des fonctions dans un script • 59 Notes • 59 Modules de fonctions • 60 Typage des parametres • 64 Valeurs par defaut pour les parametres • 64 Arguments avec etiquettes • 65 8. Utilisation de fenetres et de graphismes ... 67 Interfaces graphiques (GUI) • 67 Premiers pas avec Tkinter • 67 Examinons a present plus en detail chacune des lignes de commandes executees • 68 XVI Apprendre d programmer avec Python Programmes pilotes par des evenements • 70 Exemple graphique : trace de lignes dans un canevas • 72 Exemple graphique : deux dessins alternes • 74 Exemple graphique : calculatrice minimaliste • 76 Exemple graphique : detection et positionnement d'un clic de souris • 78 Les classes de widgets Tkinter • 79 Utilisation de la methode grid() pour controler la disposition des widgets • 80 Composition d'instructions pour ecrire un code plus compact • 83 Modification des proprietes d'un objet - Animation • 85 Animation automatique - Recursivite • 88 9. Manipuler des fichiers 91 Utilite des fichiers • 91 Travailler avec des fichiers • 92 Noms de fichiers — le repertoire courant • 93 Les deux formes d'importation • 93 Ecriture sequentielle dans un fichier • 94 Notes • 95 Lecture sequentielle d'un fichier • 95 Notes • 95 L'instruction break pour sortir d'une boucle • 96 Fichiers texte • 97 Remarques • 98 Enregistrement et restitution de variables diverses • 98 Gestion des exceptions : les instructions try — except - else • 99 10. Approfondir LES STRUCTURES DE DONNEES ... 103 Le point sur les chaines de caracteres • 103 Indicage, extraction, longueur •103 Les types string et unicode • 104 L'encodage Utf-8 • 105 Conversion (encodage/decodage) des chaines • 106 Conversion d'une chaine string en chaine unicode • 106 Conversion d'une chaine unicode en chaine string* 107 Conversion d'une chaine Utf-8 en Latin-1, ou vice-versa • 107 Quand faut-il convertir ? • 108 Extraction de fragments de chaines • 108 Concatenation, repetition • 109 Parcours d'une sequence : l'instruction for... in... • 110 Appartenance d'un element a une sequence : l'instruction in utilisee seule • 112 Les chaines sont des sequences non modifiables • 112 Les chaines sont comparables • 113 Classement des caracteres • 113 Les chaines sont des objets • 114 Fonctions integrees • 1 1 6 Formatage des chaines de caracteres • 117 Le point sur les listes • 118 Definition d'une liste - acces a ses elements • 119 Les listes sont modifiables • 119 Les listes sont des objets • 119 Techniques de slicing avance pour modifier une liste • 121 Insertion d'un ou plusieurs elements n'importe ou dans une liste • 121 Suppression / remplacement d'elements • 121 Creation d'une liste de nombres a l'aide de la fonction range() • 122 Parcours d'une liste a l'aide de for, range() etlen() • 122 Une consequence du typage dynamique • 123 Operations sur les listes • 123 Test d'appartenance • 123 Copie d'une liste • 123 Petite remarque concernant la syntaxe • 124 Nombres aleatoires — histogrammes • 125 Tirage au hasard de nombres entiers • 127 Les tuples • 128 Les dictionnaires • 129 Creation d'un dictionnaire • 129 Operations sur les dictionnaires • 130 Les dictionnaires sont des objets • 130 Parcours d'un dictionnaire • 131 Les cles ne sont pas necessairement des chaines de caracteres • 132 Les dictionnaires ne sont pas des sequences • 132 Construction d'un histogramme a l'aide d'un dictionnaire • 1 33 Controle du flux d'execution a l'aide d'un dictionnaire • 1 34 11. Classes, objets, attributs 137 Utilite des classes • 137 Definition d'une classe elementaire • 138 Attributs (ou variables) d'instance • 140 Passage d'objets comme arguments lors de l'appel d'une fonction • 140 Similitude et unicite • 141 Objets composes d'objets • 142 Objets comme valeurs de retour d'une fonction • 143 Modification des objets • 143 12. Classes, methodes, heritage 145 Definition d'une methode • 145 Definition concrete d'une methode dans un script • 146 Essai de la methode, dans une instance quelconque • 147 La methode constructeur • 147 Exemple • 148 Espaces de noms des classes et instances • 150 Heritage • 151 Heritage et polymorphisme • 153 Commentaires • 154 Modules contenant des bibliotheques de classes • 157 13. Classes et interfaces graphiques 161 Code des couleurs : un petit projet bien encapsule • 161 Cahier des charges de notre programme • 161 Table des matieres XVII Mise en oeuvre concrete • 1 62 Commentaires • 163 Petit train : heritage, echange d'informations entre classes • 164 Cahier des charges • 165 Implementation • 1 65 Commentaires • 166 OscilloGraphe : un widget personnalise • 168 Experimentation • 1 69 Cahier des charges • 170 Implementation • 1 70 Curseurs : un widget composite • 172 Presentation du widget Scale • 172 Construction d'un panneau de controle a trois curseurs • 173 Commentaires • 1 74 Propagation des evenements • 176 Integration de widgets composites dans une application synthese • 176 Commentaires • 178 14. Et pour quelques widgets de plus 185 Les boutons radio • 185 Commentaires • 186 Utilisation de cadres pour la composition d'une fenetre • 187 Commentaires • 188 Comment deplacer des dessins a l'aide de la souris • 189 Commentaires • 191 Python Mega Widgets • 191 Combo Box* 192 Commentaires • 193 Remarque concernant l'entree de caracteres accentues • 193 Scrolled Text • 194 Gestion du texte affiche • 194 Commentaires • 195 Scrolled Canvas • 196 Commentaires • 199 Barres d'outils avec bulles d'aide - expressions lambda • 199 Metaprogrammation — expressions lambda • 201 Fenetres avec menus • 202 Cahier des charges de l'exercice • 203 Premiere ebauche du programme • 203 Analyse du script • 204 Ajout de la rubrique Musiciens • 205 Analyse du script • 206 Ajout de la rubrique Peintres • 207 Analyse du script • 207 Ajout de la rubrique Options • 208 Menu avec cases a cocher • 208 Menu avec choix exclusifs • 209 Controle du flux d'execution a l'aide d'une liste • 210 Pre-selection d'une rubrique • 21 1 15. Analyse de programmes concrets 213 Jeu des bombardes • 213 Prototypage d'une classe Canon • 216 Commentaires • 218 Ajout de methodes au prototype • 21 8 Commentaires • 219 Developpement de l'application • 220 Commentaires • 224 Developpements complementaires • 225 Commentaires • 228 Jeu de Ping • 229 Principe • 229 Programmation • 230 Cahier des charges du logiciel a developper • 230 16. Gestion d'une base de donnees 235 Les bases de donnees • 235 SGBDR - Le modele client/ serveur • 236 Le langage SQL — Gadfly • 236 Mise en ceuvre d'une base de donnees simple avec Gadfly • 237 Creation de la base de donnees • 237 Connexion a une base de donnees existante • 238 Recherches dans une base de donnees • 239 La requete select • 241 Ebauche d'un logiciel client pour MySQL • 241 Decrire la base de donnees dans un dictionnaire d'application • 242 Definir une classe d'objets-interfaces • 244 Commentaires • 245 Construire un generateur de formulaires • 246 Commentaires • 247 Le corps de l'application • 248 Commentaires • 248 17. Applications web 251 Pages web interactives • 251 L'interface CGI • 252 Une interaction CGI rudimentaire • 252 Un formulaire HTML pour l'acquisition des donnees • 254 Un script CGI pour le traitement des donnees • 254 Un serveur web en pur Python ! • 256 Installation de Karrigell • 257 Demarrage du serveur • 257 Ebauche de site web • 258 Prise en charge des sessions • 260 Exemple de mise en ceuvre • 261 Autres developpements • 263 18. Communications A travers un reseau 265 Les sockets • 265 Construction d'un serveur elementaire • 266 Commentaires • 267 Construction d'un client rudimentaire • 268 Commentaires • 268 Gestion de plusieurs taches en parallele a l'aide des threads • 269 Client gerant l'emission et la reception simultanees • 270 Commentaires • 271 Serveur gerant les connexions de plusieurs clients en parallele • 272 Commentaires • 273 Jeu des bombardes, version reseau • 274 Programme serveur : vue d'ensemble • 275 XVIII Apprendre d programmer avec Python Protocole de communication • 275 Remarques complementaires • 276 Programme serveur : premiere partie • 277 Synchronisation de threads concurrents a l'aide de verrous (thread locks) • 279 Utilisation • 280 Programme serveur : suite et fin • 280 Commentaires • 283 Programme client • 283 Commentaires • 286 Conclusions et perspectives : • 286 Utilisation de threads pour optimiser les animations. • 287 Temporisation des animations a Faide deafterO • 287 Temporisation des animations a Faide de time.sleepO • 288 Exemple concret • 288 Commentaires • 290 Annexe A. Installation de Python 291 Sous Windows • 291 Sous Linux • 291 Sous Mac OS • 291 Installation des Python mega-widgets • 291 Installation de Gadfly (systeme de bases de donnees) • 292 Sous Windows • 292 Sous Linux • 292 Annexe B. Solutions des exercices 293 Index 339 1 Penser comme un programmeur Nous allons introduire dans ce chapitre quelques concepts qu'il vous faut connaitre avant de vous lancer dans I'apprentissage de la programmation. Nous avons volontairement limite nos explications afin de ne pas vous en- combrer 1' esprit, lui programmation n 'est pas vraiment difficile, mais elle exige de la methode et une bonne dose de perseverance. La demarche du programmeur Le but de ce cours est de vous apprendre a penser et a 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 raison- nements (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 com- portement de systemes complexes, il ebauche des hypotheses explicatives, il teste des predictions. L'activite essentielle d'un analyste-programmeur consiste a resoudre des 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 d'instructions, encodees en respectant de maniere tres stricte un ensemble de conventions fixees a l'avance que Ton appelle un langage informatique. La ma- chine 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 contri- bue a developper votre intelligence. Mais vous serez aussi amene a utiliser la programmation pour reali- ser des projets concrets, ce qui vous procurera certainement de tres grandes satisfactions. 2 Apprendre a programmer avec Python 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 convention- nellement comme des suites de nombres ne prenant jamais que les deux valeurs 0 et 1. Un systeme nu- merique 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 souhaite traiter (les textes, les images, les sons, les nombres, etc.), mais aussi pour les programmes, e'est-a-dire les se- quences destructions que Ton va fournir a la machine pour lui dire ce qu'elle doit faire avec ces don- nees. 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 presque incomprehen- sible pour nous. Pour « parler » a un ordinateur, il nous faudra utiliser des systemes de traduction auto- matiques, capables de convertir en nombres binaires des suites de caracteres formant des mots-cles (an- glais 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 me- thode 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 : assem- bleur) ou « de haut niveau » (ex : Pascal, Perl, Smalltalk, Clarion, Lisp...). Un langage de bas niveau est constitue d'instructions tres elementaires, tres « proches de la machine ». Un langage de haut niveau comporte des instructions plus abstraites, plus « puissantes ». Cela signifie que chacune de ces instruc- tions pourra etre traduite par l'interpreteur ou le compilateur en un grand nombre d'instructions ma- chine elementaires. Le langage que vous allez apprendre en premier est Python. II s'agit d'un langage de haut niveau, dont la traduction en code binaire 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 probabilite 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 facilitees. De plus, un programme ecrit dans un langage de haut niveau sera souvent portable, e'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 en- tierement. 1 . Penser comme un programmeur 3 Compilation et interpretation Le programme tel que nous l'ecrivons a l'aide d'un logiciel editeur (une sorte de traitement de texte spe- cialise) 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 bi- naire executable par la machine : l'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 pro- gramme 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 L interpreteur lit le code source ... ... et le resultat apparait sur lecran. 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 pro- gramme objet (ou code objet). Celui-ci peut desormais etre execute independamment du compila- teur et etre conserve tel quel dans un fichier (« fichier executable »). Cod e source Code objet Resultat Le compilateur lit le code source ... ... et produit un code objet (binaire). On execute le code objet . ... et le resultat apparait a lecran. 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'experi- mentation sur un projet. Avec cette technique, on peut en effet tester immediatement toute modifica- tion 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 net- tement plus vite que son homologue interprete, puisque dans cette technique l'ordinateur n'a plus a (re)traduire chaque instruction en code binaire avant qu'elle puisse etre executee. Certains langages modernes tentent de combiner les deux techniques afin de retirer le meilleur de cha- cune. 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 l'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. 4 Apprendre a programmer avec Python Les avantages de cette methode sont appreciates : Code source I ^CompilateurJ I ByteCode (interpreteur) Resultat Le compilateur Python lit le code source ... ... etproduit un pseudo- code intermediaire. L 'interpreteur Python lit le pseudo-code ... ... et le resultat apparait a I'ecran. • 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 com- posant 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 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. Mise au point d'un programme - Recherche des erreurs (debug) La programmation est une demarche tres complexe, et comme e'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 Francais) 2 , et l'ensemble des techniques que Ton met en ceuvre pour les detecter et les corriger s'appelle « debug » (ou « debogage »). En fait, il peut exister dans un programme trois types d'erreurs assez differentes, et il convient que vous appreniez a bien les distinguer. 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 generale- ment 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 pre- mieres semaines de votre carriere de programmeur, vous passerez certainement pas mal de temps a re- chercher vos erreurs de syntaxe. Avec de l'experience, vous en commettrez beaucoup moins. 2 bug est a l'origine un terme anglais servant a designer de petits 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 petits 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 « hogue » a ete choisi par homonymie approximative. II designe la coque epineuse de la chataigne. 1 . Penser comme un programmeur 5 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 scrupuleusement la syntaxe du langage. II est heureux que vous fassiez vos debuts en programmation avec un langage interprete tel que Python. La recherche des erreurs y est facile et rapide. Avec les langages compiles (tel C++), il vous faudrait re- compiler l'integralite du programme apres chaque modification, aussi minime soit-elle. 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 mes- sage 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 chaque instruction. Erreurs a l'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 appe- lees 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. 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 l'impossible, 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. 6 Apprendre a programmer avec Python Par exemple, vous savez que Linux est un systeme d'exploitation (et done un gros logiciel) qui com- porte 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. D'apres Larry Greenfield (« The Linux user's guide », beta version 1) : « L'un des premiers projets de Li- nus etait un programme destine a convertir une chaine de caracteres AAAA en BBBB. C'est cela qui plus tard finit par devenir Linux ! ». Ce qui precede ne signifie pas que nous voulions vous pousser a programmer par approximations suc- cessives, a partir d'une vague idee. Lorsque vous demarrerez un projet de programmation d'une cer- taine 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 rapplication envisagee. Diverses methodes existent pour effectuer cette tache d'analyse, mais leur etude sort du cadre de ces notes. Nous vous presenterons cependant plus loin (voir chapitre 15) quelques idees de base. 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 emploient 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 G 2 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 combi- ner. 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 li- sez 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 : Ambiguite. 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 'ambiguite. Chaque instruction possede une seule signification, independante du contexte. 1 . Penser comme un programmeur Redondance. Pour compenser toutes ces ambigui'tes et aussi de nombreuses erreurs ou pertes dans la transmission de 1' 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. 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 impor- tante. 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. 2 Premiers pas II est temps de nous mettre au travail. Plus exactement, nous allons prendre le commandement de I'ordinateur et lui demander de travailler a notre place, en lui donnant I'ordre, par exemple, d'effectuer une addition et d'en affi- cher le resultat. Pour cela, nous allons devoir lui transmettre des « instructions », et egalement lui indiquer les « donnees » aux- quelles nous voulons appliquer ces instructions. Calculer avec Python Python presente la particularity 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 dialo- guer 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 prefe- rerez vraisemblablement travailler dans une « fenetre de terminal », ou encore dans un environnement de travail specialise tel que IDLE. Void par exemple ce qui apparait dans une fenetre de terminal Gnome (sous Ubuntu Linux) ' : Fichier Edition Affichaqe Terminal Onglets Aide gust I 3 Sous Windows, vous aurez surtout le choix entre 1' 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 fa9on 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 une simple fenetre de terminal pour lancer l'interpreteur Python ou l'execution des scripts, et faire appel a un editeur de texte ordinaire tel que Gedit, Kate, ou un peu plus specialise comme SciTE, DrPython ou Geany pour l'edition de ces derniers (voir Annexe, page 291). Apprendre a programmer avec Python Avec IDLE sous Windows, votre environnement de travail ressemblera a celui-ci 'Python Shell Windows Help Python 2.2 (#28, Dec 21 2001, 12:21:22) [MSC 32 bit (Intel)] on Win32 Type "copyright", "credits" or "license" for more information. IDLE 0.8 — press Fl for help I Les trois caracteres « superieur a » constituent le signal d'invite, ou prompt principal, lequel vous in- dique 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 cabier 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 multi- plication 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 argu- ments 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 4 . Essayez par exemple (comparez les resultats avec celui obtenu a l'exercice precedent 5 ) : »> 20.0 / 3 »> 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 4 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 rinformatique, les nombres reels sont souvent designes comme des nombres « a virgule flottante », ou encore des nombres « de type float ». 5 Ce comportement par defaut de l'operateur de division / sous Python sera probablement modifie dans les versions futures du langage. II est propose en effet que le comportement par defaut de cet operateur consistera a l'avenir a effectuer plutot une division reelle, alors que la division entiere ne sera plus obtenue qu'a l'aide du nouvel operateur // (qui fonctionne deja). De ce fait, si vous souhaitez effectivement obtenir une division entiere, il vous est conseille d'utiliser de preference ce dernier. 2. Premiers pas 11 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 6 ), mais dans la memoire de l'or- dinateur 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 quel- conque (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 stockee une valeur bien determinee. 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. 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 tels que altitude, altit ou alt conviennent mieux que x 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 tou- jours commencer par une lettre. • Seules les lettres ordinaires sont autorisees. Les lettres accentuees, les cedilles, les espaces, les carac- teres 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 pre- miere lettre 7 ). II s'agit d'une simple convention, mais elle est largement respectee. N'utilisez les majus- cules qu'a l'interieur meme du nom, pour en augmenter eventuellement la lisibilite, comme dans tableDesMatieres. b Que peut-on numeriser au juste ? Voila une question tres importante, qu'il vous faudra debattre dans votre cours d'infomiatique generate. 7 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 cet ouvrage). II arrive aussi que Ton ecrive entierement en majuscules certaines variables que Ton souhaite traiter comme des pseudo- constantes (c'est-a-dire des variables que Ton evite de modifier au cours du programme). 12 Apprendre a programmer avec Python En plus de ces regies, il faut encore ajouter que vous ne pouvez pas utiliser comme nom de variables les 30 « 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 Affectation (ou assignation) Nous savons desormais comment choisir judicieusement un nom de variable. Voyons a present com- ment nous pouvons definir une variable et lui affecter une valeur. Les termes « affecter une valeur » ou « assigner une valeur » a une variable sont equivalents. Ils 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* : »> 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 1 1 i 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 8 I1 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). 2. Premiers pas 13 emplacements parfois fort eloignes les uns des autres. Nous aurons l'occasion de preciser ce concept plus loin dans ces pages. 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'ins- truction 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). Veuillez aussi ignorer pour l'instant les bizarreries qui se produisent si vous modifiez l'exercice prece- dent en choisissant une chaine de caracteres qui contient des lettres accentuees, comme dans l'exemple ci-dessous : »> msg = "Mon prenom est Chimene" »> msg 'Mon pr\xe9nom est Chim\xe8ne' Les lettres accentuees du francais (de meme que les lettres d'autres alphabets, tels le grec, l'arabe, le cy- rillique, l'hebreu, etc.), ne font pas partie du jeu de caracteres standard d'un ordinateur, lequel se limite aux chiffres arabes, aux lettres majuscules et minuscules de l'alphabet latin non accentue, plus quelques signes typographiques usuels (?/ +; etc.). Leur utilisation pose done quelques problemes particuliers, qui n'ont malheureusement pas toujours ete resolus de la meme maniere au cours de revolution des sys- temes informatiques (il se peut par exemple que le resultat du test presente ci-dessus soit deja different sur votre propre ordinateur !). Nous examinerons ces questions plus loin. Pour l'instant nous devons d'abord affiner notre comprehension des variables. 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 va- riable 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 automatique- ment 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 particularite interessante de Python, qui le rattache a une famille particuliere de lan- gages ou Ton trouve aussi par exemple Lisp, Scheme, et quelques autres. On dira a ce sujet que le ty- page des variables sous Python est un typage dynamique, par opposition au typage statique qui est de 14 Apprendre a programmer avec Python regie par exemple en C++ ou en Java. Dans ces langages, il faut toujours - par des instructions dis- tinctes - 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'ope- ration de compilation (dont le resultat est un code binaire « fige »). Le typage dynamique quant a lui permet d'ecrire plus aisement des constructions logiques de niveau eleve (metaprogrammation, reflexivite), en particulier dans le contexte de la programmation orientee objet (polymorphisme). II facilite egalement rutilisation de structures de donnees tees riches telles que les listes et les dictionnaires. Affectations multiples Sous Python, on peut assigner une valeur a plusieurs variables simultanement. Exemple : »> x = y = 7 »> x 7 »> y 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. Remarque 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-a-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. 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 ? 2. Premiers pas 15 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 multipli- cation. 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'ex- pression 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 ope- rateurs agissant sur les chaines de caracteres, des operateurs effectuant des tests d'identite ou d'apparte- nance, etc. Nous reparlerons de tout cela plus loin. Signalons au passage la disponibilite de l'operateur modulo, represente par le symbole %. Cet operateur fournit le reste de la division entiere d'un nombre par un autre. Essayez par exemple : »> 10 \ i 3 # (et prenez note de ce qui se passe ! ) »> 10 % i 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.) Priorite des operations Lorsqu'il y a plus d'un operateur dans une expression, l'ordre dans lequel les operations doivent etre ef- fectuees 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 aisement 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 (l+l) ** (5-2) = 8. 16 Apprendre a programmer avec Python • E pour exposants. Les exposants sont evalues ensuite, avant les autres operations. Ainsi 2**1+1 = 3 (et non 4), et 3*1**10 = 3 (et non 59049 !). • M et D pour multiplication et division, qui ont la meme priorite. Elles sont evaluees avant I'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 = -l (rappelez-vous aussi, pour ce dernier exemple, 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). Composition Jusqu'ici nous avons examine les differents elements d'un langage de programmation, a savoir : les va- riables, les expressions et les instructions, mais sans traiter de la maniere dont nous pouvons les combi- ner les unes avec les autres. Or l'une 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 l'air 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 ma- thematique : comme nous l'avons deja signale, il s'agit d'un symbole d'affectation (nous placons un cer- tain contenu dans une variable) et non un symbole d'egalite. Le symbole d'egalite (dans un test condi- tionnel, par exemple) sera evoque un peu plus loin. Ainsi par exemple, l'instruction m + l = b est tout a fait illegale. Par contre, ecrire a = a + l est inacceptable en mathematique, alors que cette forme d'ecriture est tees frequente en programmation. L'instruction a = a + l 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. 3 Controle du flux d' execution Dans notre premier chapitre, nous avons vu que I'activite essentielle d'un analysteprogrammeur est la resolution de problemes. Or, pour resoudre un probleme informatique, il faut toujours effectuer une serie d'actions dans un certain ordre. La description structuree de ces actions et de I'ordre dans lequel il convient de les effectuer s'appelle un algorithme. Le « chemin » suivi par Python a travers un programme est appele un flux d 'execution, et les constructions qui le modifient sont appelees des instructions de controle de flux. Les structures de controle sont les groupes d'instructions qui determinent I'ordre dans lequel les actions sont ef- fectuees. En programmation moderne, il en existe seulement trois : la sequence 9 et la selection, que nous allons decrire dans ce chapitre, et la repetition que nous aborderons au chapitre suivant. Sequence d'instructions Sauf mention explicite, les instructions d'un programme s'executent les unes apres les autres, dans I'ordre oil elles ont ete ecrites a I'interieur du script. Cette affirmation peut vous paraitre banale et evidente a premiere vue. L'experience montre cependant qu'un grand nombre d'erreurs semantiques dans les programmes d'ordinateur sont la consequence d'une mauvaise disposition des instructions. Plus vous progresserez dans Part de la programmation, plus vous vous rendrez compte qu'il faut etre extremement attentif a I'ordre dans lequel vous placez vos instructions les unes derriere les autres. Par exemple, dans la sequence d'instructions suivantes : »> a, b = 3, 7 »> a = b »> b = a »> print a, b Vous obtiendrez un resultat contraire si vous intervertissez les 2 e et 3 e lignes. 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. 9 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 chaines de caracteres, les tuples et les listes (voir plus loin). 18 Apprendre a programmer avec Python Selection ou execution conditionnelle Si nous voulons pouvoir ecrire des applications veritablement utiles, il nous faut des techniques permet - tant 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 condi- tion et de modifier le comportement du programme en consequence. La plus simple de ces instructions conditionnelles est l'instruction if. Pour experimenter son fonction- nement, 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 finis sez 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 constitue de trois points 10 . Si votre editeur ne le fait pas automatiquement, vous devez a present effectuer une tabulation (ou en- trer 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 : >» 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'af- fiche plus rien. L'expression que vous avez placee entre parentheses est ce que nous appellerons desormais une condi- tion. L'instruction if permet de tester la validite de cette condition. Si la condition est vraie, alors l'ins- truction 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 qua- trieme ligne debute tout a fait a gauche (pas d'indentation), mais que la cinquieme soit a nouveau inden- tee (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 pro- grammer 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 ») : 10 Dans certaines versions de l'editeur Python pour Windows, le prompt secondaire n'apparait pas. 3. Controle du flux d' execution 19 »> a = 0 >» if a > 0 : print "a est positif " . . . elif a < 0 : . . . print "a est negatif " . . . else : print "a est nul" 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 constitue de deux signes « egale » et non d'un seul 11 . Le signe « egale » utilise seul est un operateur d'affectation, et non un opera- teur de comparaison. Vous retrouverez le meme symbolisme en C++ et en Java. Instructions composees - blocs constructions La construction que vous avez utilisee avec l'instruction if est votre premier exemple d'instruction composee. Vous en rencontrerez bientot d'autres. Sous Python, les instructions composees ont tou- jours 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'instruc- tions 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. n 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. 20 Apprendre a programmer avec Python 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 print" 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 pre- mieres 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 et 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 . Quelques regies de syntaxe Python Tout ce qui precede nous amene a faire le point sur quelques regies de syntaxe : 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 ca- ractere special (souvent le point- virgule). Sous Python, c'est le caractere de fin de ligne 12 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 speci- fiques (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 ni 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. 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. 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 97). 3. Controle du flux d' execution 21 Instruction composee : en-tete, double point, bloc d'instructions indente Nous aurons de nombreuses occasions d'approfondir le concept de « bloc d'instructions » 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, etc.) se terminant par un double point. • Les blocs sont delimites par I 'indentation : toutes les lignes d'un meme bloc doivent etre indentees exactement de la meme maniere (c'est-a-dire decalees vers la droite d'un meme nombre d'espaces). Le nombre d'espaces a utiliser pour l'in- dentation 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 (il n'est im- brique dans rien). Bloc 1 Ligne d'en -tete : Bloc 2 Ligne d'en-tete : Bloc 3 Bloc 2 (suite) Bloc 1 (suite) Remarque Vous pouvez aussi indenter a I'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 I'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 I'option « Remplacer les tabulations par des espaces ». 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. 4 Instructions repetitives Uune des tdches que les machines font le mieux est la repetition sans erreur de tdches identiques. II existe bien des methodes pour programmer ces tdches repetitives. Nous allons commencer par I'une des plus fondamentales : la boucle de repetition construite autour de ['instruction while. 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 af- firmation d'egalite, mais ce n'en 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 dif ferentes 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 l'air a premiere vue. Supposons par exemple que nous voulions maintenant echanger les valeurs des variables a et c (actuel- lement, a contient la valeur 3, et c la valeur 5 ; nous voudrions que ce soit l'inverse). Comment faire ? 24 Apprendre a programmer avec Python Exercice 4.1 Ecrivez les lignes d'instructions necessaires pour obtenir ce resultat. A la suite de l'exercice propose ci-dessus, vous aurez certainement trouve une methode, et un profes- seur vous demanderait certainement de la commenter en classe. Comme il s'agit d'une operation cou- rante, 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, I 'affectation pa- rallele 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.) Repetitions en boucle - l'instruction while 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 construite a partir de l'instruction while. Veuillez done entrer les commandes ci-dessous : »> a = 0 >» while (a < 7) : # (n 1 oubliez pas le double point ! ) ... a = a + 1 # (n 1 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. Commentaires Le mot while signifie « tant que » en anglais. Cette instruction utilisee a la seconde ligne indique a Py- thon qu'il lui faut repeter continuellement le bloc d'instructions qui suit, tant que le contenu de la va- riable a reste inferieur a 7. Comme l'instruction if abordee au chapitre precedent, l'instruction while amorce une instruction compo- site. Le double point a la fin de la ligne introduit le bloc d'instructions a repeter, lequel doit obligatoire- ment 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 (e'est-a-dire decalees a droite d'un meme nombre d'espaces). Nous avons ainsi construit notre premiere boucle de programmation, 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 pa- rentheses (celles-ci sont optionnelles, nous ne les avons utilisees que pour clarifier notre explica- tion). • Si la condition se revele fausse, alors tout le bloc qui suit est ignore et l'execution du programme se termine 13 . ... 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. 4. Instructions repetitives 25 • 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 (il 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 ! " 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. 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 judicieusement l'affectation parallele) et 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 26 Apprendre a programmer avec Python Lorsque vous kncez 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 tees souvent amenes a mettre au point des boucles de repeti- tion 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 pro- gramme « suite de Fibonacci » : Variables a b c Valeurs initiales 1 1 1 Valeurs prises 1 2 2 successivement, au cours des iterations 2 3 3 3 5 4 5 8 5 Expression de remplacement b a+b c+1 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 succes- sives. On commence par inscrire en haut du tableau les noms des variables concernees. Sur la ligne sui- vante, 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 va- riable 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. Instructions repetitives 27 Premiers scripts, ou comment conserver nos programmes Jusqu'a present, vous avez toujours utilise Python en mode interactif (c'est-a-dire 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 destruc- tions 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 clef USB, de maniere a pouvoir les retravailler par etapes succes- sives, les transferer sur d'autres machines, etc. Pour ce faire, vous allez desormais rediger vos sequences destructions dans un editeur de texte quel- conque (par exemple Kate, Gedit, Geany, Scite... sous Linux, Edit sous MS-DOS, Wordpad sous Win- dows, ou encore l'editeur incorpore dans une interface de developpement telle que IDLE ou Python- Win). Ainsi vous ecrirez un script, que vous pourrez ensuite sauvegarder, modifier, copier, etc. comme n'importe quel autre texte traite par ordinateur 14 . La figure ci-dessous illustre l'utilisation de l'editeur Gedit sous Gnome (Ubuntu Linux) : Eichier Edition AfTichege Bechercher Qutih Documents Aide * a . a M «» f » a. at Nouveau Ouvrir Enregistrer Imprimer Annuler Couper Coller Rechercher Remplocer ■fibol.py o 1# -"- coding: Latin-1 -"• 3# Premier essai de script Python 4# petit programme simple affichant une suite de Fibonacci, c.a.d. une suite 5# de nombres dont chaque terme est egal a la somme des deux precedents. A 7print "Suite de Fibonacci 8| 9a,b,c = 1,1,1 # a & b servent au calcul des termes successifs 10 9 c est un simple compteur 11 print 1 * affichage du premier terme 12while c<15: # nous afficherons 15 termes au total 13 a,b,c = b,a+b,c+l 14 print b Ug 8. Col 1 INS Par la suite, lorsque vous voudrez tester l'execution de votre programme, il vous suffira de lancer l'in- terpreteur 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 com- mande 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, Mac Os, ou la plupart des gestion- naires de fichiers graphiques en usage sous Linux) 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 bien Nautilus, Konqueror... sous Linux). 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 « intelli- gents » de reconnaitre automatiquement les scripts Python et d'adapter leur coloration syntaxique en consequence. 14 I1 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 « intelligent » tel que Geany, DrPython, Scite 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 tapez 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 taper Ctrl-F5). 28 Apprendre d programmer avec Python Un script Python contiendra des sequences destructions identiques a celles que vous avez experimen- tees 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 difficulte de la programmation consiste en effet a mettre au point des algorithmes corrects. Ann que ces algorithmes puissent etre verifies, corri- ges, modifies, etc. dans de bonnes conditions, il est essentiel que leur auteur les decrive le plus comple- tement 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 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 a mesure de Vavancement de votre travail de programmation. N'attendez pas que votre script soit termine pour les ajouter « 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 remarques. Ouvrez done un editeur de texte, et redigez le script ci-dessous : # -*- coding: Latin- 1 -*- # 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 La premiere ligne, un peu etrange, doit etre reproduite telle quelle. Ne vous preoccupez pas de son role pour l'instant, il sera explique un peu plus loin dans cette page. 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'ha- bitude 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 suf- fisamment de celles-ci, afin de ne pas gener leur lisibilite. Lorsque vous aurez bien verifie votre texte, sauvegardez-le et executez-le. Remarque 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 / '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 ! 4. Instructions repetitives 29 Si vous travaillez en mode texte sous Linux, ou dans une fenetre MS-DOS, 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 : python monScript.py Dans l'explorateur Windows, vous pouvez lancer l'execution de votre script en effectuant un simple clic de souris sur l'icone correspondante (en principe, si IDLE a ete installe). 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 . Dans d'autres environnements de travail specifiquement dedies a Python, tels que DrPython, Geany ou Scite, vous trouverez egalement des icones et/ ou des rac- courcis clavier pour lancer l'execution. Consultez votre professeur ou votre gourou local concernant les autres possibilites de lancement eventuelles sur differents systemes d'exploitation. Remarque concernant les caracteres accentues 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- 1 -*- Ou bien : # -*- coding:Utf-8 -*- Ces pseudo-commentaires indiquent a Python que vous utiliserez dans votre script : • soit le jeu de caracteres accentues ASCII etendu correspondant aux principales langues de l'Europe occidentale (Francais, Allemand, Portugais, etc.), encode sur un seul octet suivant la norme ISO- 8859-1, laquelle est souvent designee aussi par l'etiquette Latin-1 ; • soit un jeu de caracteres beaucoup plus etendu suivant la nouvelle norme Unicode, laquelle a ete mise au point pour standardiser la representation numerique de tous les caracteres specifiques des differentes langues mondiales. II existe plusieurs representations ou encodages de cette norme, et nous devrons approfondir cette question un peu plus loin, mais pour l'instant il vous suffit de sa- voir que l'encodage le plus repandu sur les ordinateurs recents est Utf-8. Dans ce systeme, les carac- teres standard (ASCII) sont encore encodes sur un seul octet, ce qui assure une certaine compatibili- te avec le codage Latin-1, mais les autres caracteres (parmi lesquels nos caracteres accentues) peuvent etre encodes sur 2, 3 ou meme parfois 4 octets. Python peut traiter tout a fait correctement les caracteres de ces deux systemes, mais vous devez lui si- gnaler lequel vous utilisez a l'aide d'un pseudo-commentaires en debut de script. Si vous ne le faites pas, vos scripts ne pourront pas s'executer, ou alors ils fourniront des resultats etranges et incorrects. Supprimez par exemple la premiere ligne du script precedent, puis lancez a nou- veau son execution. Avec une version recente de Python, vous obtiendrez un message d'erreur similaire a celui-ci : SyntaxError: Non-ASCII character ' \xeO ' in file fibo2.py on line 2, but no encoding declared; see http://www.python.org/peps/pep-0263.html for details 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-1), vous devrez utiliser plutot le premier pseudo-commentaire. 30 Apprendre d programmer avec Python Si vous ignorez le mode d'encodage par defaut de votre systeme d'exploitation, vous pouvez vous ser- vir de Python pour le decouvrir. Lancez votre interpreteur python, puis entrez a la ligne de commande une simple instruction d'assignation telle que prenom = "Gerard", par laquelle vous affectez a la variable prenom une chaine de caracteres contenant au moins une lettre accentuee. Entrez ensuite le nom de cette variable, suivi de (rappelez-vous qu'a la ligne de commande, vous pouvez obtenir ainsi de l'interpreteur qu'il vous indique le contenu detaille d'une variable quelconque). Si vous obtenez un resultat tel que : »> prenom = "Gerard" »> prenom 'G\xc3\xa9rard' »> cela signifie que votre systeme d'exploitation utilise l'encodage utf-8. Python vous indique ainsi, en ef- fet, que le second caractere de la chaine « Gerard » est un caractere non-ASCII encode sur deux octets, represented ici en format hexadecimal, respectivement par \xc3 et \xa9. Par contre, si vous obtenez le resultat suivant : »> prenom = "Gerard" »> prenom 'G\xe9rard' >» cela signifie que votre systeme d'exploitation utilise toujours la norme ancienne Latin-1. Python vous in- dique en effet que la meme lettre accentuee est encodee cette fois a l'aide d'un seul octet : \xe9. Nous devrons approfondir encore cette question des differents encodages, lorsque nous etudierons un peu plus en detail le traitement des chaines de caracteres (voir le chapitre suivant, page 34). Exercices 4.5 Ecrivez un programme qui calcule le volume d'un parallepipede rectangle dont sont fournis au depart la largeur, la hauteur et la profondeur. 4.6 Ecrivez un programme qui convertit 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 : * ** *** **** 5 Principaux types de donnees Dans le chapitre 2, nous avons dejd manipule des donnees de diffe'rents types : des nombres entiers ou reels, et des chaines de caracteres. II est temps d present d'examiner d'un peu plus pres ces types de donnees, et egalement de vous en faire decouvrir d'autres. 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). Tachons de mettre en evidence les caracteristiques (et les limites) de ces concepts. 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 princi- pale : »> 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 pro- bleme (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 implicite- ment comme etant du type integer. C'est ce qui se passe toujours avec Python lorsqu'on affecte une va- 32 Apprendre d programmer avec Python leur 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. Les calculs effectues avec ce type de variable sont toujours tres rapides, parce que le processeur de l'or- dinateur 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. 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'oc- cupation 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 mention- nees 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 de- signe 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'etant 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 nume- riques entieres : 3, 2 et l. A partir de la 8 e iteration, cependant, les variables b et a sont automatique- ment 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. 5. Principaux types de donnees 33 La progression continue avec des nombres de plus en plus gigantesques, mais la vitesse de calcul dimi- nue. Les nombres memorises sous le type long occupent une place variable dans la memoire de l'ordi- nateur, en fonction de leur taille. 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 tees grands ou tees petits 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 hui- tieme 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 depassement 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) com- pris entee 10~ 308 et 10 3 " 8 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 corres- pond aux 12 chiffres significatifs, et une autre a l'ordre de grandeur (exposant de 10). 34 Apprendre a programmer avec Python 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 I'inverse. La formule de conversion est : T P =T C X 1,8+32 . 5.4 Ecrivez un programme qui calcule les interets accumules chaque annee pendant 20 ans, par ca- pitalisation 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 recom- pense. Le vieux sage demanda qu'on lui fournisse simplement un peu de riz pour ses vieux jours, et plus precisement 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 troi- sieme, 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). Les donnees alphanumeriques Jusqu'a present nous n'avons manipule que des nombres. Mais un programme d'ordinateur peut egale- ment traiter des caracteres alphabetiques, des mots, des phrases, ou des suites de symboles quel- conques. Dans la plupart des langages de programmation, il existe pour cet usage des structures de don- nees particulieres que Ton appelle « chaines de caracteres ». Sous Python, il existe deux structures de donnees distinctes pour traiter les chaines de caracteres : le type string et le type Unicode. Le type string est le plus fondamental, et c'est celui que nous utiliserons le plus souvent dans ce cours. Le type Unicode est plus elabore : il a ete invente afin de simplifier le traite- ment de tous les caracteres d'ecriture utilises dans le monde entier, les accents, les symboles mathema- tiques, etc. Si votre systeme d'exploitation est encore configure de maniere a utiliser l'encodage Latin-1 par defaut (voir le chapitre precedent, page 30), vous pouvez probablement plus ou moins ignorer le type Unicode, tout au moins a vos debuts. Si vous utilisez un ordinateur recent, par contre, il y a fort a parier que votre systeme d'exploitation utilise la nouvelle norme Utf-8 par defaut (ce qui est par ailleurs une excellente chose) et, dans ce cas, vous devrez le plus tot possible apprendre a distinguer les deux types, a les traiter correctement, a convertir les donnees d'un type a 1' autre, et inversement. Le type string Une donnee de type string peut se definir en premiere approximation comme une suite quelconque de caracteres. Dans un script python, on peut delimiter une telle suite de caracteres, soit par des apos- trophes (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. 5. Principaux types de donnees 35 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 rutilisation des apostrophes pour delimiter une chaine qui contient des guillemets. Remarquez aussi en- core 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 facon 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. De la meme maniere, la sequence \" permet d'inserer des guillemets dans une chaine delimitee elle- meme par des guillemets. • Rappelons encore ici que la casse est significative dans les noms de variables (il faut respecter scru- puleusement le choix initial de majuscules ou minuscules). Triple quotes Pour inserer plus aisement des caracteres speciaux ou « exotiques » dans une chaine, sans faire usage de Yantislash, 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 »> print al Usage: trucmuche [OPTIONS] { -h -H hote } Acces aux caracteres individuels d'une chaine Les chaines de caracteres constituent un cas particulier d'un type de donnees plus general que Ton ap- pelle des donnees composites. Une donnee composite est une entite qui rassemble dans une seule struc- ture un ensemble d'entites plus simples : dans le cas d'une chaine de caracteres, par exemple, ces entites plus simples sont evidemment les caracteres eux-memes. En fonction des circonstances, nous souhaite- 36 Apprendre a programmer avec Python rons traiter la chaine de caracteres, tantot comme un seul objet, tantot comme une collection de carac- teres 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 tou- jours 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 a partir de zero (et non a partir de un). C'est le cas pour les carac- teres d'une chaine. Exemple : »> ch = "Christine" »> print ch[0], ch[3], ch[5] C i t Limitations du type string Veuillez done recommencer l'exercice de l'exemple ci-dessus, mais en utilisant cette fois un ou deux ca- racteres « non-ASCII », tels que lettres accentuees, cedilles, etc. Les choses se compliquent quelque peu, suivant que votre systeme d'exploitation utilise l'encodage par defaut Latin-1 ou Utf-8. Si votre ordinateur utilise l'encodage Latin-1, tout semble se passer comme prevu : »> ch ="Noel en Decembre" »> »> print oh[l] ,ch[2] ,ch[3] ,ch[4] ,ch[8] ,ch[9] ,ch[10] ,ch[ll] ,ch[12] oel Decern Par contre, si votre utilisateur utilise l'encodage Utf-8, les resultats deviennent bizarres : »> ch ="Noel en Decembre" »> print ch[l] ,ch[2] ,ch[3] ,ch[4] ,ch[8] ,ch[9] ,ch[10] ,ch[ll] ,ch[12] o^^l D^^c Dans la memoire d'un ordinateur utilisant l'encodage Latin-1 , chaque caractere alphanumerique est re- presente par un seul octet. Sachant qu'un octet peut representer 256 valeurs differentes, vous compre- nez aisement qu'il est possible d'encoder sur un seul octet, non seulement l'ensemble des caracteres standards ASCII (caracteres non accentues et chiffres, plus quelques autres symboles typographiques courants, encodes avec les valeurs d'octet de 0 a 127), mais aussi un certain nombre de caracteres plus particuliers, malheureusement pas toujours les memes d'une region du monde a l'autre (encodes avec les valeurs d'octet de 128 a 255). Dans le cas de la norme ISO-8859-1 ou Latin-1 qui nous interesse ici, ces caracteres supplementaires sont les principaux caracteres accentues et symboles divers utilises en Europe occidentale. Cette norme permet done l'encodage correct de textes courants ordinaires dans notre langue, mais elle interdit d'y incorporer par exemple du grec, de l'arabe, du russe, du japonais, etc., ainsi qu'un grand nombre de symboles mathematiques ou techniques. L'encodage Latin-1 n'est done en definitive qu'une mediocre amelioration de l'encodage standard ASCII, vaguement adapte a un groupe de langues particulieres (francais, espagnol, allemand...) grace a l'appoint d'une petite centaine de caracteres, mais aux possibilites tout de meme tres limitees. En fait, le seul interet residuel de cette norme ancienne est sa simplicite. Suivant cette convention, en effet, les chaines de caracteres ne sont rien d'autre que des sequences d'octets, et leur traitement informatique reste par consequent assez simple. 5. Principaux types de donnees 37 A l'heure actuelle, nous ne pouvons cependant plus nous satisfaire de cette simplicite trompeuse. Les ordinateurs ont envahi tous les secteurs d'activite, et le monde est devenu un village. II nous faut done integrer l'idee que les diverses suites de symboles ingurgites par nos ordinateurs (e'est-a-dire non seule- ment les textes qui melangeront differentes langues, mais aussi les equations mathematiques, chimiques, etc.) devront tot ou tard accepter la coexistence de caracteres extremement varies. Etant donnee l'impossibilite evidente de representer plus de 256 caracteres differents a l'aide d'un seul octet, il faut done elaborer de nouveaux concepts et etablir de nouvelles normes de codage. Comme cela a deja ete signale plus haut, la norme Utf-8 qu'utilisent les ordinateurs recents permet d'en- coder une multitude de caracteres et de symboles de toute sorte. Afin d'assurer une certaine compatibi- lite avec les textes encodes aux normes anciennes, les caracteres standards continuent a y etre encodes sur un seul octet, exactement comme en ASCII, mais tous les autres caracteres « exotiques », comme nos lettres accentuees, y sont encodes sur deux octets ou davantage. La consequence la plus importante qui decoule de cette convention est que nous allons devoir desor- mais etablir une distinction nette entre les concepts de « chaine de caracteres » et de « sequence d'oc- tets ». Si nous travaillons sur un ordinateur moderne utilisant l'encodage Utf-8 par defaut, cela apparait assez clairement, comme dans notre dernier exemple (reproduit une fois encore ci-dessous) : »> ch ="Noel en Decembre" »> print ch[l] ,ch[2] ,ch[3] ,ch[4] ,ch[8] ,ch[9] ,ch[10] ,ch[ll] ,ch[12] Pour comprendre ce qui se passe dans cet exemple, il suffit d'admettre en effet qu'apres 1'affectation de la premiere ligne, la variable ch contient, non pas vraiment une chaine de caracteres, mais plutot une se- quence d' octets. Lorsque nous demandons a Python d'afficher separement les elements n° 1, n° 2, n° 3, etc. de cette sequence, nous n'obtenons des caracteres que pour les octets qui peuvent effectivement re- presenter des caracteres en Utf-8 (e'est-a-dire seulement les octets de valeur inferieure a 128, lesquels peuvent toujours representer des caracteres ASCII standards). Pour les autres, par contre, Python de- clare forfait, parce qu'en application de la norme Utf-8, les octets de valeur superieure a 127 ne peuvent pas etre interpreted separement comme des caracteres : ils doivent pour cela etre evalues par paires (ou meme par triplets ou par quadruplets dans certains cas). La conclusion de tout ceci est que le type de donnees string : • doit etre compris comme une sequence d'octets, et non de caracteres alphanumeriques ; • n'est adapte au traitement detaille de chaines de caracteres, qu'a la condition que celles-ci soient en- coders en ASCII ou l'une de ses variantes « etendues » telles que Latin-1, ce qui sera de moins en moins la norme a l'avenir. Afin de s'affranchir de ces limitations, Python s'est dote (a partir de sa version 1.6) d'un nouveau type de donnees appele Unicode. Celui-ci autorise dorenavant le traitement detaille des chaines de caracteres, en faisant abstraction de la maniere dont ceux-ci sont encodes dans la memoire de l'ordinateur. Nous etudierons le type Unicode en detail au chapitre 10 (voir page 103). En attendant, nous allons pro- visoirement considerer que vous n'utilisez, dans les exercices necessitant un traitement detaille des chaines de caracteres (e'est-a-dire les exercices ou il est question d'acceder aux caracteres individuels de la chaine en question), que les lettres non accentuees du jeu standard ASCII. Rien ne vous empeche d'utiliser aussi des lettres accentuees, notamment si votre poste de travail utilise toujours la norme La- tin-1 . Mais si vous le faites, attendez-vous a obtenir dans certains cas des resultats etranges. 38 Apprendre a programmer avec Python 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.). Une fois de plus, cependant, nous devons vous demander de patienter : ces questions ne seront developpees qu'a partir du chapitre 10 (voir page 103). 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 dans la section precedente. 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'ope- ration d'addition lorsqu'on l'applique a des nombres, et l'operation de concatenation lorsqu'on l'ap- plique 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() : »> ch = ' Georges ' »> print len(ch) 7 Attention : comme explique a la rubrique precedente, les donnees de type string doivent etre com- prises comme des sequences d'octets, et non de veritables chaines de caracteres. Si vous testez ainsi des chaines contenant des caracteres accentues, et que votre poste de travail utilise l'encodage Utf-8, attendez-vous done de nouveau a des resultats etranges : >» ch ='Rene' »> print len(ch) 5 Vous comprenez ce qui se passe : si elle est encodee en Utf-8, la chaine de 4 caracteres « Rene » compte en fait 5 octets, a cause de la lettre accentuee 'e' (encodee sur deux octets). • 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 into convertit la chaine en nombre entier. II serait egalement possible de convertir une chaine en nombre reel, a l'aide de la fonction integree float(). 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 aste- risques entre les caracteres. Ainsi par exemple, « gaston » devra devenir « g*a*s*t*o*n » 5. Principaux types de donnees 39 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 ». Les listes (premiere approche) Les chaines que nous avons abordees a la rubrique precedente constituaient un premier exemple de donnees composites. On appelle ainsi les structures de donnees qui sont uulisees pour regrouper de ma- niere 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 1 "'. 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, 1 'en- semble 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 le meme exemple, les elements individuels 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 diffe- rent du concept de « tableau » (array) ou de « variable indicee » que Ton rencontre dans d' autres lan- gages de programmation. Remarquons aussi que, comme les chaines de caracteres, les listes sont des sequences, c'est-a-dire des collections ordonnees d'objets. Les divers elements qui constituent une liste sont en effet toujours dis- poses 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 ce- pendant 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'] 15 Vous pourrez meme creer vos propres types de donnees composites, lorsque vous aurez assimile le concept de classe (voir page 137). 40 Apprendre d programmer avec Python On peut done rempkeer certains elements d'une liste par d'autres, comme ci-dessous : »> jour [3] = 'Juillet' »> print jour 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 in- dex). 11 s'agit de la fonction del() 16 : »> 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 mon- trer « comment ca 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 Pexemple ci-dessus, nous avons applique la methode appendO 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 appendO 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 inte- grees, ou plutot « encapsulees » dans les objets de type « liste »). Notons simplement au passage que Ton applique une methode a un objet en reliant les deux a Yaide 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.) Comme les chaines de caracteres, les listes seront approfondies plus loin dans ces notes (voir page 118). 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 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 opera teur ? 16 I1 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 ceuvre en placant plusieurs indices au lieu d'un seul entre les crochets que Ton accole au nom de la variable. Ainsi jour[ 1:3] designe le sous-ensemble ['mardi', 'mercredi']. Ces techniques un peu particulieres sont decrites plus loin (voir pages 103 et suivantes). 5. Principaux types de donnees 41 Exercices 5.11 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 ele- ments 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'appli- quait 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 affi- cher : 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. 6 Fonctions predefinies Uun des concepts les plus importants en programmation est celui de fonction 17 . Les fonctions permettent en effet de decomposer un programme complexe en une serie de sous-programmes plus simples, lesquels peuvent d leur tour etre decomposes en fragments plus petits, et ainsi de suite. D 'autre part, les fonctions sont reutilisables : si nous disposons d'une fonction capable de calculer une ratine carree, par exemple, nous pouvons I'utiliser un peu partout dans nos programmes sans avoir a la re-ecrire a chaque fois. Interaction avec l'utilisateur : la fonction input() La plupart des scripts elabores necessitent a un moment ou Pautre une intervention de rutilisateur (en- tree d'un parametre, clic de souris sur un bouton, etc.). Dans un script en mode texte (comme ceux que nous avons crees jusqu'a present), la methode la plus simple consiste a employer la fonction integree in- put(). Cette fonction provoque une interruption dans le programme courant. L'utilisateur est invite a en- trer 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 l'utilisa- teur 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 argu- ment un message explicatif destine a l'utilisateur. Exemple : print 'Veuillez entrer un nombre positif 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 inputo renvoie une valeur dont le type correspond a ce que rutilisateur a entre. Dans notre exemple, la variable nn contiendra done un entier, une chaine de caracteres, un reel, etc. sui- vant ce que rutilisateur aura decide. Si l'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. Sous Python, le terme « 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. 44 Apprendre a programmer avec Python • Pour cette raison, il sera souvent preferable d'utiliser dans vos scripts la fonction similaire raw JnputO, laquelle renvoie toujours une chaine de caracteres. Vous pouvez ensuite convertir cette chaine en nombre a l'aide de into 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) 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 fi- chiers 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 mafhematiques telles que sinus, cosinus, tangente, ratine carree, etc. Pour pouvoir utiliser ces fonctions, il vous suffit d'in- corporer 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 « joker » * ) du module math, lequel contient une bibliotheque de fonc- tions 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 6. Fonctions predefines 45 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 representent qu'un tout premier exemple. Un simple coup d'ceil dans la documentation des bibliotheques Python vous permettra de constater que de tees nombreuses fonc- tions 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 aisement 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. 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 I'aire d'un triangle quelconque dont l'utilisa- teur fournit les 3 cotes. (Rappel ■. I'aire d'un triangle quelconque se calcule a I'aide de la formule : S= 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=2n 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 de- vrait fonctionner en boucle, l'utilisateur etant invite a entrer sans cesse de nouvelles valeurs, jus- qu'a ce qu'il decide de terminer en frappant en guise d'entree. Le programme se ter- minerait 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] 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. 46 Apprendre a programmer avec Python 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 geome- triques correspondant a la piste laissee derriere elle par une petite « tortue » virtuelle, dont nous contro- lons les deplacements sur l'ecran de l'ordinateur a l'aide destructions simples. Activer cette tortue est un vrai jeu d'enfant. Plutot que de vous donner 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 25), car si c'est le cas vous risquez de ne plus pouvoir re- prendre 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 : On efface tout et on recommence Aller a l'endroit de coordonnees x, y Avancer d'une distance donnee Reculer Relever le crayon (pour pouvoir avancer sans dessiner) Abaisser le crayon (pour recommencer a dessiner) couleur peut etre une chaine predefinie ('red', 'blue', 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 texte doit etre une chaine de caracteres reset () goto(x, y) forward(distance) backward(distance) up() down() color(couleur) left(angle) right(angle) width(epaisseur) fill(l) write(texte) Veracite/faussete d'une expression Lorsqu'un programme contient des instructions telles que while ou if, l'ordinateur qui execute ce pro- gramme 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 aus si 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 6. Fonctions predefines 47 notion de vrai/ faux. En Python, tout comme en C, en Basic et en de nombreux autres langages de pro- grammation, 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 nume- rique, 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 appli- quer 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. 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 petits programmes. 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." 48 Apprendre a programmer avec Python 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 'ins- tructions 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 [ 1 ] > 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'ins- truction contenue dans cette ligne est done toujours executee, alors que les deux suivantes (qui consti- tuent 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. Boucle while - instructions imbriquees Continuons dans cette voie en imbriquant d'autres structures : 1# # Instructions composees - - - 2# 3# print 'Choisissez un nombre de 1 a 3 (ou zero pour terminer) ' , 4# a = input ( ) 5# while a != 0: # 1 ' operateur != signifie "different de" 6# if a == 1: 7# print "Vous avez choisi un : " 8# print "le premier, 1' unique, 1' unite ..." 9# elif a == 2: 10# print "Vous pref erez le deux : " 11# print "la paire, le couple, le duo ..." 12# elif a == 3: 13# print "Vous optez pour le plus grand des trois : " 14# print "le trio, la trinite, le triplet ..." 15# else : 16# print "Un nombre entre UN et TROIS, s.v.p." 17# print 'Choisissez un nombre de 1 a 3 (ou zero pour terminer) ' , 18# a = input ( ) 19# print "Vous avez entre zero : " 20# print "L' exercice est done termine . " Nous retrouvons ici une boucle while, associee a un groupe d'instructions if, elif et else. Notez bien cette fois encore comment la structure logique du programme est creee a l'aide des indentations (... et n'ou- bliez 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 ni- veau d'indentation, qui est le meme que celui du bloc if, elif et else. Apres ces instructions, le pro- gramme 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. 6. Fonctions predefines 49 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 ? Conclure ! 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 mul- tiple de 1 00, a moins que A ne soit multiple de 400. 6.10 Demander a rutilisateur son nom et son sexe (M ou F). En fonction de ces donnees, afficher « Cher Monsieur » ou « Chere Mademoiselle » suivi du nom de la personne. 6.11 Demander a rutilisateur d'entrer trois longueurs a, b, c. A l'aide de ces trois longueurs, determi- ner s'il est possible de construire un triangle. Determiner ensuite si ce triangle est rectangle, iso- cele, equilateral ou quelconque. Attention : un triangle rectangle peut etre isocele. 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 N >= 80 % Appreciation A 80 % > N >= 60 % 60 % > N >= 50 % 50 % > N >= 40 % N < 40 % % B % C % D E 50 Apprendre a programmer avec Python 6.14 Soit la liste suivante : ['Jean-Michel', 'Marc', 'Vanessa', 'Anne', 'Maximilien ' , ' Alexandre-Benoi t ' , ' 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 en- trees, 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 10 000 kg , pour des distances qui augmentent suivant une progression geometrique de raison 2, a partir de 5 cm (0,05 metre). ii m-m ' La force de gravitation est regie par la formule r —6,67 .10 ^ — Exemple d'affichage : d = .05 i : 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. 7 Fonctions originales 1m programmation est I'art d'apprendre d un ordinateur comment accomplir des tdches qu'il n'etait pas capable de realiser auparavant. L'une des methodes les plus interessantes pourj arriver consiste a aj outer de nouvelles instructions au langage de programmation que vous utilise^ sous la forme de fonctions originales. Definir une fonction Les scripts que vous avez ecrits jusqu'a present etaient a chaque fois tees courts, car leur objectif etait seulement de vous faire assimiler les premiers elements du langage. Lorsque vous commencerez a deve- lopper 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-pro- blemes 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 18 pour que ceux-ci restent clairs. D'autre part, il arrivera souvent qu'une meme sequence d'instructions doive etre utilisee a plusieurs re- prises dans un programme, et on souhaitera bien evidemment ne pas avoir a la reproduire systemati- quement. Les fonctions 19 et les classes d'objets sont differentes structures de sous-programmes qui ont ete imagi- nees 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 nomDeLaFonction (liste de parametres) : bloc d'instructions 18 On appelle algorithme la sequence detaillee de toutes les operations a effectuer pour resoudre un probleme. 19 I1 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). 52 Apprendre a programmer avec Python • Vous pouvez choisir n'importe quel nom pour la fonction que vous creez, a l'exception des mots reserves du langage 20 , 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'uti- liser surtout des lettres minuscules, notamment au debut du nom (les noms commencant par une majuscule seront reserves aux classes que nous etudierons plus loin). • 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, le- quel 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 pro- gramme, 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). Fonction simple sans parametres Pour notre premiere approche concrete des fonctions, nous allons travailler a nouveau en mode inter- actif. Le mode interactif de Python est en effet ideal pour effectuer des petits tests comme ceux qui suivent. C'est une facilite que n'offrent pas tous les langages de programmation ! »> def table 7 () : 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 21 , le double point, et l'in- dentation 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 : »> table 7 () provoque l'affichage de : 7 14 21 28 35 42 49 56 63 70 Nous pouvons maintenant reutiliser cette fonction a plusieurs reprises, autant de fois que nous le sou- haitons. 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() 20 La liste complete des mots reserves Python se trouve page 12. 21 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. 7. Fonctions originales 53 table7 ( ) table7 ( ) Utilisons cette nouvelle fonction, en entrant la commande : »> table7 triple () 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 troi- sieme, etc. Au stade ou nous sommes, vous ne voyez peut-etre pas encore tees bien Futilite 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'ins- tructions. De cette maniere, vous pouvez simplifier le corps principal d'un programme, en dissimu- lant un algorithme secondaire complexe sous une commande unique, a laquelle vous pouvez don- ner 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 votee langage de programmation. Fonction avec parametre 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 mo- ment 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. 54 Apprendre a programmer avec Python 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 tablet) en affectant la va- leur 9 a la variable base. Utilisation d'une variable comme argument Dans les 2 exemples qui precedent, l'argument que nous avons utilise en appelant la fonction tabled etait a chaque fois une constante (la valeur 13, puis la valeur 9). Cela n'est nullement obligatoire. L'argu- ment que nous utilisons dans l'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 dormer un premier apercu de l'utilite des fonctions pour accomplir simple- ment 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 tabled 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 desi- gnent pas la meme chose (en depit du fait qu'ils puissent contenir une valeur identique). 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. 7. Fonctions originales 55 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 ver- sion que nous appellerons cette fois tableMultiO : »> 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 prece- dent, 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 ar- gument sera affecte au premier parametre, le second argument sera affecte au second parametre, et ainsi de suite. • A titre d'exercice, essayez la sequence destructions suivantes et decrivez dans votre cahier d'exer- cices le resultat obtenu : »> t, d, f = 11, 5, 10 >» while t<21: tableMulti (t,d,f) t, d, f = t +1, d +3, f +5 Variables locales, variables globales Lorsque nous definissons des variables a l'interieur du corps d'une fonction, ces variables ne sont ac- cessibles 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 tableMultiO est appelee, Python reserve pour elle (dans la memoire de l'ordi- nateur) un nouvel espace de noms 22 . Les contenus des variables base, debut, fin et n sont stockes dans cet espace de noms qui est inaccessible depuis I'exterieur de la fonction. Ainsi par exemple, si nous es- 22 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. 56 Apprendre d programmer avec Python sayons d'afficher le contenu de la variable base juste apres avoir effectue l'exercice ci-dessus, nous obte- nons 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 tableMultiO elle-meme. L'espace de noms qui contient le symbole base est stric- tement reserve au fonctionnement interne de tableMultiO, 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 « vi- sible » 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 la definition de la fonction terminee, 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 loca- lement qui ont la priorite. 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, e'est la valeur attribute localement qui est af- fichee. On pourrait croire d'abord que la fonction mask() a simplement modifie le contenu de la variable glo- bale 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 com- bien il est utile que des variables soient ainsi definies comme etant locales, e'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 defi- nition d'une fonction - quelles sont les variables a traiter globalement. 7. Fonctions originales 57 Dans l'exemple ci-dessous, la variable a utilisee a l'interieur de la fonction monterO est non seulement accessible, mais egalement modifiable, parce qu'elle est signalee explicitement comme etant une va- riable qu'il faut traiter globalement. Par comparaison, essayez le meme exercice en supprimant l'instruc- tion 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 »> Vraies fonctions et procedures Pour les puristes, les fonctions que nous avons decrites jusqu'a present ne sont pas tout a fait des fonc- tions au sens strict, mais plus exactement des procedures 2 '. 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 aisement que dans cette expres- sion, la fonction sin() renvoie une valeur (le sinus de l'argument a) qui est directement affectee a la va- riable 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 tableO 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. Dans la foulee, nous en profiterons pour apprendre 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) 23 Dans certains langages de programmation, les fonctions et les procedures sont definies a l'aide d'instructions differentes. Python utilise la meme instruction def pour defmir les unes et les autres. 58 Apprendre a programmer avec Python 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). Notes • Comme nous l'avons vu dans l'exemple precedent, l'instruction return definit ce que doit etre la va- leur « 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 24 . • 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 append!) a l'objet result. Nous preciserons petit a petit ce qu'il faut entendre par objet en programmation. Pour l'instant, ad- mettons simplement que ce terme tees general s 'applique notamment aux listes de Python. Une methode n'est en fait rien d'autee 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 precisement de la classe particuliere a laquelle cet objet appartient (nous etudierons ce concept de classe plus tard). Mettre en oeuvre 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 ob- jet objet3, a l'aide d'une instruction du type : objet3.methode4() , c'est-a-dire le nom de l'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 l'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 append!) 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 moins ra- tionnelle et beaucoup 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 modifi- cation de la liste existante (sans la recopier dans une nouvelle variable). Cette technique est done 24 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 l'objet None (objet particulier, correspondant a « rien »). 7. Fonctions originales 59 preferable, car elle mobilise moins lourdement les res sources 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 va- riable (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)... Utilisation des fonctions dans un script Pour cette premiere approche des fonctions, nous n'avons utilise jusqu'ici que le mode interactif de l'in- terpreteur 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 la formule que jt 4 3 vous connaissez certamement : V = — tt K 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 cubed et volumeSphereO, et ensuite le corps principal du programme. Dans le corps principal du programme, il y a un appel de la fonction volumeSphereO. A l'interieur de la fonction volumeSphereO, 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 defi- nition 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 un 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 pro- gramme 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 sui- 60 Apprendre a programmer avec Python vante, 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 de- finition 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 correcte- ment 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 done concretement que vous devrez souvent analyser un script en commencant par ses dernieres lignes ! 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, le- quel pourra ensuite etre exploite par certains utilitaires et editeurs « intelligents ». Si vous programmez dans l'environnement IDLE, par exemple, vous verrez apparaitre cette chaine do- cumentaire 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 » en- toure 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, page 140). Ainsi, vous pouvez vous-meme retrouver la chaine de documentation d'une fonction quelconque en af- fichant le contenu de cette variable. Exemple : 7. Fonctions originales 61 »> def essai() : ... "Cette fonction est bien documentee mais ne fait presque rien." . . . print "rien a signaler" »> essai () rien a signaler »> print essai. doc 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 rutiliser dans n'im- porte 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 : 1 = 0 while i < 10: down() # abaisser le crayon carre(25, 'red') # tracer un car re up ( ) # relever le crayon forward (30) # avancer + loin 1 = 1+1 a = input ( ) # attendre Attention 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 I'un des 29 mots reserves Python signales a la page 12, 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 I'exercice lui-meme que vous allez importer ! Exercices 7.2 Definissez une fonction MgneCar(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 surfCercle (2 . 5) doit donner le resultat 19 . 635 . 7.4 Definissez une fonction volBoite(xl,x2,x3) qui renvoie le volume d'une boite parallelepipedique dont on fournit les trois dimensions xl, x2, x3 en arguments. Par exemple, l'execution de l'instruction : print volBoite (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 . 62 Apprendre d 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 " nc = 0 i = 0 ^ while i < len(ch) if ch[i] == car: nc = nc + 1 i = i + 1 return nc ################################ # 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 globules). - Les definitions locales de fonctions et/ou de classes. - Le corps principal du programme. Le programme peut 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 : - V 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 decrementation 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. 7. Fonctions originales 63 7.6 Completez le module de fonctions graphiques dessins_tortue.py decrit a la page 60. 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 equi- lateral 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 : -i ,■. . -■ ° x " 7.8 Ajoutez au module de l'exercice precedent une fonction etoile6() capable de dessiner une etoile a 6 branches, elle-meme constituee 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 : 64 Apprendre a programmer avec Python 7.9 Deflnissez une fonction compteCar(ca,ch) qui renvoie le nombre de fois que Ton rencontre le ca- ractere ca dans la chaine de caracteres ch. Par exemple, l'execution de l'instruction : print compteCar ( ' e ' , ' Cette phrase est un exemple') doit donner le resultat : 7 7.10 Deflnissez 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.11 Deflnissez une fonction nomMois(n) qui renvoie le nom du n-ieme mois de l'annee. Par exemple, l'execution de l'instruction : print nomMois (4) doit donner le resultat : Avril . 7.12 Deflnissez 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 Deflnissez 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. 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 devient automatiquement le meme que ce- lui de l'argument qui a ete transmis a la fonction. Exemple : »> def af f icher3fois (arg) : print arg, arg, arg »> af ficher3fois (5) 5 5 5 »> af ficher3fois ( ' zut' ) zut zut zut »> afficher3fois ( [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 resul- tat de cette evaluation qui est transmis comme argument a la fonction. Valeur s par defaut pour les parametres Dans la definition d'une fonction, il est possible (et souvent souhaitable) de definir un argument par de- faut 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 . 7. Fonctions originales 65 »> 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 seule- ment. 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) : 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 ' , ' OUT ' ) : . . . 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. 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 corres- pondent dans la definition de la fonction. Python autorise cependant une souplesse beaucoup plus grande. Si les parametres annonces dans la de- finition 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, a 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 66 Apprendre a programmer avec Python si vous le branchez sur 100 volts ! L'auteur de ceci est completement allume Exercices 7.14 Modiflez la fonction volBoite(xl,x2,x3) que vous avez definie dans un exercice precedent, de ma- niere a ce qu'elle puisse etre appelee avec trois, deux, un seul, ou meme aucun argument. Utili- sez pour ceux ci des valeurs par defaut egales a) 10. Par exemple : print volBoite () doit donner le resultat : 1000 print volBoite (5.2) doit donner le resultat : 520 . 0 print volBoite (5.2, 3) doit donner le resultat : 156 . 0 7.15 Modiflez 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'ar- gument etant l'arete de ce cube). Si deux sont utilises, la boite est consideree comme un prisme a base carree (auquel 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 volBoite () doit donner le resultat : -1 (indication d'une erreur) print volBoite (5.2) doit donner le resultat : 140 . 608 print volBoite (5.2, 3) doit donner le resultat : 81 . 12 print volBoite (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 extre- mite a l'autre). Exemples de la fonctionnalite attendue : »> phrase = ' Ceci est une toute petite phrase . ' »> print changeCar (phrase , ' 1 , ' * ' ) 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 8 Utilisation de fenetres et de graph ismes Jusqu 'a present, nous avons utilise Python exclusivement « en mode texte ». Nous avons procede ainsi pane 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 gra- phiques, 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 I'approche textuelle reste la plus abordable. Interfaces graphiques (GUI) Si vous ne le saviez pas encore, apprenez des a present que le domaine des interfaces graphiques (ou GUI : Graphical User Interfaces) est extremement complexe. Chaque systeme d'exploitation peut en ef- fet proposer plusieurs « bibliotheques » de fonctions graphiques de base, auxquelles viennent frequem- ment 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 bi- bliotheques 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 heureusement des versions similaires (et gratuites) pour les plates-formes Lznux, Windows et MacOS. Premiers pas avec Tkinter Pour la suite des explications, nous supposerons bien evidemment que le module Tkinter a deja ete ins- talls sur votre systeme. Pour pouvoir en utiliser les fonctionnalites dans un script Python, il faut que l'une 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 ligne de commande, en ayant simplement lance Python en mode interactif. 68 Apprendre a programmer avec Python Dans l'exemple qui suit, nous allons creer une fenetre tres simple, et y ajouter deux widgets 2 ^ typiques : un bout de texte (ou label) et un bouton (ou button). »> from Tkinter import * »> fenl = Tk() »> texl = Label (fenl, text='Bonjour tout le monde !', fg= ' red ' ) »> texl pack ( ) »> boul = Button (fenl, text= ' Quitter ' , command = fenl destroy) »> boul pack ( ) »> fenl mainloop ( ) 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 26 . 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 im- porter 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 : fenl = 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 Program- ming). La classe est en quelque sorte un modele general (ou un moule) a partir duquel on demande a la ma- chine de construire un objet informatique particulier. La classe contient toute une serie de defini- tions 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 tallies 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. 25 « 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. 26 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 Python Win. Vous pourrez mieux observer ce qui se passe apres 1' entree de chaque commande. 8. Utilisation de fenetres et de graphismes 69 L'instruction d' instanciation ressemble a une simple affectation de variable. Comprenons bien ce- pendant qu'il se passe ici deux choses a la fois : - la creation d'un nouvel objet, (lequel peut etre fort complexe dans certains cas, et par consequent occuper un espace memoire considerable) ; - I'affectation d'une variable, qui va desormais servir de reference pour manipuler l'objet 27 . 3. A la troisieme ligne : texl = Label(fenl, text='Bonjour tout le monde !', f g= ' red '), nous creons un autre objet (un widget), cette fois a partir de la classe Label(). Comme son nom 1'indique, cette classe definit toutes sortes d'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 que nous creons ici l'objet texl par instanciation de la classe Label(). Remarquons que nous faisons appel a une classe, de la meme maniere que nous faisons appel a une fonction : c'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 chalne de caracteres : d'abord le texte de Petiquette, 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 ca- racteristiques 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, no- tamment). 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 element de pro- gramme contenant toujours : - un certain nombre de donnees (numeriques ou autres), contenues dans des variables de types di- vers : 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 automatique- 27 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). 70 Apprendre a programmer avec Python ment la taille de la fenetre « maitre » afln qu'elle soit juste assez grande pour contenir les widgets « esclaves » definis au prealable. 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 ButtonO en fournissant entre parentheses un certain nombre d'arguments. Etant donne qu'il s'agit cette fois d'un objet inter- actif, nous devons preciser avec l'option command ce qui devra se passer lorsque l'utilisateur effectue- ra 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 demar- rage du receptionnaire d' evenements associe a la fenetre. Cette instruction est necessaire pour que votre application soit « a l'affut » des dies de souris, des pressions exercees sur les touches du cla- vier, etc. C'est done cette instruction qui « la met en marche », en quelque sorte. Comme son nom l'indique (mainloop), il s'agit d'une 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 environ- nement, notamment au niveau des peripheriques d'entree (souris, clavier, etc.). Lorsqu'un evene- ment quelconque est detecte, divers messages decrivant cet evenement sont expedies aux pro- grammes qui souhaitent en etre avertis. Voyons cela un peu plus en detail. Initialisation Programmes pilotes par des evenements Vous venez d'experimenter votre premier programme utilisant une interface graphique. Ce type de pro gramme est structure d'une maniere differente des scripts « textuels » etudies auparavant. Tous les programmes d'ordinateur comportent grosso-modo trois phases princi- pales : 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'lnternet, etc.), une phase centrale ou Ton trouve la veritable fonctionnalite du programme (e'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 (e'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 corres- pondant a la sequence d 'instructions 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, le- quel est plus ou moins intimement integre au systeme d'exploitation de l'ordinateur et « tourne » en per- manence. Fonctionnalite centrale du programme Terminaison 8. Utilisation de fenetres et de grophismes 71 Ce receptionnaire d'evenements scrute sans cesse tous les peripheriques (clavier, souris, horloge, mo- dem, 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 exemple) Lorsqu'il detecte un evenement, le receptionnaire envoie un message specifique au programme 28 , lequel doit etre concu pour reagir en consequence. La phase d'initialisation d'un programme utilisant une interface graphique comporte un ensemble des- tructions 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 destructions 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 func- tions independantes. Chacune de ces fonctions est appelee specifiquement lorsqu'un evenement parti- culier est detecte par le systeme d'exploitation : elle effectue alors le travail que Ton attend du pro- gramme en reponse a cet evenement, et rien d autre . II est important de bien comprendre ici que pendant tout ce temps, le receptionnaire continue a « tour- ner » et a guetter l'apparition d'autres evenements eventuels. 28 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. 29 Au sens strict, une telle fonction qui ne devra renvoyer aucune valeur est done plutot une procedure (cf. page 57). 72 Apprendre a programmer avec Python S'il arrive d'autres evenements, il peut done se faire qu'une deuxieme fonction (ou une 3 e , une 4 e ...) soit activee et commence a effectuer son travail « en parallele » avec la premiere qui n'a pas encore termine le sien 30 . Les systemes d'exploitation et les langages modernes permettent en effet ce parallelisme que Ton appelle aussi multitache. Au chapitre precedent, nous avons deja remarque que la structure du texte d'un programme n'indique pas directement l'ordre dans lequel les instructions seront finalement executees. Cette remarque s'ap- plique encore bien davantage dans le cas d'un programme avec interface grapbique, puisque l'ordre dans lequel les fonctions sont appelees n'est plus inscrit nulle part dans le programme. Ce sont les eve- nements qui pilotent ! Tout ceci doit vous paraitre un peu complique. Nous allons l'illustrer dans quelques exemples. Exemple graph i que : trace de lignes dans un canevas Le script decrit ci-dessous cree une fenetre compor- tant trois boutons et un canevas. Suivant la termi- nologie de Tkinter, un canevas est une surface rec- tangulaire delimitee, dans laquelle on peut installer ensuite divers dessins et images a l'aide de me- thodes specifiques ' 1 . Lorsque Ton clique sur le bouton « Tracer une ligne », une nouvelle ligne coloree apparait sur le ca- nevas, 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 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 , fill=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 n ombre aleatoire de 0 a 7 coul = pal [c] 30 En particulier, la meme fonction peut etre appelee plusieurs fois en reponse a 1' 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 bord » genants. 31 Ces dessins pounont eventuellement etre animes dans une phase ulterieure. 8. Utilisation de fenetres et de grophismes 73 # 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 ', heigh t=200 ,width=200) canl .pack (side=LEFT) 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 () fenl . 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 drawlineO et changecolorO, 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 dif- ferents widgets par instanciation a partir des classes Tk(), CanvasO et ButtonO. Remarquons au passage que la meme classe ButtonO sert a instancier plusieurs boutons, qui sont des objets similaires pour l'es- sentiel, mais neanmoins individualises grace aux options de creation et qui pourront fonctionner inde- pendamment l'un de l'autre. L'initialisation se termine avec l'instruction fenl.mainloopO qui demarre le receptionnaire d'evenements. Les instructions qui suivent ne seront executees qu'a la sortie de cette boucle, sortie elle-meme declen- chee par la methode fenl.quitO (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 « clic gauche de la souris sur le widget » 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 ni- veau principal du programme. Cela est rendu possible grace a l'instruction global utilisee dans la defini- tion de ces fonctions. Nous nous permettrons de proceder ainsi pendant quelque temps encore (ne se- rait-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 vraiment recommandable, surtout lors- qu'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 137). Dans notre fonction changecolorO, une couleur est choisie au hasard dans une liste. Nous utilisons pour ce faire la fonction randrangeO importee du module random. Appelee avec un argument N, cette fonc- tion renvoie un nombre entier, tire au hasard entre 0 et N-l. La commande liee au bouton « Quitter » appelle la methode quit() de la fenetre fenl. Cette methode sert a former (quitter) le receptionnaire d'evenements (mainloop) associe a cette fenetre. Lorsque cette me- thode 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. 74 Apprendre a programmer avec Python Exercices 8.1 Comment faut-il modifier le programme pour ne plus avoir que des lignes de couleur cyan, ma- roon et green ? 8.2 Comment modifier le programme pour que toutes les lignes tracees soient horizontales et paral- leles ? 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 rindication « vi- seur ». 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 createarc, create oval, et createpolygon. 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 bouton « Quitter » doit permettre de fermer la fenetre. b) Modifiez le programme ci-dessus en y ajoutant 5 boutons. Chacun de ces boutons provoque- ra le trace de chacun des 5 anneaux 8.8 Dans votre cahier de notes, 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). Laissez de la place pour completer ulterieurement. Exemple graphique : deux dessins alternes Cet autre exemple vous montrera comment vous pouvez exploiter les connaissances que vous avez ac- quises 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) 8. Utilisation de fenetres et de graphismes n dessin 1 dessin 2 =A dessin 1 dessin 2 # 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' ] , [70, 70, 15, 'blue' ] , [130, 70, 15, 'blue' ] , [70, 70, 5, 'black' ] , [130, 70, 5, 'black' ] , [44, 115, 20, 'red' ] , [156, 115, 20, 'red' ] , [100, 95, 15, 'purple' ] , [100, 145, 30, 'purple']] visage yeux # joues # nez # bouche ' aide d ' une boucle # on trace tous les cercles a 1 ' i =0 while i < len(cc) : # parcours de la liste el = cc[i] # chaque element est lui-meme une liste cercle (el [0] , el[l], el [2], el [3]) i += 1 ##### 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 ( ) 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 ins- tancie dans la variable can, et les deux boutons dans les variables bl et b2. Comme dans le script prece- dent, 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 : 76 Apprendre a programmer avec Python • l'option side peut accepter les valeurs TOP, BOTTOM, LEFT ou RIGHT, pour « pousser » le wid- get du cote correspondant dans la fenetre. Ces noms ecrits en majuscules sont en fait ceux d'une serie de variables importees avec le module Tkinter, et que vous pouvez considerer comme des « pseudo-constantes ». • les options padx et pady permettent de reserver un petit espace autour du widget. Cet espace est ex- prime 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 1() et figure 2(). Considerant que nous aurions a tracer un certain nombre de cercles dans ces dessins, nous avons esti- me qu'il serait bien utile de definir d'abord une fonction cercleO specialisee. En effet, vous savez proba- blement deja que le canevas Tkinter est dote d'une methode create_oval() qui permet de dessiner des el- lipses quelconques (et done aussi des cercles), mais cette methode doit etre invoquee avec quatre argu- ments qui seront les coordonnees des coins superieur gauche et inferieur droit d'un rectangle fictif, dans lequel rellipse 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 cercleO, laquelle invoque la methode create oval() en effectuant la conversion des coordonnees. Remarquez que cette fonction attend un ar- gument 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 crois- sant). Notez au passage 1'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 tallies 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 : coor- donnees x et y du centre, rayon et couleur. Pour chaque cercle, nous pouvons placer ces quatre caracte- ristiques 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. Exercices 8.9 Inspirez-vous du script precedent pour ecrire une petite appli- cation 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 pres- sion sur le bouton fera apparaitre un nouveau pion). Exemple graphique : calculatrice minimaliste Bien que tres court, le petit script ci-dessous implemente une calculatrice com- plete, avec laquelle vous pourrez meme effectuer des calculs comportant des parentheses et des fonctions scientifiques. N'y voyez rien d'extraordinaire. Toute cette fonctionnalite n'est qu'une consequence du fait que vous utilisez un interpreteur plutot qu'un compilateur pour executer vos programmes. damier pions 8. Utilisation de fenetres et de graphismes 77 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 pro- gramme. 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 une expression mathema- tique entree au clavier par l'utilisateur. Les langages interpretes disposent done toujours de fonctions permettant d'evaluer une chaine de ca- racteres 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 neces- saire 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 evaluerO, qui sera en fait la commande executee par le pro- gramme lorsque rutilisateur actionnera la touche Return (ou Enter) apres avoir entre une expression mathematique quelconque dans le champ d'entree decrit plus loin. Cette fonction utilise la methode configureO du widget chaine 32 , 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 l'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 rutilisateur d'entrer une expression numerique 32 La methode configureO peut s'appliquer a n'importe quel widget preexistant, pour en modifier les proprietes. 78 Apprendre a programmer avec Python quelconque a l'aide de son clavier. La methode get() permet en quelque sorte « d'extraire » du widget en- tree 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 LabelO, et un widget entree instancie a partir de la classe EntryO. Attention : afin que ce dernier widget puisse vraiment faire son travail, c'est-a-dire transmettre au pro- gramme l'expression que l'utilisateur y aura encodee, nous lui associons un evenement a l'aide de la me- thode bind() 33 : entree . bind ( "" , evaluer ) Cette instruction signifie : « Lier Y 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 dies de la souris, enfoncement des touches du clavier, positionnement et redimensionnement des fe- netres, passage au premier plan, etc.). Vous trouverez la liste des chalnes specifiques de tous ces evene- ments dans les ouvrages de reference traitant de Tkinter. Remarquez bien qu'il n'y a pas de parentheses apres le nom de la fonction evaluer. En effet : dans cette instruction, nous ne souhaitons pas deja invoquer la fonction elle-meme (ce serait premature) ; ce que nous voulons, e'est etablir un lien entre un type d'evenement particulier et cette fonction, de maniere a ce qu'elle soit invoquee plus tard, chaque fois que l'evenement se produira. Si nous mettions des paren- theses, l'argument qui serait transmis a la methode bind() serait la valeur de retour de cette fonction et non sa reference. Profitons aussi de l'occasion pour observer encore une fois la syntaxe des instructions destinees a mettre en ceuvre 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'opera- teur), puis le nom de la methode a mettre en ceuvre ; entre les parentheses associees a cette methode, on indique enfin les arguments qu'on souhaite lui transmettre. 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 34 . 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 pre- mier argument. Cet argument designe en effet un objet cree automatiquement par Tkinter, qui permet de transmettre au gestionnaire d'evenement un certain nombre d'attributs de l'evenement : • le type d'evenement : deplacement de la souris, enfoncement ou relachement de l'un de ses bou- tons, 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 caracteris- tiques du ou des widget(s) concerne(s), etc. 33 En anglais, le mot bind signifie « lier ». 34 La presence d'un argument est obligatoire, mais le nom event est une simple convention. Vous pourriez utiliser un autre nom quelconque a sa place, bien que cela ne soit pas recommande. 8. Utilisation de fenetres et de graphismes 79 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 pointeur (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 (Frame) rec- tangulaire de couleur jaune pale, dans lequel 1'utilisateur est invite a effectuer des clics de souris. La methode bind() du widget cadre associe Pevenement au gestionnaire d'evenement « pointeur ». Ce gestionnaire d'evenement peut utiliser les attributs x et y de l'ob- jet event genere automatiquement par Tkinter, pour construire la chaine de caracteres qui affichera la position de la souris au mo- ment du clic. Exercice 8.11 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). Les classes de widgets Tkinter Note Au long de cet ouvrage, nous vous presenterons petit a 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-a-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 12) si vous souhaitez davantage de precisions. Clic detecte en X =75. Y =45 80 Apprendre a programmer avec Python II existe 15 classes de base pour les widgets Tkinter : Widget Description Button Un bouton classique, a utiliser pour provoquer I'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 die sur ce widget provoque le changement d'etat. Entry Un champ d'entree, dans lequel I'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. El le peut aussi etre decoree d'une bordure. Label Un texte (ou libelle) quelconque (eventuellement une image). Listbox Une liste de choix proposes a I'utilisateur, generalement presentes dans une sorte de botte. On peut egalement configurer la Listbox de telle maniere qu'elle se comporte comme une serie de « boutons radio » ou de cases a cocher. Menu 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 die. Menubutton Un bouton-menu, a utiliser pour implementer des menus deroulants. Message Permet d'afficher un texte. Ce widget est une variante du widget Label, qui permet 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 deplacant 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 formate. Permet aussi a I'utilisateur d'editer le texte affiche. Des images peuvent egalement etre inserees. Toplevel Une fenetre affichee separement, au premier plan. Ces classes de widgets integrent chacune un grand nombre de methodes. On peut aussi leur associer (tier) 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 dif- ferentes : la methode grid(), la methode pack() et la methode placet). L'utilite de ces methodes apparait clairement lorsque Ton s'efforce de realiser des programmes por- tables (e'est-a-dire susceptibles de fonctionner de maniere identique sur des systemes d'exploitation aus- si differents que Unix, Mac OS ou Windows), et dont les fenetres soient redimensionnables. Utilisation de la methode grid() pour controler 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 liberte pour disposer les widgets a notre guise. Comment faire, par exemple, pour obtenir la fenetre ci- contre ? Premie champ : 1 Second : | 8. Utilisation de fenetres et de graphismes 81 Nous pourrions effectuer un certain nombre de tentatives en fournissant a la methode pack() des argu- ments de type « side = », comme nous l'avons deja fait precedemment, mais cela ne nous mene pas tres loin. Essayons par exemple : from Tkinter import * fenl = Tk() txtl = Label (fenl, text = 'Premier champ :') txt2 = Label (fenl, text = 'Second :') entrl = Entry (fenl) entr2 = Entry (fenl) txtl. pack (side =LEFT) txt2. pack (side =LEFT) entrl .pack (side =RIGHT) entr2 .pack (side =RIGHT) fenl . mainloop ( ) ... mais le resultat n'est pas vraiment celui que nous recherchions ! mm t3 Pour mieux comprendre comment fonctionne la methode pack(), vous pouvez encore essayer diffe- rentes 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 FrameO supplementaires, et en y incorporant ensuite separement les widgets LabelO et EntryO. Cela devient fort complique. 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) 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 pou- vez le constater, rutilisation 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 co- lonne (column) de ce tableau on souhaite placer les widgets. On peut numeroter les lignes et les co- lonnes 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 gridO utilisee pour ces widgets. L'option sticky peut prendre l'une des quatre valeurs N, S, W, E (les quatre points cardi- naux 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 : 82 Apprendre a programmer avec Python txtl . grid (row =0, sticky =E) txt2 . grid (row =1 , sticky =E) ... et vous atteindrez enfin exactement le but recherche. 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' con tenant une image bitmap : canl = Canvas (fenl, width =160, height =160, bg =' white') photo = Photolmage (f ile =' Mar tin_P.gif ' ) item = canl . create_image (80 , 80, image =photo) # Mise en page a l'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 ( ) 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 stan- dard n'accepte qu'un petit nombre de formats pour cette image. Choisissez de preference le format GIF 35 . 35 D'autres formats d'image sont possibles, mais a la condition de les traiter a l'aide des modules graphiques de la bibliotheque PIL (Python Imaging Library), qui est une extension de Python disponible sur : http://www.pythonware.com/products/pil/. Cette bibliotheque permet en outre d'effectuer une multitude de traitements divers sur des images, mais l'etude de ces techniques depasse le cadre que nous nous sommes fixes pour ce manuel. 8. Utilisation de fenetres et de graphismes 83 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 PhotolmageO. 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 co- lonnes 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 troi- sieme 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 decorti- que, pour apprendre a simplifier quelque peu notre code. . . Composition constructions pour ecrire un code plus compact Python etant 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 placeO) au moment meme ou vous creez ces wid- gets. 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 interme- diaire txtl. Nous avions utilise cette variable pour bien degager les etapes successives de notre de- marche, mais elle n'est pas toujours indispensable. Le simple fait d'invoquer la classe Label() provoque en effet l'instanciation d'un 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 84 Apprendre a programmer avec Python 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 instantiation a partir d'une classe quelconque), en les affectant a chaque fois a des variables. Par exemple, lorsque nous avons ecrit : txtl = Label ( f enl , text = ' Premier champ : ' ) nous avons cree une instance de la classe LabelO, 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 gridO, 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 destructions. 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 la suite, comme c'est souvent le cas pour les widgets de la classe LabelO, on peut en general appliquer la methode de mise en page (grid() , pack() ou place()) 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 assignes a des variables, afin que I'on puisse encore interagir avec eux ailleurs dans le script. Et dans ce cas, il faut obligatoirement utiliser deux instructions distinctes, l'une pour instancier le wid- get, 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 va- riable entree n'est pas la reference du widget ! C'est la valeur de retour de la methode pack() : vous devez vous rappeler en effet que les methodes, comme les fonctions, renvoient toujours une valeur au pro- gramme qui les appelle. Et vous ne pouvez rien faire de cette valeur de retour : il s'agit en l'occurrence d'un objet vide (None). Pour obtenir une vraie reference du widget, vous devez obligatoirement utiliser deux instructions : entree = Entry (fenl) # instanciation du widget entree .pack () # application de la mise en page 8. Utilisation de fenetres et de graphismes 85 Note Lorsque vous utilisez la methode grid(), vous pouvez simplifier encore un peu votre code, en omettant / 'indication de nombreux numeros de lignes et de colonnes. A partir du moment ou c'est la la methode gridO qui est utilisee pour positionner les widgets, Tkinter considere en effet qu'il existe forcement des lignes et des colonnes 36 . 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 ( ) , Entry ( ) , et Checkbutton ( ) Label(fenl, text = 'Premier champ :') .grid (sticky =E) Label(fenl, text = 'Deuxieme :') .grid (sticky =E) Label(fenl, text = 'Troisieme :') .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 chekl . grid (columnspan =2) ces widgets devront certainement etre references plus loin : il faut done les assigner chacun a une variable distincte a cocher , pour voir ' ) # 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 ' ) canl . create_image (80 , 80 , image =photo) canl .grid (row =0, column =2, rowspan =4, padx =10, pady =5) # demarrage : fenl . mainloop ( ) Modification des proprietes d'un objet - Animation A ce stade de votre apprentissage, vous souhaitez probablement 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, yl+30) # gestionnaires d'evenements : def depl_gauche ( ) : avance(-10, 0) def depl_droite () : avance(10, 0) def depl_haut() : avance(0, -10) def depl_bas(): avance(0, 10) Surtout, n'utilisez pas plusieurs methodes de positionnement differentes dans la meme fenetre ! Les methodes gridQ, packQ et place() sont mutuellement exclusives. Exercice d'animation avec Tk... nrri|x| Gauche Droite Haul o Bas Quitter 86 Apprendre d programmer avec Python # 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 , f ill= ' red') canl .pack (side=LEFT) Button (fenl , text= ' Quitter ' , command=fenl . 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 ( fenl , text= ' Bas ' , command=depl_bas ) . pack ( ) # demarrage du receptionnaire d'evenements (boucle principale) : fenl.mainloopO Le corps de ce programme reprend de nombreux 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 va- riables (c'est inutile, puisque nous n'y faisons plus reference par la suite) : 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, puis on agit sur ces objets en modifiant leurs proprietes, par Vinterme- diaire de methodes. En programmation imperative « a l'ancienne » (e'est-a-dire sans utilisation d'objets), on anime des fi- gures 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 direc- tions. 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 (cf. exercice 6.16, page 50, et un manuel 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 perma- nence la force resultante agissant sur chacun des trois (en effet : chacun subit en permanence l'attraction gravitationnelle exercee par les deux autres !). 8. Utilisation de fenetres et de graphismes 87 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 . Revoyez aussi le petit programme concernant la calculatrice simplifiee (page 76). 8.17 Ecrivez un programme qui fait 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 resul- tat 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 profes- seur pour des suggestions (courbes de Lissajous). 8.21 Ecrivez un programme qui fait 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 : □ tk BSB o • • o 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 'en- tree qui permettront de parametrer chaque element (c'est-a-dire choisir les valeurs des resis- tances 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 effectues par l'utilisateur. 88 Apprendre a programmer avec Python 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=f enl . 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 ( ) La seule nouveaute mise en ceuvre 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. Ain- si par exemple, window. after (200, qqc) declenche pour le widget window un appel de la fonction qqc() apres une pause de 200 millisecondes. 8. Utilisation de fenetres et de graphismes 89 Dans notre script, la fonction qui est appelee par la methode afterO 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 fonc- tion s'appelle elle-meme. On obtient bien evidemment ainsi un bouclage, qui peut se perpetuer indefi- niment si Ton ne prevoit pas aussi un moyen pour l'interrompre. Voyons comment cela fonctionne dans notre exemple. La fonction moved est invoquee une premiere fois lorsque Ton clique sur le bouton « Demarrer ». Elle effectue son travail (e'est-a-dire positionner la balle). Ensuite, par l'intermediaire de la methode afterO, 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 d'un simple test conditionnel : a chaque itera- tion de la boucle, nous examinons le contenu de la variable flag a l'aide d'une instruction if. Si le conte- nu de la variable flag est zero, alors le bouclage ne s'effectue plus et l'animation s'arrete. Remarquez que nous avons pris la precaution de definir flag comme une variable globale. Ainsi nous pouvons aisement changer sa valeur a l'aide d'autres fonctions, en l'occurrence celles que nous avons associees aux bou- tons « 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, puis 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, parce que la valeur de flag vaut desormais 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 itO, 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'ob- tenir un mouvement circulaire (comme dans les exercices de la page 87). 8.27 Modifiez ce programme, ou bien ecrivez-en un autre similaire, de maniere a simuler le mouve- ment 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 fonction- nant 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 cer- tain nombre de dies 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 rebon- dissent les unes sur les autres ainsi que sur les parois. 90 Apprendre d programmer avec Python 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 deux 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 » (constitue en fait 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 fle- chees du clavier. Sur le canevas se trouvent egalement des « proies » (des petits cercles fixes dis- poses 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'al- longe d'un carre, le joueur gagne un point, et une nouvelle proie apparait ailleurs. La partie s'ar- rete 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 ». 9 Manipuler des fichiers Jusqu'd 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 fafon de proceder devient cependant tout a fait inadequate lorsque I'on souhaite traiter une quantite d'informations plus importante. Utilite des fichiers Imaginons par exemple que nous voulions ecrire un petit programme exerciseur qui fasse apparaitre a l'ecran des questions a choix multiple, avec traitement automatique des reponses de rutilisateur. Com- ment 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 malheureusement 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 selection- ner au hasard l'une ou l'autre de ces questions pour les presenter a rutilisateur. 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 ?" 92 Apprendre a programmer avec Python Rappel L 'indicage commence a partir de zero. 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 im- portant. En corollaire, nous accroitrons la probabilite d'inserer une erreur de syntaxe dans la defini- tion 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 sepa- rer 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 permet- tant de creer des fichiers, d'y envoyer des donnees et de les recuperer par la suite. Les langages de programmation proposent des jeux d'instructions plus ou moins sophistiques pour ef- fectuer ces taches. Lorsque les quantites de donnees deviennent tres importantes, il devient d'ailleurs ra- pidement necessaire de structurer les relations entre ces donnees, et Ton doit alors elaborer des sys- temes appeles bases de donnees relationnelles, dont la gestion peut s'averer tres complexe. Lorsque Ton est confronte a ce genre de probleme, il est d'usage de deleguer une bonne part du travail a des logiciels tres specialises tels que Oracle, IBM DB2, Sybase, Adabas, PostgreSQL, MySQL, etc. Python est parfaite- ment capable de dialoguer avec ces systemes, mais nous laisserons cela pour un peu plus tard (voir : gestion d'une base de donnees, page 235). Nos ambitions presentes sont plus modestes. Nos donnees ne se comptent pas encore par centaines de milkers, aussi nous pouvons nous contenter de mecanismes simples pour les enregistrer dans un fichier de taille moyenne, et les en extraire ensuite. Travailler avec des fichiers L'utilisation d'un fichier ressemble beaucoup a rutilisation 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 rutiliser, vous le refer- mez. 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 egalement aux fichiers informatiques. Un fichier se compose de donnees enregistrees sur votre disque dur, sur une disquette, une clef USB ou un CD-Rom. Vous y accedez grace a son nom (lequel peut inclure aussi un nom de repertoire). Vous pou- vez 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. 9. Manipuler des fichiers 93 Noms de fichiers - le repertoire courant Pour simplifler 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 re- pertoire 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 reper- toire courant, afin que celui-ci corresponde a vos attentes. Pour ce faire, utilisez les commandes sui- vantes en debut de session. Nous supposons pour la demonstration que le repertoire vise est le reper- toire /home/ jules/exercices . Meme si vous travaillez sous Windows (ou ce n'est pas la regie), vous pouvez 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 neces- saires, suivant que vous travaillez sous Mac OS, Linux, ou Windows^ 7 . »> 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 I'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. Les deux formes d'importation Les lignes d'instructions que nous venons d'utiliser sont l'occasion d'expliquer un mecanisme interes- sant. 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 ma- nieres 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 La premiere ligne de cet exemple importe Yintegralite du module os, lequel contient de nombreuses fonctions interessantes pour l'acces au systeme d'exploitation. La seconde ligne utilise la fonction 37 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. 94 Apprendre a programmer avec Python getcwdO du module os 38 . Comme vous pouvez le constater, la fonction getcwdO renvoie le nom du re- pertoire courant (getcwd = get current working directory). Par comparaison, voici un exemple similaire utilisant la seconde methode d'importation : »> from os import getcwd »> rep_cour = getcwdO »> print rep_cour C : \Python22\essais Dans ce nouvel exemple, nous n'avons importe du module os que la fonction getcwdO. 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 rutilisons, 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'inconve- nient (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'ori- gines diverses, il sera done toujours preferable de privilegier 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). Ecriture sequentielle dans un fichier Sous Python, l'acces aux fichiers est assure par l'intermediaire d'un objet-interface particulier, que Ton appelle objet-fichier. On cree cet objet a l'aide de la fonction integree open{) 39 . Celle-ci renvoie un objet dote de methodes specifiques, qui vous permettront de lire et ecrire dans le fichier. L'exemple ci-apres 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 automatique- ment. Par contre, si le nom utilise concerne un fichier preexistant qui contient deja des donnees, les ca- racteres que vous y enregistrerez viendront s'ajouter a la suite de ceux qui s'y trouvent deja. Vous pou- vez faire tout cet exercice directement a la ligne de commande : 38 Le point separateur exprime done ici une 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 ambiguite 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. 39 Une telle fonction, dont la valeur de retour est un objet particulier, est souvent appelee fonction-fabrique. 9. Manipuler des fichiers 95 »> obFichier = open ( 'Monf ichier ' , ' a ' ) >» obFichier .write ( ' Bon jour , f ichier !') »> obFichier. write ("Quel beau temps, aujourd'hui !") >» obFichier . close ( ) »> Notes • La premiere ligne cree Yobjet-fichier obFichier, lequel fait reference a un fichier veritable (sur disque ou disquette) dont le nom sera Monfichier. Attention : ne confondez pas le nom de fichier avec le nom de 1'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 dont vous pouvez visualiser le contenu a l'aide d'un editeur quelconque). • La fonction open() attend deux arguments, qui doivent tous deux 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 (c'est la raison pour laquelle on parle de fichier a acces sequentiel). Chaque nouvel appel de write() continue l'ecri- ture a la suite de ce qui est deja enregistre. • La methode closed referme le fichier. Celui-ci est desormais disponible pour tout usage. Lecture sequentielle d'un fichier Vous allez maintenant rouvrir le fichier, mais cette fois en lecture, de maniere a pouvoir y relire les in- formations 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 trans- fere dans une variable de type chaine de caracteres (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-fi- chier. 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 ou- vert en lecture (argument ' r'). 96 Apprendre a programmer avec Python • 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 dis- tinctes. • La methode read() peut egalement etre utilisee avec un argument. Celui-ci indiquera combien de ca- racteres 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 sim- plement a la fin du fichier : »> t = ofi. read (1000) >» print t beau temps, aujourd'hui ! Si la fin du fichier est deja atteinte, readO renvoie une chaine vide : »> t = of i. read () >» print t N'oubliez pas de refermer le fichier apres usage : »> ofi. close () 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 d'un 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 diffe- rente 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 9. Manipuler des fichiers 97 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 rinstruction break, laquelle permet eventuellement de mettre en place plu- sieurs mecanismes de sortie differents pour une meme boucle : while : instructions diverses if break instructions diverses if : break etc . Dans notre fonction copieFichierO, il est facile de voir que l'instruction break s'executera seulement lorsque la fin du fichier aura ete atteinte. 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 » 4 . II est tres facile de traiter ce genre de fichiers sous Python. Les instructions suivantes creent un fichier texte de quatre lignes : »> f = open ( "Fichier texte " , "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 sou- haite separer les lignes de texte dans l'enregistrement. Sans ce marqueur, les caracteres seraient enregis- tres 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 readlineO, 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. readlineO »> print t Ceci est la ligne un »> print f. readlineO Voici la ligne deux La methode readlinesO transfere toutes les lignes restantes dans une liste de chaines : »> t = f. readlinesO >» print t 40 Voir page 46 : Veracite/faussete d'une expression 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, Mac OS 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). 98 Apprendre a programmer avec Python ['Voici la ligne trois\012', 'Voici la ligne quatre\012'] »> f. close () Remarques • La liste apparait ci-dessus en format brut, avec des apostrophes pour delimiter les chaines, et les ca- racteres 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 readlinesO 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 integrale- ment 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 readlineO dans une boucle, comme le montrera Pexemple suivant. • Notez bien que readlineO est une methode qui renvoie une chaine de caracteres, alors que la me- thode readlinesO renvoie une liste. A la fin du fichier, readlineO renvoie une chaine vide, tandis que readlinesO 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) fs . 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') Enregistrement et restitution de variables diverses L'argument de la methode write() doit etre une chaine de caracteres. Avec ce que nous avons appris jus- qu'a present, nous ne pouvons done enregistrer d'autres types de valeurs qu'en les transformant d'abord en chaines de caracteres (string). 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 117). 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 nu- meriques lorsque nous allons relire le fichier. Exemple : »> a = 5 »> b = 2.83 9. Manipuler des fichiers 99 — \ — /-»7 »> C — D / »> f = open ( ' Monf ichier ' , ' w' ) »> f . write (str (a) ) »> f .write (str (b) ) »> f .write (str (c) ) »> f. close () »> f = open ( 'Monf ichier ' , '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 flchier ? 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 mo- dule Python specialise : le module pickle 42 . Voici comment il s'utilise : »> import pickle »> f = open ( 'Monf ichier ' , 'w') »> pickle, dump (a, f) »> pickle . dump (b , f ) »> pickle, dump (c, f) »> f. close () »> f = open ( 'Monf ichier ' , '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 () 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 va- riable 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 aisement comprendre ce que font exactement les fonctions du module pickle en effec- tuant une lecture « classique » du fichier resultant, a l'aide de la methode read() par exemple. 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 affichees, qui indiquent notamment a quel endroit du script I '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. 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. 100 Apprendre d programmer avec Python Dans de nombreux cas, il est possible de prevoir a l'avance certaines des erreurs qui risquent de se pro- duire a tel ou tel endroit du programme et d'inclure a cet endroit des instructions particulieres, qui se- ront 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 ins- tructions. Un mecanisme de ce type s'appelle en general mecanisme de traitement des exceptions. Celui de Py- thon utilise l'ensemble d'instructions try - except - else, qui permettent d'intercepter une erreur et d'exe- cuter 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 instruc- tion 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 pour- suivre 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 avertissement 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" Si nous estimons que ce genre de test est susceptible de rendre service a plusieurs endroits d'un pro- gramme, 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. Exercices 9.1 Ecrivez un script qui permette de creer et de relire aisement 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 (e'est-a-dire utiliser la touche seule). 9. Manipuler des fichiers 101 L'affichage du contenu devra montrer les lignes du fichier separees les unes des autres de la ma- niere la plus naturelle (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 diffe- rentes 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 multipli- cation 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. 9.5 Vous avez a votre disposition un fichier texte dont chaque ligne est la representation d'une va- leur 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 alternative - ment 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 deman- der 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 fi- chier contenant les coordonnees d'un certain nombre de personnes. Ecrivez un script qui per- mette d'extraire de ce fichier les lignes qui correspondent a un code postal bien determine. 9.11 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 99). Ces fonctions doivent permettre l'enregistrement de variables diverses dans un fichier texte, en les accompagnant systematiquement d'informations concernant leur format exact. 10 Approfondir les structures de donnees Jusqu'd present, nous nous sommes contentes d' operations asse% simples. Nous allons maintenant passer a la Vi- tesse superieure. Les structures de donnees que vous utilise^ deja presentent quelques caracteristiques que vous ne connaisse^pas encore, et il est egalement temps de vous faire decouvrir d'autres structures plus complexes. Le point sur les chaines de caracteres Nous avons deja rencontre les chaines de caracteres au chapitre 5. A la difference des donnees nume- riques, qui sont des entites singulieres, les chaines de caracteres constituent un type de donnee compo- site. Nous entendons par la une entite bien definie qui est faite elle-meme d'un 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 indivi- duel. En fait, les chaines de caracteres font partie d'une categorie d'objets Python que Ton appelle des se- quences, et dont font partie aussi les listes et les tuples. On peut effectuer sur les sequences tout un en- semble d'operations similaires. Vous en connaissez deja quelques unes, et nous allons en decrire quelques autres dans les paragraphes suivants. Indicage, extraction, longueur Petit rappel du chapitre 5 : 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 nu- merotes) de la meme maniere, c'est-a-dire a partir de zero. Pour extraire un caractere d'une chaine, il suffit d'accoler au nom de la variable qui contient cette chaine, son indice entre crochets : »> nom = ' Cedric 1 >» print nom[l] , nom [3] , nom [5] e r c Si Ton desire determiner le nombre de caracteres presents dans une chaine, on utilise la fonction inte- gree len() : »> print len (nom) 6 Nous avons cependant deja attire votre attention sur le fait que cela ne marche pas toujours comme prevu. Les resultats mentionnes dans les deux exemples ci-dessus seront peut-etre ceux-la, mais seule- 104 Apprendre a programmer avec Python ment si vous travaillez dans un environnement fonctionnant toujours avec une norme d'encodage an- cienne, telle que Windows-1252 ou ISO-8859-1 (Latin-1). Par contre, si vous utilisez un poste de travail moderne, configure de maniere a utiliser l'encodage Utf-8 par defaut, vous obtiendrez plutot : »> nom = 'Cedric' »> print nom[l] , nom [3] , nom [5] ❖ d i »> print len (nom) 7 Ces resultats genants peuvent etre evites, mais il faut pour cela approfondir nos connaissances. Les types string et Unicode A l'origine du developpement des technologies informatiques, alors que les capacites de memorisation des ordinateurs etaient encore assez limitees, on n'imaginait pas que ceux-ci seraient utilises un jour pour traiter d'autres textes que des communications techniques, essentiellement en anglais. II semblait done tout-a-fait raisonnable de ne prevoir pour celles-ci qu'un jeu de caracteres restreint, de maniere a pouvoir representer chacun de ces caracteres avec un petit nombre de bits, et ainsi occuper aussi peu d'espace que possible dans les couteuses unites de stockage de l'epoque. Le jeu de caracteres ASCII 43 fut done choisi en ce temps la, avec l'estimation que 128 caracteres suffiraient (a savoir le nombre de com- binaisons possibles pour des groupes de 7 bits 44 ). En l'etendant par la suite a 256 caracteres, on put l'adapter aux exigences du traitement des textes ecrits dans d'autres langues que l'anglais, mais au prix d'une dispersion des normes (ainsi par exemple, la norme ISO-8859-1 codifie tous les caracteres accen- tues du francais ou de l'allemand (entre autres), mais aucun caractere grec, hebreu ou cyrillique. Pour ces langues, il faudra respectivement utiliser les normes ISO-8859-7, ISO-8859-8, ISO-8859-5, bien evi- demment incompatibles, et d'autres normes encore pour l'arabe, le tcheque, le hongrois... L'interet residuel de ces normes anciennes reside dans leur simplicite. Elles permettent en effet aux de- veloppeurs d'applications informatiques de considerer que chaque caractere typographique est assimi- lable a un octet, et que par consequent une chaine de caracteres n'est rien d'autre qu'une sequence d'oc- tets. C'est ainsi que fonctionne le type de donnees string de Python. Toutefois, comme nous l'avons deja evoque sommairement au chapitre 5, les applications informa- tiques modernes ne peuvent plus se satisfaire de ces normes etriquees. II faut desormais pouvoir enco- der, dans un meme texte, tous les caracteres de n'importe quel alphabet de n'importe quelle langue. Une organisation internationale a done ete creee : le Consortium Unicode, laquelle a effectivement deve- loppe une norme universelle sous le nom de Unicode. Cette nouvelle norme vise a donner a tout carac- tere de n'importe quel systeme d'ecriture de langue un nom et un identifiant numerique, et ce de ma- niere unifiee, quelle que soit la plate-forme informatique ou le logiciel. Une difficulte se presente, cependant. Se voulant universelle, la norme Unicode doit attribuer un identi- fiant numerique different a plusieurs dizaines de milliers de caracteres. Tous ces identifiants ne pour- ront evidemment pas etre encodes sur un seul octet. A premiere vue, ils serait done tentant de decreter qu'a l'avenir, chaque caractere devra etre encode sur deux octets (cela ferait 65536 possibilites), ou trois (16777216 possibilites) ou quatre (plus de 4 milliards de possibilites). Chacun de ces choix rigides en- traine cependant son lot d'inconvenients. Le premier, commun a tous, est que Ton perd la compatibilite avec la multitude de documents informatiques preexistants (et notamment de logiciels), qui ont ete en- codes aux normes anciennes, sur la base du paradigme « un caractere egale un octet ». Le second est lie a l'impossibilite de satisfaire deux exigences contradictoires : si Ton se contente de deux octets, on 43 ASCII = American Standard Code for Information Interchange 44 En fait, on utilisait deja les octets a l'epoque, mais l'un des bits de l'octet devait etre reserve comme bit de controle pour les systemes de rattrapage d'erreur. L'amelioration ulterieure de ces systemes permit de liberer ce tantieme bit pour y stocker de l'information utile : cela autorisa l'extension du jeu ASCII a 256 caracteres (normes ISO-8859, etc.). 10. Approfondir les structures de donnees 105 risque de manquer de possibilities pour identifier des caracteres rares ou des attributs de caracteres qui seront probablement souhaites dans l'avenir ; si Ton impose trois, quatre octets ou davantage, par contre, on aboutit a un monstrueux gaspillage de ressources : la plupart des textes courants se satisfai- sant d'un jeu de caracteres restreint, l'immense majorite de ces octets ne contiendraient en effet que des zeros. Afin de ne pas se retrouver piegee dans un carcan de ce genre, la norme Unicode ne fixe aucune regie concernant le nombre d'octets ou de bits a reserver pour l'encodage. Elle specifie seulement la valeur numerique de l'identifiant associe a chaque caractere. En fonction des besoins, chaque systeme infor- matique est done libre d'encoder « en interne » cet identifiant comme bon lui semble, par exemple sous la forme d'un entier ordinaire. Comme tous les langages de programmation modernes, Python s'est done pourvu d'un type de donnees « chaine de caracteres Unicode » que nous designerons desormais comme le type Unicode. Par opposition au type string, qui doit etre compris plutot comme une sequence d'octets (representes il est vrai par des caracteres, mais seulement lorsque e'est possible), le type Unicode peut etre veritable - ment etre considere comme une sequence de caracteres, avec un statut identique pour chacun d'eux. Dans une chaine Unicode en effet, tous les caracteres sont traites de la meme maniere, qu'il s'agisse de caracteres ASCII standards, de caracteres accentues, grecs, cyrilliques, etc. Par exemple, si nous modi- fions un tout petit peu les deux derniers exemples de la rubrique precedente (en supposant toujours que nous travaillons sur un terminal moderne utilisant l'encodage Utf-8), nous pouvons obtenir les resultats attendus : »> nom = u ' Cedric ' »> print nom[l] , nom [3] , nom [5] e r c »> print len (nom) 6 Qu'avons nous change, au juste ? A vrai dire, pas grand chose : juste un petit prefixe u accole a la chaine litterale qui assigne un contenu a la variable nom. Ce prefixe devant une chaine litterale indique a l'interpreteur Python que la variable a laquelle on affecte cette valeur doit etre initialisee comme une va- riable de type Unicode, et que la chaine litterale qui suit doit done etre convertie au format interne Uni- code. Pour effectuer cette conversion, il est clair que l'interpreteur doit savoir dans quel encodage cette chaine litterale lui est fournie. Si vous travaillez a la ligne de commande, comme e'est le cas dans notre exemple, Python considere que la chaine est encodee avec l'encodage par defaut du terminal utilise 45 . A l'interieur d'un script, par contre, Python considere que l'encodage utilise pour toutes les chaines litte- rales de ce script est celui qui est declare a la premiere ou a la deuxieme ligne, dans un « pseudo-com- mentaire » du genre : -*- coding:Utf-8 -*- ou -*- coding :Latin-l -*- . L'encodage Utf-8 A ce stade de nos explications, il devient urgent de preciser encore quelque chose. Nous avons vu que la norme Unicode ne fixe en fait rien d'autre que des valeurs numeriques, pour tous les identifiants standardises destines a designer de maniere univoque les caracteres des alphabets du monde entier (plus de 240000 en novembre 2005). Elle ne precise en aucune facon la maniere dont ces valeurs numeriques doivent etre encodees concretement sous forme d'octets ou de bits. Pour le fonctionnement interne des applications informatiques, cela n'a pas d'importance. Les concep- teurs de langages de programmation, de compilateurs ou d'interpreteurs pourront decider librement de representer ces caracteres par des entiers sur 8, 16, 24, 32, 64 bits, ou meme (bien que Ton n'en voie pas l'interet !) par des reels en virgule flottante : e'est leur affaire et cela ne nous concerne pas. Nous ne de- Si vous ne connaissez pas l'encodage utilise par votre terminal, voyez page 30. 106 Apprendre a programmer avec Python vons done pas nous preoccuper du format reel des caracteres, a l'interieur d'une chaine Unicode de Py- thon. II en va tout autrement, par contre, pour les entrees/sorties. Les developpeurs que nous sommes de- vons absolument pouvoir preciser sous quelle forme exacte les donnees doivent etre introduites dans nos programmes, que ce soit par l'intermediaire de frappes au clavier ou par importation depuis une source quelconque. De meme, nous devons pouvoir choisir le format des donnees que nous exportons vers n'importe quel dispositif peripherique, qu'il s'agisse d'une imprimante, d'un disque dur, d'un ecran... Pour toutes ces entrees ou sorties de chaines de caracteres, nous devrons done toujours considerer qu'il s'agit concretement de sequences d'octets, et done utiliser le type string de Python, en veillant a gerer convenablement I'encodage qui y sera utilise. Meme si cela peut vous paraitre a premiere vue un peu complique, dites-vous bien que malheureuse- ment I'encodage ideal n'existe pas. En fonction de ce que Ton veut en faire, il peut etre preferable d'en- coder un meme texte de plusieurs manieres differentes. C'est pour cette raison qu'ont ete definies, en parallele avec la norme Unicode, plusieurs normes d'encodage : Utf-8, Utf-16, Utf-32, et quelques va- riantes. Ne vous affolez pas, cependant : vous ne serez vraisemblablement jamais confronte qu'a la pre- miere d'entre elles. Les autres ne concerneront que certains specialistes de domaines « pointus ». La norme d'encodage Utf-8 est desormais la norme preferentielle pour la plupart des textes courants, parce que : • d'une part, elle assure une parfaite compatibilite avec les textes encodes en « pur » ASCII (ce qui est le cas de nombreux codes sources de logiciels), ainsi qu'une compatibilite partielle avec les textes encodes a l'aide de ses variantes « etendues », telles que Latin-1 ; • d'autre part, cette nouvelle norme est celle qui est la plus econome en res sources, tout au moins pour les textes ecrits dans une langue occidentale. Suivant cette norme, les caracteres du jeu ASCII standard sont encodes sur un seul octet. Les autres se- ront encodes en general sur deux octets, parfois trois ou meme quatre pour les caracteres plus rares. Conversion (encodage/decodage) des chaines Nous entrons a present dans le vif du sujet. Pour leur traitement a l'interieur d'un script Python, il fau- dra frequemment convertir les chaines de caracteres, du type string au type Unicode, ou vice-versa. Vous devez done savoir comment effectuer ces conversions. Python vous fournit fort heureusement les outils necessaires, sous la forme de methodes des objets concernes. Conversion d'une chaine string en chaine Unicode Considerons un script dans lequel on utilise la fonction interne raw JnputO pour accepter les entrees d'un utilisateur. Cette fonction renvoie toujours une valeur de type string, laquelle est encodee en Latin-1, en Utf-8, ou encore une autre norme, suivant I'encodage utilise par defaut sur le poste de travail de cet utilisateur. Supposons par exemple qu'il s'agisse d'Utf-8. Pour convertir cette chaine Utf-8 en chaine Unicode, vous appliquerez a cet objet string la methode decoded, avec l'argument "utf-8" (Vous pouvez aussi utiliser "utf-8", "Utf8" ou "utf8"). Exemple : print "Veuillez entrer une chaine avec des caracteres accentues , svp : " chs = raw_input() # la chaine est entree est un objet de type string chu = chs .decode ( "utf-8" ) # conversion en chaine Unicode (decodage) A la troisieme ligne de cet exemple, la variable chu se voit affecter une chaine de type Unicode. Vous pouvez vous en assurer, par exemple en extrayant de cette chaine l'un ou l'autre caractere accentue, ou en testant sa longueur comme nous l'avons fait dans les exemples precedents. Vous pouvez aussi plus simplement faire appel a la fonction interne type(). Ainsi par exemple, l'instruction : 10. Approfondir les structures de donnees 107 print type(chs), type(chu) ajoutee a la fin du petit script script ci-dessus, provoquerait l'affichage : Si le poste de travail de l'utilisateur utilise l'encodage Latin-1 (Cas de nombreux postes Windows), vous pouvez egalement decoder la chaine entree. II vous suffit de remplacer l'argument "utf-8" par "Latin-l" (ou encore "Latinl", "latin-1", "latinl"). Conversion d'une chaine Unicode en chaine string Considerons a present que vous souhaitiez memoriser une chaine de caracteres dans un fichier texte. Si la chaine a memoriser est du type Unicode dans votre script, vous devez d'abord choisir un encodage, et la convertir en string avant de pouvoir l'enregistrer. Pour ce faire, vous appliquerez a cet objet Unicode sa methode encoded, avec l'argument correspondant a l'encodage souhaite. Par exemple, pour enregis- trer des chaines avec l'encodage Utf-8, vous ferez : chu = u"Chaine avec accents : deja Noel !" # chaine Unicode chs = chu. encode ("utf 8") # conversion Unicode -> string of = open("MonFichier" , "w") of .write (chs) of . close () Attention : Vous ne pouvez pas enregistrer une chaine Unicode telle quelle dans un fichier texte, car le choix d'un encodage est indispensable. Si vous essayez par exemple : chu = u"Chaine avec accents : deja Noel ! " # chaine Unicode of = open("MonFichier" , "w") of .write (chu) of . close () vous allez obtenir un message d'erreur similaire au suivant : Traceback (most recent call last) : File "test.py", line 5, in of .write (chu) UnicodeEncodeError : ' ascii ' codec can ' t encode character u ' \xee ' in position 3 : ordinal not in range (128) En effet, du fait que vous ne precisez aucun encodage, Python essaie d'encoder la chaine Unicode chu avec l'encodage par defaut, a savoir presque toujours ASCII 46 . Cela ne marche evidemment pas avec les caracteres accentues, ce qu'indique assez clairement le message d'erreur 47 . Conversion d'une chaine Utf-8 en Latin-1, ou vice-versa Si vous avez compris le fonctionnement des methodes decodeO et encodeO decrites dans les rubriques precedentes, vous pouvez bien evidemment les appliquer a la conversion d'une chaine de type string, d'un format d'encodage a un autre : # Conversion Utf-8 -> Latin-1 chLat = chUtf . decode ( ' Utf 8 ' ) . encode ( ' Latinl ' ) 46 Attention : cet encodage par defaut de Python reste toujours ASCII, meme si vous avez pris la peine d'indiquer votre encodage par defaut en debut de script, a l'aide d'une ligne telle que : # -*- coding:Utf-8 -*- Une ligne de ce genre indique en effet a Python l'encodage que vous utilisez pour toutes les chaines de texte litterale, les commentaires, etc. que vous inserez vous-meme dans votre script. Elle ne force pas l'encodage des entrees/sorties. II est possible de modifier ce comportement par defaut de Python, mais cela sort du cadre de ce manuel. 47 En informatique, on appelle codec (codeur/decodeur) tout dispositif de conversion de format. Vous rencontrerez par exemple de nombreux codecs dans le monde du multimedia (codecs audio, video...). Python dispose de nombreux codecs pour convertir les chaines de caracteres suivant les differentes nonnes en vigueur. 108 Apprendre a programmer avec Python Vous remarquerez encore une fois que la composition d'instructions permet d'eviter le passage par une variable intermediate. La chaine d'origine chutf est convertie en Unicode, puis re-convertie en string, en une seule operation. # Conversion Latin-1 -> Utf-8 chOtf = chLat . decode ( ' Latinl ' ) . encode ( ' Utf 8 ' ) Quand faut-il convertir ? Si vous travaillez dans un environnement « ancien » utilisant toujours l'encodage ISO-8859-1 par defaut (ce qui est encore le cas de nombreux postes de travail fonctionnant sous Windows), vous pouvez peut- etre provisoirement ignorer la question, et vous contenter de traiter toutes vos chaines de caracteres en objets de type string. Ce n'est cependant pas une bonne idee si vous avez l'ambition de vous tenir au courant de revolution des techniques informatiques. Vous devrez tot ou tard vous preoccuper de rendre vos programmes compatibles avec les exigences de l'internationalisation. Et le plus tot sera le mieux. Si votre poste de travail fonctionne avec un systeme d'exploitation aux normes modernes, tel Ubuntu Linux, par exemple, vous n'avez deja plus le choix : vous devez comprendre ce que sont les types string et Unicode de Python, et vous devez maitriser leurs conversions reciproques. Ce n'est finalement pas tres complique ; en resume, il faut retenir que : • Toutes les entrees/sorties de chaines de caracteres doivent etre du type string. • Avant d'effectuer sur une chaine toute operation necessitant un acces a ses caracteres individuels, il faut imperativement la convertir au prealable en objet Unicode. • Les operations qui ne necessitent pas un acces aux caracteres individuels peuvent souvent encore etre effectuees directement sur les chaines de type string, sans conversion prealable en Unicode. • Convertir une chaine Unicode en string constitue un encodage. On l'effectue a l'aide de la methode encodeO appliquee a cet objet. Exemple : chs = chu. encode ('Latin-1' ). • Convertir une chaine de type string en Unicode constitue un decodage. On l'effectue en appliquant la methode decodeO a cet objet. Exemple : chu = chs . decode ( ' utf-8 ' ) . Dans la suite de ce chapitre, nous rencontrerons divers exemples d'application de ces regies. Vous vous familiariserez vite avec celles-ci. Remarque A I'avenir, il sera certainement plus rationnel d'effectuer toutes les manipulations de chaines a I'interieur de vos programmes a l'aide du seul type Unicode, et de reserver le type string aux entrees/sorties. Dans la suite de ce livre, cependant, nous utiliserons encore beaucoup le type string, essentiellement pour des raisons « historiques » : la plus grande grande partie des scripts que nous vous y presentons comme exemples ont en effet ete developpes sur des machines utilisant toujours la norme ancienne Latin-1. Nous n'y avons ajoute des conversions qu'aux seuls endroits ou cela s'averait indispensable pour que ces scripts s'executent correctement sur des machines recentes. Dans vos propres scripts, prenez I'habitude d'utiliser preferentiellement le type Unicode pour traiter toutes vos chaines de caracteres. Extraction de fragments de chaines II arrive frequemment, lorsque Ton travaille avec des chaines, que Ton souhaite extraire une petite chaine d'une chaine plus longue. Python propose pour cela une technique simple que Ton appelle sli- cing (« 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 : 10. Approfondir les structures de donnees 109 »> ch = ' 'Juliette" »> print ch[0:3] Jul Dans la tranche [n,m], le n-ieme caractere est inclus, mais pas le m-ieme. Si vous voulez memoriser aise- ment ce mecanisme, il faut vous representer que les indices pointent des emplacements situes entre les caracteres, comme dans le schema ci-dessous : J u I. t. e 1 1 e 1 1. 2 ?, A , 5 6 7 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 ce qui suit les 3 premiers caracteres iette Attention : comme nous l'avons explique dans les rubriques precedentes, les manipulations de chaines qui impliquent un acces a leurs caracteres individuels ne fonctionneront pas correctement sur les chaines de type string, si celles-ci sont encodees suivant une norme recente telle que Utf-8. Avant d'ef- fectuer une operation de slicing sur une chaine, comme decrit ci-dessus, il faudra done dorenavant veiller a convertir d'abord celle-ci en Unicode. Exemple (test effectue sous encodage Utf-8) : >» chs = 'Adelaide' »> print chs[:3], chs [4: 8] Ad^ lai Ce resultat est a l'evidence incorrect. Mais si nous faisons : »> chu = chs . decode ( ' Utf-8 1 ) »> print chu[:3] , chu[4:8] Ade aide Le resultat est cette fois tout a fait correct. Si la sous-chaine a extraire doit etre ensuite re-encodee en objet de type string, nous pouvons utiliser la composition pour ecrire une instruction compacte : »> chsl = 'Cunegonde' »> chs2 = chsl .decode ( 'utf8 ' ) [:4] . encode ( 'utf 8 ' ) »> print chsl, type (chsl), chs2 , type(chs2) Cunegonde Cune A la deuxieme ligne de cet exemple, la chaine chsl est d'abord convertie en Unicode, puis une tranche de 4 caracteres en est extraite par slicing, et enfin cette tranche est elle-meme reconvertie en string (en- code au format Utf-8). Concatenation, repetition Les chaines peuvent etre concatenees avec l'operateur + et repetees avec l'operateur * : >» n = 'abc' + 1 'def ' # concatenation »> m = ' zut ! ' * 4 # repetition »> print n, m abedef zut ! zut ! zut ! 1 zut ! Remarquez au passage que les operateurs + et * peuvent aussi etre utilises pour l'addition et la multipli- cation lorsqu'ils s'appliquent a des arguments numeriques. Le fait que les memes operateurs puissent 110 Apprendre a programmer avec Python fonctionner differemment en fonction du contexte dans lequel on les utilise est un mecanisme fort inte- ressant 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 concate- nation, par exemple. Exercices 10.1 Determinez vous-meme ce qui se passe, dans la technique de slicing, 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. La chaine doit pouvoir contenir des caracteres accentues. 10.3 Tachez d'ecrire une petite fonction trouveO qui fera exactement le contraire de ce que fait l'ope- rateur d'indexage (c'est-a-dire les crochets [ ]). Au lieu de partir d'un index donne pour retrou- ver le caractere correspondant, cette fonction devra retrouver l'index correspondant a un carac- tere 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 : il 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. Les caracteres accentues doivent etre acceptes. 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 compteCarO qui compte le nombre d'occurrences d'un caractere donne dans une chaine. Ainsi : print compteCar ("ananas au jus", "a") devra afficher : 4 print compteCar ("Gedeon est deja la","e") devra afficher : 3. 10.6 Ecrivez un programme qui recopie un fichier source, encode a l'origine en Latin-1, dans un nouveau fichier destinataire, encode cette fois en Utf-8, en effectuant au passage un traitement de toutes ses lignes : dans celles-ci, chaque caractere « espace » sera remplace par le groupe de 3 caracteres « -*- ». Le programme demandera les noms des fichiers a l'utilisateur. Parcours (Tune 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 pou- vons envisager d'encoder un tel parcours a l'aide d'une boucle, articulee autour de l'instruction while : nom = u ' Josephine ' # chaine Unicode, de preference index = 0 while index < len (nom) : 10. Approfondir les structures de donnees 111 print nom[index] + ' *', index = index +1 Cette boucle parcourt done la chaine nom pour en extraire un a un tous les caracteres, lesquels sont en- suite 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 0 a 9. Le parcours d'une sequence est une operation tres frequente en programmation. Pour en faciliter l'ecri- ture, Python vous propose une structure de boucle plus appropriee que la boucle while, basee sur le couple destructions for ... in ... : Avec ces instructions, le programme ci-dessus devient : nom = u ' Josephine ' 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 (e'est Python qui s'en charge). La structure for ... in ... ne montre done que l'essentiel, a savoir que 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 donnee. Dans l'exemple ci-dessus, la sequence etait une chaine de carac- teres. 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', u' elephant'] 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 longueur de la chaine elephant = 8 L'instruction for ... in ... : est un nouvel exemple & instruction composee. N'oubliez done pas le double point obligatoire a la fin de la ligne, et l'indentation pour le 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 (rappelons en effet que dans le cas d'une liste, tous les elements ne sont pas necessaire- ment du meme type). Exemple : divers = [ ' cheval ' , u'lezard', 3, 17.25, [5, 'Jean']] for e in divers : print e, type(e) L'execution de ce script donne : cheval lezard 3 17.25 [5, 'Jean'] 112 Apprendre a programmer avec Python Bien que les elements de la liste divers soient tous de types differents (une chaine de caracteres string, une chaine Unicode, un entier, un reel, une liste), on peut affecter successivement leurs contenus a la va- riable e, sans qu'il s'ensuive des erreurs (ceci est rendu possible grace au typage dynamique des variables Python). Exercices 10.7 Dans un conte americain, huit petits canetons s'appellent respectivement : Jack, Kack, Lack, Mack, Nack, Oack, Pack et Qack. Ecrivez un petit script qui genere tous ces noms a partir des deux chaines suivantes : prefixes = ' JKLMNOP ' et suf fixe = 'ack' Si vous utilisez une instruction for ... in votre script ne devrait comporter que deux lignes. 10.8 Ecrivez un script qui recherche le nombre de mots contenus dans une phrase donnee. 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 alpha- betique fait partie d'un groupe bien determine : car = "e" voyelles = "aeiouyAEIOOY" 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 I'aide d'une boucle classique utilisant l'instruction while. Exercices 10.9 Ecrivez une fonction chiffreO qui renvoie « vrai » si l'argument transmis est un chiffre. 10.10 Ecrivez une fonction majuscule! ) qui renvoie «vrai» si l'argument transmis est une majuscule. Tachez de tenir compte des majuscules accentuees ! 10.11 Ecrivez une fonction chaineListeO qui convertisse une phrase en une liste de mots. 10.12 Utilisez les fonctions definies dans les exercices precedents pour ecrire un script qui puisse ex- traire d'un texte tous les mots qui commencent par une majuscule. 10.13 Utilisez les fonctions definies dans les exercices precedents pour ecrire une fonction qui renvoie le nombre de caracteres majuscules contenus dans une phrase donnee en argument. Les chaines 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 intuitivement a remplacer une lettre dans une chaine) : 10. Approfondir les structures de donnees 113 salut = 'bonjour a tous ' salut[0] = 'B' print salut Le resultat attendu par le programmeur qui a ecrit ces instructions est « Bonjour a tous ». Mais contrai- rement a ses attentes, ce script leve une erreur du genre : TypeError-. object doesn't support item assign- ment. 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 parfaitement : salut = 'bonjour a tous' salut = 'B' + salut[l:] print salut Dans cet autre exemple, en effet, nous ne modifions pas la chaine salut. Nous en re-creons une nou- velle 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). Les chaTnes 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 peut vous etre 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 dans toutes les normes d'encodage, les codes numeriques representant les caracteres ont ete attribues dans l'ordre alphabetique, tout au moins pour les caracteres non accentues. Dans le systeme de codage ASCII, par exemple, A=65, B=66, C=67, etc. Comprenez cependant que cela ne fonctionne bien que pour des mots qui sont tous entierement en mi- nuscules, ou entierement en majuscules, et qui ne comportent aucun caractere accentue. Vous savez en effet que les majuscules et minuscules utilisent des ensembles de codes distincts. Quant aux caracteres accentues, vous avez vu qu'ils sont encodes en dehors de l'ensemble constitue par les caracteres du standard ASCII. Construire un algorithme de tri alphabetique qui prenne en compte a la fois la casse des caracteres et tous leurs accents n'est done pas une mince affaire ! Classement des caracteres A ce stade, il peut etre utile de s'interesser aux valeurs des identifiants numeriques associes a chaque ca- ractere. Cela peut simplifier parfois certains algorithmes, notamment dans le cas ou les chaines traitees n'utilisent que des caracteres strictement ASCII. Afin que vous puissiez effectuer plus aisement 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 (string ou Unicode) comme argument. En retour, elle fournit la valeur de l'identifiant numerique correspondant a ce caractere. Ainsi ord('A' ) renvoie la valeur 65, et ord(u' i' ) renvoie la valeur 296. La fonction unichr(num) fait exactement le contraire, pour toute valeur « legale » num correspondant ef- fectivement a un caractere dans la norme Unicode. Ainsi, par exemple, unichr(65) renvoie le caractere A, et unichr (1046) renvoie le caractere cyrillique >K. 114 Apprendre a programmer avec Python La fonction chr(num) fait la meme chose, mais uniquement pour les caracteres strictement ASCII. L'ar- gument qu'on lui transmet doit done etre un entier compris entre 32 et 127. Cette fonction obsolete ne devrait plus etre utilisee desormais. Vous pouvez exploiter ces fonctions predefinies pour explorer le jeu de caracteres disponible sur votre ordinateur. Vous pouvez par exemple retrouver les caracteres minuscules de l'alphabet grec, en sachant que les codes qui leur sont attribues vont de 945 a 969. Ainsi le petit script ci-dessous : s = U"" # chaine Unicode vide i = 945 # premier code while i <= 969: # dernier code s += unichr(i) i = i + 1 print "Alphabet grec (minuscule) : " , s devrait afficher le resultat suivant : Alphabet grec (minuscule) aPy6e£n6 iKAuv£onp 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 effectue done 1'action 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 " ".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) 26 Attention ! Comme on peut le constater dans l'exemple ci-dessus, le resultat est incorrect si la chaine traitee est de type string et encodee en Utf-8, car dans cet encodage les caracteres non-ASCII sont codes sur 2 octets. Le resultat correct n'est garanti que pour les chaines Unicode : »> chl = u"Cette lecon vaut bien un fromage, sans doute ?" »> ch2 = u" 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 = "CELIMENE est un prenom ancien" »> print ch. lower () cElimEne est un prenom ancien On voit que le resultat n'est pas parfait avec le type string. Essayons en Unicode : »> ch = u" CELIMENE est un prenom ancien" »> print ch . lower ( ) celimene est un prenom ancien • upper() : convertit une chaine en majuscules : >» ch = "Maitre Jean-Noel Hebert" »> print ch . upper ( ) MAiTRE JEAN-NOeL HeBeRT »> ch = u"Maitre Jean-Noel Hebert" »> print ch . upper ( ) MAITRE JEAN-NOEL HEBERT Meme remarque que pour la fonction precedente : il faut privilegier Unicode. • title() : convertit en majuscule l'initiale de chaque mot : 116 Apprendre a programmer avec Python »> ch ="albert rene elise veronique" »> print ch. title () Albert Rene eLise VeRonique »> ch =u" albert rene elise veronique" »> print ch. title () Albert Rene Elise Veronique Meme remarque encore. • capitalizeO : variante de la precedente. Convertit en majuscule seulement la premiere lettre : »> b3 = "quel beau temps, aujourd'hui !" »> print b3 . capitalize () "Quel beau temps, aujourd'hui !" • swapcaseO : convertit toutes les majuscules en minuscules, et vice-versa : >» ch = "Le Lievre Et La Tortue" »> print ch . swapcase ( ) IE lleVRE eT 1A tORTUE »> ch = u"Le Lievre Et La Tortue" »> print ch . swapcase ( ) IE 1IEVRE eT 1A tORTUE • stripO : 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(car) : retrouve l'index de la premiere occurrence du caractere car dans la chaine : »> ch9 ="Portez ce vieux whisky au juge blond qui fume" >» print ch9. index ("w") 16 Comme signale deja a plusieurs reprises, une fonction de ce genre peut renvoyer un resultat incor- rect avec certaines chaines de type string. Dans la plupart de ces methodes, il est possible de preciser quelle portion de la chaine doit etre traitee, en ajoutant des arguments supplementaires. Exemples : »> 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. Comprenez bien qu'il n'est pas possible de decrire toutes les methodes disponibles, ainsi que leur para- metrage, dans le cadre restreint de ce cours. Si vous souhaitez en savoir davantage, il vous faut consul- ter la documentation en ligne de Python (Library reference), ou un bon ouvrage de reference. 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 : 10. Approfondir les structures de donnees 117 • len(ch) renvoie la longueur de la chaine ch. Comprenez bien qu'il ne s'agit toujours du nombre de caracteres que pour une chaine de type Unicode. Pour une chaine de type string, il s'agit du nombre d'octets, lequel peut etre tres different, en particulier avec les encodages Utf-16 et Utf-32. • 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" ) # Attention : pas de virgule decimale ! »> print a + 5 17.36 • int(ch) convertit la chaine ch en un nombre entier : »> a = int("184") »> print a + 20 204 • str(obj) convertit l'objet obj en une chaine de type string, obj peut etre une donnee de tout type : »> n, 1 = 15, [17.334, "zut"] »> ch = str(n) + " est un entier, et " + str(l) + " est une liste." »> print ch 15 est un entier, et [17.334, 'zut'] est une liste. • unicode(obj) convertit l'objet obj en une chaine de type Unicode. Meme utilite que ci-dessus. »> n, 1 = 15, [17.334, "zut"] »> ch = Unicode (n) + " est un entier, et " + Unicode (1) + " est une liste." »> print ch, type(ch) 15 est un entier, et [17.334, 'zut'] est une liste. Remarquons au passage que la concatenation de chaines des types string et Unicode est possible, le resultat etant dans ce cas une chaine du type Unicode. Formatage des chaines de caracteres Pour terminer ce tour d'horizon des fonctionnalites associees aux chaines de caracteres, il nous semble interessant de vous presenter encore une technique que Ton appelle formatage des chaines. Celle-ci se revele particulierement utile dans tous les cas ou vous devez construire une chaine de caracteres com- plexe a partir d'un certain nombre de morceaux, tels que les valeurs de variables diverses. Considerons par exemple que vous ayez ecrit un programme qui traite de la couleur et de la tempera- ture d'une solution aqueuse, en chimie. La couleur est memorisee dans une chaine de caracteres nom- inee coul, et la temperature dans une variable nommee temp (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 concatena- tion (le symbole +), mais il vous faudra aussi utiliser plusieurs fois l'une des fonctions integrees str() ou unicodeO 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 ele- ments a l'aide de l'operateur % . A gauche de cet operateur, vous fournissez une chaine de formatage (un patron, en quelque sorte) qui contient des marqueurs de conversion, et a droite (entre parentheses) le ou les 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 rem- places respectivement par les contenus des deux variables coul et temp. 118 Apprendre a programmer avec Python Le marqueur %s accepte n'importe quel objet (chaine, entier, float, liste...). Vous pouvez experimenter d'autres mises en forme, plus interessantes encore, 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 nombres reels et peuvent deter- miner la largeur et la precision qui seront affichees. La description complete de toutes ces 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 ma- nuels plus specialises. Le formatage fonctionne bien evidemment aussi avec des chaines Unicode : »> c = u"Couleur = %s - temperature = %s°C" % ('rouge', 14.783) »> print c, type(c) Couleur = rouge - temperature = 14.783°C Exercices 10.21 Ecrivez un script qui recopie en Utf-8 un ficbier texte encode a l'origine en Latin-1, en veillant en outre a ce que chaque mot commence par une majuscule. 10.22 Ecrivez un script qui compte le nombre de mots contenus dans un fichier texte. 10.23 Ecrivez un script qui compte dans un fichier texte quelconque le nombre de lignes contenant des caracteres numeriques. 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 d'un 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 caracteris- tiques de ces spheres (surface de section, surface exterieure et volume), dans des phrases telles que : Diam. 46.20 cm Section 1676.39 cm 2 Surf. 6705.54 cm 2 Vol. 51632.67 cm 3 Diam. 120.00 cm Section 11309.73 cm 2 Surf. 45238.93 cm 2 Vol. 904778.68 cm 3 Diam. 0.03 cm Section 0.00 cm 2 Surf. 0.00 cm 2 Vol. 0.00 cm 3 Diam. 13.90 cm Section 151.75 cm 2 Surf. 606.99 cm 2 Vol. 1406.19 cm 3 Diam. 88.80 cm Section 6193.21 cm 2 Surf. 24772.84 cm 2 Vol. 366638.04 cm 3 etc . 10.26 Vous avez a votre disposition un fichier texte dont les lignes representent des valeurs nume- riques 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 pou- vant etre que 0 ou 5 (rarrondi doit etre correct). 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 in- dique l'emplacement de 1' objet dans la sequence). 10. Approfondir les structures de donnees 119 Definition d'une liste - acces a ses elements Vous savez deja que Ton delimite une liste a l'aide de crochets : »> n ombres = [5, 38, 10, 25] »> mots = [ " j ambon " , " f r omage " , "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. 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'indicage) 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 ele- ments 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. 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. En voici quelques-unes : 120 Apprendre a programmer avec Python »> nombres = [17, 38, 10, 25, 72] »> nombres . sort ( ) »> nombres [10, 17, 25, 38, 72] # trier la liste »> n ombre s . append (12) »> nombres [10, 17, 25, 38, 72, 12] # ajouter un element a la fin »> nombres . reverse ( ) »> nombres [12, 72, 38, 25, 17, 10] # inverser 1 ' ordre des elements »> nombres . index (17) 4 # retrouver 1 ' index d'un element »> nombres . remove (38) »> nombres [12, 72, 25, 17, 10] # 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] Notez bien la difference entre la methode removeO et l'instruction del : del travaille avec un index ou une tranche d'index, tandis que removeO 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 n radians). 10.29 Ecrivez un script qui permette d'obtenir a l'ecran les 15 premiers termes des tables de multipli- cation 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. Faites attention aux caracteres accentues ! 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 (rutilisateur du pro- gramme doit pouvoir entrer une phrase de son choix). Tachez de tenir compte des caracteres accentues. 10. Approfondir les structures de donnees 121 10.33 Ecrivez un script capable d'afflcher 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 me- thode integree sort() de Python : vous devez done deflnir vous-meme l'algorithme de tri. Techniques de slicing avance pour modifier une liste Comme nous venons de le signaler, vous pouvez aj outer ou supprimer des elements dans une liste en utilisant une instruction (del) et une methode (appendO) 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 destructions ou de methodes dediees, mais elle permet davantage de souplesse : 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 sup- pression d'element(s) dans une liste, vous devez obligatoirement y indiquer une « tranche » dans la liste cible (e'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'inse- rez 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 'fro- mage' ), alors que l'element mots[l:3] en est une. Vous comprendrez mieux ces contraintes en analysant ce qui suit : Suppression / remplacement d'elements »> mots [2: 5] = [] # [] designe une liste vide »> mots [ ' jambon ' , ' f romage ' , ' saucisson ' , ' ketchup' ] »> mots [1:3] = [ ' salade ' ] »> mots ['jambon', 'salade', 'ketchup'] »>mots[l:] = ['mayonnaise', 'poulet' , 'tomate'] »> mots ['jambon', 'mayonnaise', 'poulet', 'tomate'] 122 Apprendre a programmer avec Python • A la premiere ligne de cet exemple, nous remplacons la tranche [2:5] par une liste vide, ce qui cor- respond 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 7 e ligne, nous remplacons une tranche de deux elements par une autre qui en comporte 3. Creation d'une liste de nombres a l'aide de la fonction range() Si vous devez manipuler des sequences de nombres, vous pouvez les creer tres aisement a l'aide de cette fonction : »> range (10) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] La fonction ranged genere une liste de nombres entiers de valeurs croissantes. Si vous appelez ranged avec un seul argument, la liste contiendra un nombre de valeurs egal a l'argument fourni, mais en com- mencant a partirde zero (c'est-a-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 ranged avec deux, ou meme trois arguments separes par des virgules, afin de ge- nerer 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 ranged attend toujours de un a 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 sui- vante. S'ils ne sont pas fournis, les parametres FROM et STEP prennent leurs valeurs par defaut, qui sont respectivement 0 et 1 . Parcours d'une liste a l'aide de for, range() 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 ranged et lend pour obtenir automatiquement tous les in- dices d'une sequence (liste ou chaine). Exemple : fable = [ ' Maitre ' , ' Corbeau ' , ' sur ' , ' un ' , , ' arbre ' , , ' perche ' ] for index in range (len (fable) ) : print index, fable [index] L'execution de ce script donne le resultat : 0 Maitre 1 Corbeau 2 sur 3 un 4 arbre 5 perche 10. Approfondir les structures de donnees 123 Une consequence du typage dynamique Comme nous l'avons deja signale plus haut (page 112), 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 va- riables Python). Operations sur les listes On peut appliquer aux listes les operateurs + (concatenation) et * (multiplication) : »> fruits = [ ' orange ' , ' citron 1 ] »> 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] *len(A) . Test d'appartenance Vous pouvez aisement 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 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 seule- ment une nouvelle reference vers cette liste. Essayez par exemple : Apprendre a programmer avec Python >» fable = [ ' Je ' , ' plie ' , ' mais ' , ' ne ' ' romps ' , ' point ' ] »> phrase = fable »> fable [4] = ' casse ' »> phrase ['Je', 'plie ', '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. fable phrase fable phrase 'Je' 'plie' 'mais' 'ne' 'casse' 'point' 1 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. Nous verrons plus tard 1'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. Petite remarque concernant la syntaxe Python vous autorise a « etendre » une longue instruction sur plusieurs lignes, si vous continuez a enco- der quelque chose qui est delimite par une pake de parentheses, de crochets ou d'accolades. Vous pou- vez 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 ' , ' j aune ' , ' ver t ' , '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. Approfondir les structures de donnees 125 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 conte- nant 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 etablit la liste de tous les nombres premiers compris entre 1 et 1 000, 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. 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'ordi- nateur 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 tech- niques 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. 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. 126 Apprendre a programmer avec Python 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. Utili- sez 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 pe- tit nombre de valeurs, nous ne pouvons rien verifier. Par contre, si nous utilisons un grand nombre de fois la fonction randomO, 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 inter- valle 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. 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!). U 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 frac- tions par defaut (5 par exemple). Vous pouvez egalement limiter le choix de l'utilisateur a un nombre compris entre 2 et le l/10 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 ceuvre 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 c'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 10. Approfondir les structures de donnees 127 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 en- semble plus vaste. Par exemple, vous pourriez chercher a definir d'abord une fonction numeroFractionO qui servirait a de- terminer dans quelle fraction de 1'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 (c'est-a-dire le n" de la fraction correspondante). II existe peut-etre un rai- sonnement mathematique simple qui permette de determiner l'index de la fraction a partir de ces deux arguments. Pensez notamment a la fonction integree into, 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 numeroFractionO que nous souhaitons mettre au point. Si vous disposez d'un temps suffisant, vous pouvez aussi realiser une version graphique de ce pro- gramme, qui presentera les resultats sous la forme d'un bistogramme (diagramme « en batons »). Tirage au hasard de nombres entiers Lorsque vous developperez des projets personnels, il vous arrivera frequemment de souhaiter 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 ha- sard un nombre entier compris entre 1 et 52. Vous pouvez pour ce faire utiliser la fonction randrangeO 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, randrangeO, 13, 3) renverra un des nombres de la serie 3, 6, 9, 12 : »> for i in range (15) : print random. randrange (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 correc- tement 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. 128 Apprendre a programmer avec Python Les tuples Nous avons etudie jusqu'ici deux types de donnees composites : les chaines, qui sont composees de ca- racteres, 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-modifiable s. Exemple : »> liste = [ ' jambon ' , ' f romage ' , ' miel ' , ' confiture ' , ' chocolat ' ] »> liste [1:3] =['salade'] »> print liste [ ' jambon ' , ' salade ' , ' confiture ' , ' chocolat ' ] »> chaine =' Romeo prefere 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 possibilite 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 49 , 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'enfer- mant dans une paire de parentheses, comme l'instruction print de Python le fait elle-meme. II s'agit sim- plement 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 ef- fectue sur les listes, si ce n'est que les tuples ne sont pas modifiables : »> print tuple [2: 4] Cc\ '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 memoire). Ce terme n'est pas un mot anglais ordinaire : il s'agit d'un neologisme informatique. 10. Approfondir les structures de donnees 129 Les dictionnaires Les types de donnees 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 re- vanche, nous pourrons acceder a n'importe lequel d'entre eux a l'aide d'un index specifique que Ton ap- pellera une cle, laquelle pourra etre alphabetique, numerique, ou meme d'un type composite sous cer- taines 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, et meme aussi des fonctions, des classes ou des instances (voir plus loin) 50 . Creation d'un dictionnaire A titre d'exemple, nous allons creer un dictionnaire de langue, pour la traduction de termes informa- tiques anglais en francais. 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 de la syntaxe, on reconnait un 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 derniere ligne ci-dessus, un dictionnaire apparait dans la syntaxe Python sous la forme d'une serie d'elements separes par des virgules, le tout etant enferme entre deux accolades. Chacun de ces elements est lui-meme constitue d'une paire d'objets : un index et une valeur, separes par un double point. Dans un dictionnaire, les index s'appellent des cles, et les elements peuvent done s'appeler des paires cle-valeur. Dans notre dictionnaire d'exemple, les cles et les valeurs sont des chaines de caracteres. Veuillez a present constater que l'ordre dans lequel les elements apparaissent a la derniere ligne ne cor- respond 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 Remarquez aussi que contrairement a ce qui se passe avec les listes, il n'est pas necessaire de faire appel a une methode particuliere (telle que appendO) pour ajouter de nouveaux elements a un dictionnaire : il suffit de creer une nouvelle paire cle-valeur. 50 Les listes et les tuples peuvent eux aussi contenir des dictionnaires, des fonctions, des classes ou des instances, Nous n'avions pas mentionne tout cela jusqu'ici, afin de ne pas alourdir l'expose. 130 Apprendre a programmer avec Python Operations sur les dictionnaires Vous savez deja comment ajouter des elements a un dictionnaire. Pour en enlever, vous utiliserez l'ins- truction integree del. Creons pour l'exemple un autre dictionnaire, destine cette fois a contenir l'inven- taire 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 (les valeurs sont done cette fois des donnees 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 integree len() est utilisable avec un dictionnaire : elle en renvoie le nombre d'elements : »> print len (invent) 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 bien determinee. On fournit la cle en argument, et la methode renvoie une valeur 'vraie' ou 'fausse' (en fait, 1 ou 0), sui- vant 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. Cette methode se revelera tres utile plus loin, lorsque nous voudrons parcourir un dictionnaire a l'aide d'une boucle : »> print invent, items () [('oranges', 274), ('bananes', 312), ('poires', 137)] 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 refe- rence vers le meme objet, et non un nouvel objet. Nous avons deja discute ce phenomene (aliasing) a propos des listes (voir page 124). Par exemple, l'instruction ci-dessous ne definit pas un nouveau dic- tionnaire (contrairement aux apparences) : 10. Approfondir les structures de donnees 131 »> stock = invent >» print stock { ' oranges ' : 274 , 'bananes ' : : 312, 'poires': 137} Si nous modiflons invent, alors stock est egalement modifie, et vice-versa (ces deux noms designent en effet le meme objet dictionnaire dans la memoire 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} Parcours d'un dictionnaire Vous pouvez utiliser une boucle for pour traiter successivement tous les elements contenus dans un dic- tionnaire, mais attention : • au cours de l'iteration, ce sont les cles utilisees dans le dictionnaire qui seront successivement affec- tees 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 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 132 Apprendre a programmer avec Python Dans cet exemple, la methode items() appliquee au dictionnaire invent renvoie une liste de tuples (clef, valeur). Le parcours effectue sur cette liste a l'aide de la boucle for permet d'examiner chacun de ces tuples un par un. Les cles ne sont pas necessairement des chaines 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 modi- fiable : des entiers, des reels, des chaines de caracteres (string ou Unicode), 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 in- diquant 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, 1): '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 seule- ment pour certains couples de coordonnees. Ailleurs, il n'y a rien. Par consequent, si nous voulons in- terroger le dictionnaire pour savoir ce qui se trouve la ou il n'y a rien, comme par exemple aux coor- donnees (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((l,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. 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 par- ticulier. Des operations comme la concatenation et 1'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 er- reur lors de l'execution du code : >» print arb [1:3] ***** Erreur : KeyError: slice (1, 3, None) ***** 10. Approfondir les structures de donnees 133 Vous avez vu egalement qu'il suffit d'affecter un nouvel indice (une nouvelle cle) pour ajouter une en- tree au dictionnaire. Cela ne marcherait pas avec les listes 51 : »> invent [' cerises ' ] = 987 »> print invent { 'oranges': 274, 'cerises': 987, 'poires': 137} »> liste = [ ' j ambon ' , '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 sup- pressions, dans n'importe quel ordre. lis remplacent avantageusement 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 dic- tionnaire, 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 constituees de tuples (age, taille), dans lesquels 1'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 re- sultat 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 117. 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. Construction d'un histogramme a l'aide d'un dictionnaire Les dictionnaires constituent un outil tees elegant pour consteuire des histogrammes. Supposons par exemple que nous voulions etablir rhistogramme qui represente la frequence d'utilisa- tion de chacune des lettees 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 Rappel : les methodes permettant d'ajouter des elements a une liste sont decrites page 121 . 134 Apprendre a programmer avec Python { 't' : 2, 'u' : 2, 'r' : : 1, '■• : : 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 cha- cune 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 l'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 me- thode 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 (c'est-a-dire au carac- tere en cours de traitement). Pour fignoler notre travail, nous pouvons encore souhaiter afficher l'histogramme dans l'ordre alphabe- tique. 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 dic- tionnaire en une liste de tuples : »> lettres_triees = lettres . items () »> lettres_triees . sort ( ) »> print lettres triees [(' ' , 8) , ('a' , 4) , ('c' , 3) , Cd' , 1) , ('e', 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 simplifiera le pro- bleme en ne tenant pas compte 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 es- paces, mais egalement par divers signes de ponctuation. Pour simplifier le probleme, vous pou- vez commencer par remplacer tous les caracteres non-alphabetiques par des espaces, et conver- tir la chaine resultante en une liste de mots a l'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. Controle du flux d'execution a l'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 l'aide d'une serie destructions if - elif - else , mais cela peut devenir assez lourd et inelegant si vous avez af- faire a un grand nombre de possibilites. Exemple : materiau = raw input ( "Choisissez le materiau : ") if materiau == ' f er ' : f onctionA ( ) elif materiau = = 'bois' : fonctionC () elif materiau = = ' cuivre ' : fonctionB () elif materiau = = ' pierre ' : fonctionD () elif . . . etc 10. Approfondir les structures de donnees 135 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 l'aide d'une liste (nous en donnons un exemple de- taille a la page 210), ou mieux encore a l'aide d'un dictionnaire. Exemple : materiau = raw input ( "Choisissez le material! : ") dico = { ' f er ' : f onctionA, ' bois : f onctionC , ' 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 possibili- tes 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 crea- tion du dictionnaire). • La seconde instruction invoque la fonction correspondant au choix opere a l'aide de la variable ma- teriau. Le nom de la fonction est extrait du dictionnaire a l'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 se- rie de elif) : dico . get (materiau , fonctAutre) () Lorsque la la valeur de la variable materiau ne correspond a aucune cle du dictionnaire, c'est la fonction fonctAutreO qui est invoquee. Exercices 10.50 Completez l'exercice 10.46 (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 forma- tee de maniere a bien separer : - la cle et la valeur (c'est- a-dire le nom de la personne, d'une 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. 136 Apprendre a programmer avec Python Votre programme affichera par exemple : Choisissez : (R) ecuperer un dictionnaire preexistant sauvegarde dans un f ichier (A)jouter des donnees au dictionnaire courant (C)onsulter le dictionnaire courant (S) auvegarder le dictionnaire courant dans un f ichier (T)erminer : Suivant le choix opere par l'utilisateur, vous effectuerez alors l'appel de la fonction correspon- dante en la selectionnant dans un dictionnaire de fonctions. 11 Classes, objets, attributs Les chapitres precedents vous ont dejd mis en contact dplusieurs reprises avec la notion d'objet. Vous save% done dejd qu'un objet est une entite que I'on construit par instantiation dpartir d'une classe (e'est-d-dire en quelque sorte une « categorie » ou un « type » d'objet). Par exemple, on peut trouver dans la bibliotheque Tkinter, une classe Buttonf) dpartir de laquelle on peut crier dans une fienetre un nombre quelconque de boutons. Nous allons a present examiner comment vous pouve% vous-memes definir de nouvelles classes d 'objets. II s'agit la d'un sujet relativement ardu, mais vous I'abordere^ de maniere tres progressive, en commenfant par definir des classes d'objets tres simples, que vous perfectionnere^ ensuite. Attende^-vous cependant a rencontrer des objets de plus en plus complexes par apres. Comme les objets de la vie courante, les objets infiormatiques 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. 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 reside 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 d'encapsu- lation : la fonctionnalite interne de l'objet et les variables qu'il utilise pour effectuer son travail, sont en quelque sorte « enfermees » dans l'objet. Les autres objets et le monde exterieur ne peuvent y avoir ac- ces qu'a travers des procedures bien definies : Yinterface de l'objet. En particulier, l'utilisation de classes dans vos programmes va vous permettre - entre autres avantages - d'eviter au maximum I'emploi de variables globales. Vous devez savoir en effet que l'utilisation de va- riables globales comporte des risques, d'autant plus importants que les programmes sont volumineux, parce qu'il est toujours possible que de telles variables soient modifiees, ou meme redefinies, n'importe ou dans le corps du programme (ce risque s'aggrave particulierement si plusieurs programmeurs diffe- rents travaillent sur un meme logiciel). Un second benefice resultant de rutilisation des classes est la possibilite qu'elles offrent de construire de nouveaux objets a partir d'objets preexistants, et done de reutiliser des pans entiers d'une program- mation deja ecrite (sans toucher a celle-ci !), pour en tirer une fonctionnalite nouvelle. Cela est rendu possible grace aux concepts de derivation et de polymorphisme : 138 Apprendre a programmer avec Python • La derivation est le mecanisme qui permet de construire une classe « enfant » au depart d'une classe « parente ». L'enfant ainsi obtenu herite toutes les proprietes et toute la fonctionnalite de son an- cetre, auxquelles on peut aj outer ce que Ton veut. • Le polymorphisme permet d'attribuer des comportements differents a des objets derivant les uns des autres, ou au meme objet ou en fonction d'un certain contexte. Avant d'aller plus loin, signalons ici que la programmation orientee objet est optionnelle sous Python. Vous pouvez done mener a bien de nombreux projets sans l'utiliser, avec des outils plus simples tels que les fonctions. Sachez cependant que si vous faites l'effort d'apprendre a programmer a l'aide de classes, vous maitriserez un niveau d'abstraction plus eleve, ce qui vous permettra de traiter des pro- blemes de plus en plus complexes. En d'autres termes, vous deviendrez un programmeur beaucoup plus competent. Pour vous en convaincre, rappelez-vous les progres que vous avez deja realises au long de ce cours : • Au debut de votre etude, vous avez d'abord utilise de simples instructions. Vous avez en quelque sorte « programme a la main » (e'est-a-dire pratiquement sans outils). • Lorsque vous avez decouvert les fonctions predefinies (cf. chapitre 6), vous avez appris qu'il exis- tait ainsi de vastes collections d'outils specialises, realises par d'autres programmeurs. • En apprenant a ecrire vos propres fonctions (cf. chapitre 7 et suivants), vous etes devenu capable de creer vous-meme de nouveaux outils, ce qui vous a donne un surcroit de puissance conside- rable. • Si vous vous initiez maintenant a la programmation par classes, vous allez apprendre a construire des machines productrices d'outils. C'est evidemment plus complexe que de fabriquer directement ces outils, mais cela vous ouvre des perspectives encore bien plus larges ! Une bonne comprehension des classes vous aidera notamment a bien maitriser le domaine des inter- faces graphiques (Tkinter, wxPythori) et vous preparera efficacement a aborder d'autres langages mo- dernes, tels que C+ + ou Java. Definition d'une classe elementaire Pour creer une nouvelle classe d'objets Python, on utilise l'instruction class. Nous allons done ap- prendre a utiliser cette instruction, en commencant par definir un type d'objet tres rudimentaire, lequel sera simplement un nouveau type de donnee. Nous avons deja utilise differents types de donnees jus- qu'a present, mais il s'agissait a chaque fois de types integres dans le langage lui-meme. Nous allons maintenant creer un nouveau type composite : le type Point. Ce type correspondra au concept de point en geometrie plane. Dans un plan, un point est caracterise par deux nombres (ses coordonnees suivant x et y). En notation mathematique, on represente done un point par ses deux coordonnees x et y enfermees dans une paire de parentheses. On parlera par exemple du point (25, 17). Une maniere naturelle de representer un point sous Python serait d'utiliser pour les coordonnees deux valeurs de type float. Nous voudrions cependant combiner ces deux valeurs dans une seule entite, ou un seul objet. Pour y arriver, nous allons definir une classe PointO : »> class Point (object) : "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 PointO. 1 1 . Classes, objets, attributs 139 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 moins une ligne. Dans notre exemple ultra-simplifie, cette ligne n'est rien d'autre qu'un simple commentaire. Comme nous l'avons vu precedemment pour les fonctions (cf. page 60), vous pou- vez inserer une chaine de caracteres directement apres l'instruction class, afin de mettre en place un commentaire qui sera automatiquement incorpore dans le dispositif de documentation interne de Python. Prenez done l'habitude de toujours placer une chaine decrivant la classe a cet endroit. • Les parentheses sont destinees a contenir la reference d'une classe preexistante. Cela est requis pour permettre le mecanisme d'heritage. Toute classe nouvelle que nous creons peut en effet heri- ter d'une classe parente un ensemble de caracteristiques, auxquelles elle ajoutera les siennes propres. Lorsque Ton desire creer une classe fondamentale - e'est-a-dire ne derivant d'aucune autre, comme e'est le cas ici avec notre classe PointO - la reference a indiquer doit etre par conven- tion le nom special object, lequel designe l'ancetre de toutes les classes 32 . • Une convention tres repandue veut que Ton donne aux classes des noms qui commencent par une majuscule. Dans la suite de ce texte, nous respecterons cette convention, ainsi qu'une autre qui de- mande que dans les textes explicatifs, on associe a chaque nom de classe une paire de parentheses, comme nous le faisons deja pour les noms de fonctions. Nous venons done de definir une classe PointO. Nous pouvons a present nous en servir pour creer des objets de cette classe, que Ton appellera aussi des instances de cette classe. L'operation s'appelle pour cette raison une instanciation. Creons par exemple un nouvel objet p9 53 : Apres cette instruction, la variable p9 contient la reference d'un nouvel objet PointO. Nous pouvons dire egalement que p9 est une nouvelle instance de la classe PointO. 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 effectivement etre appelees avec des arguments. Voyons maintenant si nous pouvons faire quelque chose avec notre nouvel objet p9 : »> 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 PointO, laquelle est definie elle-meme au niveau principal (main) du programme. Elle est situee dans un emplacement bien determine de la memoire vive, dont l'adresse ap- parait ici en notation hexadecimale. »> print p9. doc Definition d'un point mathematique Comme nous l'avons explique pour les fonctions (cf. page 60), les chaines de documentation de divers objets Python sont associees a l'attribut predefini _doc_. II est done toujours possible de retrouver la documentation associee a un objet Python quelconque, en invoquant cet attribut. 52 Dans les premieres versions de Python, il etait permis de n'indiquer aucune reference (et meme d'omettre les parentheses) dans la definition d'une classe fondamentale. Cette pratique est encore autorisee aujourd'hui, mais il est vivement conseille d'adopter la syntaxe actuelle qui fait reference a la classe ancetre object. 53 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 l'on cree un nouvel objet a partir d'un moule. Exemple : p9 = new Point(). 140 Apprendre a programmer avec Python Attributs (ou variables) d'instance L'objet que nous venons de creer est juste une coquille vide. Nous allons a present lui ajouter des com- posants, par simple assignation, en utilisant le systeme de qualification des noms par points 54 : »> p9.x = 3.0 »> p9.y = 4.0 Les variables x et y que nous avons ainsi definies en les liant d'emblee a p9 , sont desormais des attributs de l'objet p9. On peut egalement les ap- peler des variables d'instance. Elles sont en effet incorporees, ou plutot encapsulees dans cette instance (ou 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 pourra utiliser les attributs d'un objet dans n'importe quelle expression, exactement comme toutes les variables ordinaires : »> print p9 . x 3.0 »> print p9.x**2 + p9.y**2 25.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 refe- rence 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 independante x , et l'attribut x de l'objet p9. L'objet p9 contient en effet son propre es- pace de noms, independant de l'espace de nom principal ou se trouve la variable x. Remarque importante Nous venons de voir qu'il est tres aise d 'ajouter 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 /'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 anecdotique, 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. Passage d'objets comme arguments lors de l'appel d'une fonction Les fonctions peuvent utiliser des objets comme parametres, et elles peuvent egalement fournir un ob- jet comme valeur de retour. 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 PointO, dont l'instruction qui suit uti- lisera les variables d'instance p.x et p.y. Lorsqu'on appelle cette fonction, il faut done lui fournir un objet de type PointO comme argument. Essayons avec l'objet p9 : 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 P 9 x y 3.0 4.0 1 1 . Classes, objets, attributs 141 >» affiche_point(p9) coord, horizontals = 3.0 coord, verticale = 4.0 Exercice 11.1 Ecrivez une fonction distanced qui permette de calculer la distance entre deux points. Cette fonction attendra evidemment deux objets PointO comme arguments. 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 dis- tinctes. 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 ambiguite. Par exemple, si nous parlons de l'egalite de deux objets PointO, 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 = PointO >» pi . x = 3 >» pi .y = 4 >» p2 = PointO >» p2 . x = 3 >» p2 . y = 4 »> print (pi == p2) 0 Ces instructions creent deux objets pi et p2 qui restent distincts, meme s'ils font partie d'une meme classe et 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, memorises a des emplacements differents dans la memoire de l'ordinateur. 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^ 5 l'une de 1' autre. Concernant ce phenomene d'aliasing, voir egalement page 124. 142 Apprendre a programmer avec Python 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 : >» pi . x = 7 »> print p2 . x 7 Lorsqu'on modifie l'attribut x de pi, on constate que l'attribut x de p2 a change lui aussi. »> print pi < main .Point instance at 00C2CBEO »> print p2 < main .Point instance at 00C2CBEO Les deux references pi et p2 pointent vers le meme emplacement dans la memoire. Objets composes d'objets Supposons maintenant que nous voulions definir une classe qui servira a 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 convention qui soit retenue. Definissons done notre nouvelle classe : »> class Rectangle (oject) : "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 Nous creons ainsi un nouvel objet Rectangle!) et lui donnons ensuite deux attributs. Pour specifier le coin superieur gauche, nous allons a present utiliser une nouvelle instance de la classe PointO que nous avons definie precedemment. Ainsi nous allons creer un objet, a l'interieur d'un autre objet ! »> boite . . coin PointO >» boite . . coin , , X = 12.0 »> boite . . coin , • y = 27.0 A la premiere de ces trois instructions, nous creons un nouvel attribut coin pour l'objet boite. Ensuite, pour acceder a cet objet qui se trouve lui-meme a l'interieur d'un autre objet, nous utilisons la qualifica- tion 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, se- lectionner son attribut y. » Vous pourrez peut-etre mieux vous representer tout cela a l'aide d'un diagramme tel que celui-ci : Le nom boite se trouve dans I'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 1 1 . Classes, objets, attributs 143 Espaces de noms boite largeur hauteur coin x y Valeurs 50.0 35.0 12.0 > 27.0 a leur tour, soit d'autres espaces de noms (cas du nom « coin »), soit des valeurs bien determinees, les- quelles sont memorisees ailleurs. Python reserve des espaces de noms differents pour chaque module, chaque classe, chaque instance, chaque fonction. Vous pouvez tirer parti de tous ces espaces de noms bien compartimentes afin de rea- liser des programmes robustes, c'est-a-dire des programmes dont les differents composants ne peuvent pas facilement interferer. 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 RectangleO et elle renvoie un objet de type PointO, 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 Vous pouvez par exemple appeler cette fonction, en utilisant comme argument l'objet boite defini plus haut : »> centre = trouveCentre (boite) »> print centre . x , centre . y 37.0 44.5 Modification des objets 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 jusqu'a la version actuelle 2.5). D'autres langages etablissent une distinction nette entre attributs publics (accessibles de l'exterieur de l'objet) et attributs prives (qui sont accessibles seule- ment aux algorithmes inclus dans l'objet lui-meme). Cependant, comme nous l'avons deja signale plus haut (a propos de la definition des attributs par assi- gnation simple, depuis l'exterieur de l'objet), modifier de cette facon les attributs d'une instance n'est pas une pratique recommandable, parce qu'elle contredit l'un des objectifs fondamentaux de la pro- grammation orientee objet, qui vise a etablir une separation stricte entre la fonctionnalite d'un objet 144 Apprendre d programmer avec Python (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). Concretement, cela signifie que nous devons maintenant etudier comment faire fonctionner les objets a l'aide d'outils vraiment appropries, que nous appellerons des methodes. Ensuite, lorsque nous aurons bien compris le maniement de celles-ci, nous nous fixerons pour regie de ne plus modifier les attributs d'un objet par assignation directe depuis le monde exterieur, comme nous l'avons fait jusqu'a present. Nous veillerons au contraire a toujours utiliser pour cela des methodes mises en place specifiquement dans ce but, comme nous allons l'expliquer dans le chapitre suivant. L'ensemble de ces methodes constituera ce que nous appellerons desormais Yinterface de l'objet. 12 Classes, methodes, heritage Les classes que nous avons definies dans le chapitre precedent peuvent etre considere'es comme 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 en- semble (l'objet), a la fois un certain nombre de donnees (ce sont les attributs d'instance), et les algo- rithmes destines a effectuer divers traitements sur ces donnees (ce sont les methodes, a savoir des fonc- tions particulieres encapsulees dans l'objet). Objet = [ attributs + methodes ] Cette facon d'associer dans une meme « capsule » les proprietes d'un objet et les fonctions qui per- mettent 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 » dans une application graphique. II nous parait raison- nable 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 nous savons que 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 ma- niere, nous souhaiterons done 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 entre ce bouton et un autre, ou a fortiori entre ce bouton et d'autres entites. Definition d'une methode Pour illustrer notre propos, nous allons definir une nouvelle classe Time(), laquelle devrait nous per- mettre d'effectuer toute une serie d'operations sur des instants, des durees, etc. : »> class Time (object) : "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 146 Apprendre a programmer avec Python »> instant. minute = 34 »> instant . seconde = 25 A titre d'exercice, ecrivez main tenant vous-meme une fonction affiche heure() , qui serve a visualiser le contenu d'un objet de classe Time() sous la forme conventionnelle « heures:minutes:secondes ». Appliquee a l'objet instant cree ci-dessus, cette fonction devrait done afficher 11:34:25 : »> print affiche_heure (instant) 11:34:25 Votre fonction ressemblera probablement a ceci : »> def affiche heure(t) : print str(t.heure) + ":' ' + str (t. minute) + " : :" + str (t. seconde) ... ou mieux encore, a : »> def af f iche_heure (t) : print "%s:%s:%s" % (t.heure, t. minute, t. seconde) en application de la technique de formatage des chaines decrite a la page 117. Si par la suite vous deviez utiliser frequemment des objets de la classe Time(), cette fonction d'affichage vous serait probablement fort utile. II serait done judicieux d'arriver a 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 aura a manipuler des objets de la classe Time(). Une fonction ainsi encapsulee dans une classe s'appelle preferentiellement une methode. Vous avez evidemment deja rencontre des methodes a de nombreuses reprises dans les chapitres prece- dents de cet ouvrage, et vous savez done deja qu'une methode est bien une fonction associee a une classe particuliere d'objets. II vous reste seulement a apprendre comment construire une telle fonction. Definition concrete (Tune methode dans un script On definit une methode comme on definit une fonction, e'est-a-dire en ecrivant un bloc destructions a la suite du mot reserve def, mais cependant avec deux differences : • la definition d'une methode est toujours placee a I'interieur de la definition d'une classe, de ma- niere a ce que la relation qui lie la methode a la classe soit clairement etablie ; • la definition d'une methode doit toujours comporter au moins un parametre, lequel doit etre une reference d'instance, et ce parametre particulier doit toujours etre liste en premier. Vous pourriez en principe utiliser un nom de variable quelconque pour ce parametre, mais il est vive- ment conseille de respecter la convention qui consiste a toujours lui donner le nom : self. Ce parametre self est necessaire, parce qu'il faut pouvoir designer I'instance a laquelle la methode sera associee, dans les instructions faisant partie de sa definition. Vous comprendrez cela plus facilement avec les exemples ci-apres. Remarquons que la definition d'une methode comporte toujours au moins un parametre : self, alors que la definition d'une fonction peut n'en comporter aucun. Voyons comment cela se passe en pratique : Pour faire en sorte que la fonction affiche heure() devienne une methode de la classe Time(), il nous suffit de deplacer sa definition a I'interieur de celle de la classe : »> class Time (object) : "Nouvelle classe temporelle" 12. Classes, methodes, heritage 147 def af f iche_heure (t) : print "%s:%s:%s" % (t.heure, t. minute, t.seconde) Techniquement, c'est tout a fait suffisant, car le parametre t peut parfaitement designer l'instance a la- quelle seront attaches les attributs heure, minute et seconde. Etant donne son role particulier, il est cepen- dant fortement recommande de changer son nom en self : »> class Time (object) : "Nouvelle classe temporelle" def af f iche_heure (self ) : print "%s:%s:%s" % (self. heure, self. minute, self . seconde) La definition de la methode affiche_heure() fait maintenant partie du bloc d'instructions indentees sui- vant l'instruction class (et dont fait partie aussi la chaine documentaire « Nouvelle classe temporelle »). Essai de la methode, dans une instance quelconque Nous disposons done des a present d'une classe Time(), dotee d'une methode affiche_heure(). En prin- cipe, nous devons maintenant pouvoir creer des objets de cette classe, et leur appliquer cette methode. Voyons si cela fonctionne. Pour ce faire, commencons par instancier un objet : »> maintenant = Time ( ) Si nous essayons un peu trop vite de tester notre nouvelle methode sur cet objet, cela 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 ... et reessayer. A present, ca marche : »> maintenant . af f iche_heure ( ) 13:34:21 A plusieurs reprises, nous avons cependant deja signale qu'il n'est pas recommandable de creer ainsi des attributs d'instance par assignation directe en dehors de l'objet lui-meme. Entre autres desagrements, cela conduirait frequemment a des erreurs comme celle que nous venons de rencontrer. Voyons done a present comment nous pouvons mieux faire. 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 une mani- pulation sur l'objet nouvellement cree. En d'autres termes, il serait judicieux que les variables d'ins- tance soient predefmies elles aussi a l'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 designera par la suite sous le nom de constructeur. Une methode constructeur a ceci de particulier qu'e//e est executee auto- matiquement lorsque Ton instancie un nouvel objet a partir de la classe. On peut done y placer tout ce qui semble necessaire pour initialiser automatiquement l'objet que Ton cree. Afin qu'elle soit reconnue comme telle par Python, la methode constructeur devra obligatoirement s'appeler init (deux caracteres « souligne », le mot init, puis encore deux caracteres « souligne »). 148 Apprendre a programmer avec Python Exemple »> class Time (object) : "Encore une nouvelle classe temporelle" def init (self) : self.heure =0 self .minute =0 self . seconde =0 def af f iche_heure (self ) : print "%s:%s:%s" % (self.heure, self. minute, self . seconde) Comme precedemment, creons un objet de cette classe et testons-en la methode affiche_heure() : »> tstart = Time() »> tstart. af fiche_heure () 0:0:0 Nous n'obtenons plus aucune erreur, cette fois. En effet : lors de son instantiation, l'objet tstart s'est vu attribuer automatiquement les trois attributs heure, minute et seconde par la methode constructeur, avec zero comme valeur par defaut pour chacun d'eux. Des lors qu'un objet de cette classe existe, on peut done tout de suite demander l'affichage de ces attributs. 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. Et dans le cas de cette methode particuliere qu'est le constructeur, les parametres peuvent jouer un role tres inte- ressant, parce qu'ils vont permettre d'initialiser certaines de ses variables d'instance au moment meme de 1'instantiation de l'objet. Veuillez done reprendre l'exemple precedent, en modifiant 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 Notre nouvelle methode init () comporte a present 3 parametres, avec pour chacun une valeur par defaut. Nous obtenons ainsi une classe encore plus perfectionnee. Lorsque nous instancions un objet de cette classe, nous pouvons maintenant initialiser ses principaux attributs a l'aide d'arguments, au sein meme de l'instruction d'instanciation. Et si nous omettons tout ou partie d'entre eux, les attributs re- coivent de toute maniere des valeurs par defaut. Pour lui transmettre des arguments, lorsque Ton ecrit l'instruction d'instanciation d'un nouvel objet, il suffit de placer ceux-ci dans les parentheses qui accompagnent le nom de la classe. On procede done exactement de la meme maniere que lorsqu'on invoque une fonction quelconque. 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 . af f iche_heure ( ) 10:30:0 12. Classes, methodes, heritage 149 ou encore : »> rendezvous = Time(hh =18) »> rendezvous . af f iche_heure ( ) 18:0:0 Exercices 12.1 Definissez une classe DominoO 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) »> d2 = Domino (4, 3) »> dl . af f iche_points ( ) face A : 2 face B : 6 »> d2 . af f iche_j>oints ( ) 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. 12.2 Definissez une classe CompteBancaireO, 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'ajouter une certaine somme au solde ; • retrait(somme) permettra de retirer une certaine somme du solde ; • afficheO 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 . affiche ( ) Le solde du compte bancaire de Duchmol est de 950 euros . »> compte2 = CompteBancaireO »> compte2 . depot (25) »> compte2 . affiche ( ) Le solde du compte bancaire de Dupont est de 1025 euros . 12.3 Definissez une classe VoitureO qui permette d'instancier des objets reproduisant le comporte- ment 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 VoitureO, 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 150 Apprendre a programmer avec Python 1,3 m/s 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 . af f iche_tout ( ) Ford verte pilotee par Juliette, vitesse = 21.6 m/s. »> a3 . af f iche_tout ( ) Mercedes rouge pilotee par personne, vitesse = 0 m/s. 12.4 Definissez une classe Satellite!) qui permette d'instancier des objets simulant des satellites artifi- ciels lances dans l'espace, autour de la terre. Le constructeur de cette classe initialisera les attri- buts d'instance suivants, avec les valeurs par defaut indiquees : masse = 100, vitesse = 0. Lorsque Ton instanciera un nouvel objet SatelliteO, 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 Av subie par un objet de masse T^X t m soumis a Taction d'une force F pendant un temps t vaut A v = . Par exemple : un m satellite de 300 kg qui subit 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. • energieO renverra au programme appelant la valeur de l'energie cinetique du satellite. ..2 WAV 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 si. energieO 200000 »> si. impulsion (500, 15) »> si . af f iche_vitesse () vitesse du satellite Zoe = 70 m/s. »> print si. energieO 612500 Espaces de noms des classes et instances Vous avez appris precedemment (voir page 55) 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. 12. Classes, methodes, heritage 151 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 consultation seulement : elles peuvent utiliser les va- leurs 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 variables de classe ou attributs de 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 148, nous avons instancie trois objets de cette classe : recreation, rentree et rendezvous. Chacun a ete initialise avec des valeurs dif- ferentes, independantes. Nous pouvons modifier et reafficher ces valeurs a volonte dans chacun de ces trois objets, sans que l'autre n'en soit affecte : »> recreation . heure = 12 »> rentree . affiche_heure ( ) 10:30:0 »> recreation . af f iche_heure ( ) 12:15:18 Veuillez a present encoder et tester l'exemple ci-dessous : »> class Espaces (object) : # 1 ... aa = 33 # 2 . . . def af f iche (self ) : # 3 ... print aa, Espaces. aa, self.aa # 4 >» aa = 12 # 5 »> essai = Espaces () # 6 »> essai. aa = 67 # 7 »> essai. aff iche () # 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'inte- rieur 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 a l'interieur de la definition d'une classe. Heritage Les classes constituent le principal outil de la programmation orientee objet (Object Oriented Program- ming ou OOP), qui est consideree de nos jours comme la technique de programmation la plus perfor- mante. L'un des principaux atouts de ce type de programmation reside dans le fait que Ton peut tou- jours se servir d'une classe preexistante pour en creer une nouvelle, qui heritera toutes ses proprietes 152 Apprendre d programmer avec Python mais pourra modifier certaines d'entre elles et/ ou y ajouter les siennes propres. Le procede s'appelle de- rivation. II permet de creer toute une hierarchie de classes allant du general au parficulier. Nous pouvons par exemple definir une classe MammifereO, qui contienne un ensemble de caracteris- tiques propres a ce type d'animal. A partir de cette classe parente, nous pouvons deriver une ou plu- sieurs classes piles, comme : une classe PrimateO, une classe RongeurO, une classe CarnivoreO, etc., qui he- riteront toutes les caracterisfiques de la classe MammifereO, en y ajoutant leurs specificites. Au depart de la classe CarnivoreO, nous pouvons ensuite deriver une classe BeletteO, une classe LoupO, une classe ChienO, etc., qui heriteront encore une fois toutes les caracterisfiques de la classe parente avant d'y ajouter les leurs. Exemple : »> class Mammifere (object) : caractl = "il allaite ses petits ;" »> class Carnivore (Mammifere) : 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 petits ; il se nourrit de la chair de ses proies ; Dans cet exemple, nous voyons que l'objet mirza , qui est une instance de la classe ChienO, herite non seulement l'attribut defini pour cette classe, mais egalement les attributs definis pour les classes pa- rentes. 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 attri- buer a la nouvelle classe, et on place entre parentheses le nom de la classe parente. Les classes les plus fondamentales derivent quant a elles de l'objet « ancetre » object. 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 CarnivoreO, contraire- ment 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 CarnivoreO. 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 pa- rentes. S'il existe des variables aux noms identiques dans plusieurs de ces espaces, laquelle sera selec- tionnee lors de l'execufion 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 12. Classes, methodes, heritage 153 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. Heritage et polymorphisme Analysez soigneusement le script de la page suivante. II met en ceuvre plusieurs concepts decrits prece- demment, 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 entites, constitues d'un certain nombre de protons (particules chargees d'electricite positive), d'electrons (charges negativement) et de neutrons (neutres). Le type d'atome (ou element) est determine par le nombre de protons, que Ton appelle egalement nu- mero 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 plu- sieurs electrons, et d'un ion positifs'A 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 AtomeO et des objets lon(). Nous avons rappele ci-dessus qu'un ion est simplement un atome modifie. Dans notre programmation, la classe qui definit les objets lon() sera done une classe derivee de la classe AtomeO : 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 afficheO) remplace une methode de meme nom heritee de la classe AtomeO. Les classes AtomeO et lon() 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 afficheO de la classe AtomeO a ete surchargee. 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 AtomeO, doit contenir une version simplifiee du tableau perio- dique des elements (tableau de Mendeleiev), 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 pro- duits a partir de cette classe. Voyons concretement comment toutes ces idees s'articulent : class Atome (object) : """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 154 Apprendre a programmer avec Python self.nn = Atome . table [nat] [1] # nb. de neutrons trouves dans table def af f iche (self ) : print print "Nora 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 AtomeO en four- nissant leur numero atomique (lequel doit etre compris entre 1 et 10). Pour instancier des objets lon(), par contre, on doit fournir un numero atomique et une charge electrique globale (positive ou negative). La meme methode afficheO 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 supplementaire (polymorphisme) . Commentaires La definition de la classe AtomeO 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 Mendeleiev. Pour chacun de ces elements, la liste contient un tuple : (nom de l'element, nombre de neutrons), a l'in- dice 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. 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. 12. Classes, methodes, heritage 155 Viennent ensuite les definitions de deux methodes : • Le constructeur init () sert essentiellement ici a generer trois attributs d'instance, destines a me- moriser respectivement les nombres de protons, d'electrons et de neutrons pour chaque objet atome construit a partir de cette classe (rappelez-vous que les attributs d'instance sont des variables liees au parametre self). Notez au passage 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, comme dans rinstruction : self .nn = Atome. table [nat] [1]. • 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. La definition de la classe lon() inclut dans ses parentheses le nom de la classe AtomeO qui precede. Les methodes de cette classe sont des variantes de celles de la classe AtomeO. Elles devront done vrai- semblablement faire appel a celles-ci. Cette remarque est importante : comment peut-on, a l'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, il suffit de I'invoquer directement, via cette autre classe, en lui transmettant la reference de l'instance comme premier argument. C'est ainsi que dans notre script, par exemple, la methode afficheO de la classe lon() peut faire appel a la methode afficheO de la classe AtomeO : les informations affichees seront bien celles de l'objet-ion cou- rant, puisque sa reference a ete transmise dans l'instruction d'appel : Atome . af fiche (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 lon() fait appel a la methode constructeur de sa classe parente, dans : Atome. init (self, nat) Cet appel est necessaire, afin que les objets de la classe lon() soient initialises de la meme maniere que les objets de la classe AtomeO. Si nous n'effectuons pas cet appel, les objets-ions n'heriteront pas auto- matiquement les attributs ne, np et nn, car ceux ci sont des attributs d'instance crees par la methode constructeur de la classe AtomeO, et celle-ci n'est pas invoquee automatiquement lorsqu'on instancie des objets d'une classe derivee. Comprenez done bien que l'heritage ne concerne que les classes, et non les instances de ces classes. Lorsque nous disons qu'une classe derivee herite toutes les proprietes de sa classe parente, cela ne signi- fie pas que les proprietes des instances de la classe parente sont automatiquement transmises aux ins- tances de la classe fille. En consequence, retenez bien que : Dans la methode constructeur d'une classe derivee, il faut presque toujours prevoir un appel a la methode constructeur de sa classe parente. 156 Apprendre d programmer avec Python 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. csgR = Point (40, 30) csgC = Point (10, 25) gauche s La classe est un moule servant a produire des objets. Chacun d'eux sera une instance de la classe consideree. Les instances de la classe PointQ 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 PointQ. 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 faire appel au constructeur de la classe parente, en lui transmettant la reference de Vinstance (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... # "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 ... 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. 12. Classes, methodes, heritage 157 Modules contenant des bibliotheques de classes Vous connaissez deja depuis longtemps l'utilite des modules Python (cf. pages 44 et 60). 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-des- sous dans un fichier-module 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. Exemple : »> 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 carre de 13 sur 13 a une surface de 13 * 13 = 169 et un perimetre de (13 + 13) * 2 = 52 On voit dans ce script que la classe Carre() est construite par derivation a partir de la classe RectangleO dont elle herite toutes les caracteristiques. En d'autres termes, la classe CarreO est une classe fille de la classe RectangleO. Vous pouvez remarquer encore une fois que le constructeur de la classe CarreO doit faire appel au constructeur de sa classe parente ( Rectangle. init (self, . . . ) ), en lui transmettant la reference de l'instance (self) comme premier argument. Quant a l'instruction : if name == " main " : 158 Apprendre a programmer avec Python placee a la fin du module, elle sert a determiner si le module est « lance » en tant que programme auto- nome (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 CercleO. Les objets construits a partir de cette classe seront des cercles de tallies variees. En plus de la methode constructeur (qui utilisera done un parametre rayon), vous definirez une methode surfaceO, qui devra renvoyer la surface du cercle. Definissez ensuite une classe CylindreO derivee de la precedente. Le constructeur de cette nou- velle classe comportera les deux parametres rayon et hauteur. Vous y ajouterez une methode volumeO 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 CylindreO, et dont le constructeur comportera lui aussi les deux parametres rayon et hauteur. Cette nouvelle classe possedera sa propre methode volumeO, 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 12.7 Definissez une classe jeuDeCartesO permettant d'instancier des objets dont le comportement soit similaire a celui d'un 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 entiers. Cette liste de tuples contiendra les caracteristiques de chacune des 52 cartes. Pour chacune d'elles, il faut en effet memoriser separement un 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 entier indiquant la couleur de la carte (e'est-a-dire 3,2,1,0 pour Cceur, Carreau, Trefle et Pique). Dans une telle liste, l'element (11,2) designe done le valet de Trefle, et la liste terminee doit etre du type : [ (2 , 0), (3,0), (3,0), (4,0), (12,3), (13,3), (14,3)] • methode nom_carte() : cette methode doit renvoyer, 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 battreO : 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 JeuDeCartesO : jeu = JeuDeCartesO # instanciation d'un objet jeu. battre () # melange des cartes 12. Classes, methodes, heritage 159 for n in range (53) : c = jeu.tirer() if c == None : # tirage des 52 cartes print ' Termine ! ' else : # il ne reste plus aucune carte # dans la liste print jeu . nom_carte (c) # 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 c'est la premiere des deux 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. 12.9 Ecrivez un nouveau script qui recupere le code de l'exercice 12.2 (compte bancaire) en l'impor- tant comme un module. Definissez-y une nouvelle classe CompteEpargneO, derivant de la classe CompteBancaireO importee, qui permette de creer des comptes d'epargne rapportant un certain interet au cours du temps. Pour simplifier, nous admettrons que ces interets sont calcules tous les mois. Le constructeur de votre nouvelle classe devra initialiser un taux d'interet mensuel par defaut egal a 0,3 %. Une methode changeTaux(valeur) devra permettre de modifier ce taux a volonte. Une methode capitalisation(nombreMois) devra : • afficher le nombre de mois et le taux d'interet pris en compte ; • calculer le solde atteint en capitalisant les interets composes, pour le taux et le nombre de mois qui auront ete choisis. Exemple d'utilisation de la nouvelle classe : »> cl = CompteEpargne ( ' Duvivier ' , 600) »> cl. depot (350) »> cl . af f iche ( ) Le solde du compte bancaire de Duvivier est de 950 euros. »> cl . capitalisation (12) Capitalisation sur 12 mois au taux mensuel de 0.3 %. »> cl . af f iche ( ) Le solde du compte bancaire de Duvivier est de 984.769981274 euros. »> cl . changeTaux ( . 5 ) »> cl . capitalisation (12) Capitalisation sur 12 mois au taux mensuel de 0.5 %. »> cl . af f iche ( ) Le solde du compte bancaire de Duvivier est de 1045.50843891 euros. 13 Classes et 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 etof- fee, 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 efforfant de mettre en evidence les avantages qu'apporte 1' orientation objet dans nos programmes. 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'electro- nique. L'application que nous decrivons ci-apres permet de retrouver rapidement le code de trois cou- leurs qui correspond a une resistance electrique de valeur bien determinee. 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 tubu- laires cerclees de bandes de couleur (en general 3). Ces bandes de couleur indiquent la valeur nume- rique de la resistance, en fonction du code suivant : Chaque couleur correspond conventionnellement a l'un des chiffres de zero a neuf : Noir = 0 ; Brun = 1 ; Rouge = 2 ; Orange = 3 ; Jaune = 4 ; Vert = 5 ; Bleu = 6 ; Violet = 7 ; Oris = 8 ; Blanc = 9. On oriente la resistance de maniere telle que les bandes colorees soient placees a gauche. La valeur de la resistance - exprimee en ohms (Q) - s'obtient en lisant 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 l'indication fournie par la troisieme bande. Exemple concret : supposons qu'a partir de la gauche, les bandes colorees soient jaune, violette et verte. La valeur de cette resistance est 4700000 Q, ou 4700 kQ , ou encore 4,7 MQ . Ce systeme ne permet evidemment de preciser une valeur numerique qu'avec deux chiffres significatifs seulement. II est toutefois considere comme largement suffisant pour la plupart des applications elec- troniques « ordinaires » (radio, TV, etc.). 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 rutilisateur peut encoder une valeur numerique. Un bouton « Montrer » de- clenche 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. 162 Apprendre a programmer avec Python Contrainte : Le programme doit accepter toute entree nu- merique fournie sous forme entiere ou reelle, dans les li- mites 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. Mise en oeuvre concrete Nous construisons le corps de cette application simple sous la forme d'une classe. Nous voulons vous montrer ainsi comment une classe peut servir d'espace de noms commun, dans lequel vous pouvez encapsuler vos variables et nos fonctions. Le principal interet de proceder ainsi est que cela vous permet de vous passer de variables globales. En effet : • Mettre en route l'application se resumera a instancier un objet de cette classe. • Les fonctions que Ton voudra y mettre en ceuvre seront les methodes de cet objet-application. • A l'interieur de ces methodes, il suffira de rattacher un nom de variable au parametre self pour que cette variable soit accessible de partout a l'interieur de l'objet. Une telle variable d'instance est done tout a fait l'equivalent d'une variable globale (mais seulement a l'interieur de l'objet), puisque toutes les autres methodes de cet objet peuvent y acceder par l'intermediaire de self. 1# class Application (object) : 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, text ="Entrez la valeur de la resistance, en ohms :").\ 8# grid (row =2, column =1, columnspan =3) 9# Button (self . root, text ='Montrer', command =self . changeCouleurs) . \ 10# grid (row =3, column =1) 11# Button (self . root, text ='Quitter', command =self . root . quit) . \ 12# grid (row =3, column =3) 13# self .entree = Entry (self . root, width =14) 14# self .entree. grid (row =3, column =2) 15# # Code des couleurs pour les valeurs de zero a neuf : 16# self . cc = [ ' black ' , ' brown ' , ' red ' , ' orange ' , ' yellow ' , 17# ' green ' , ' blue ' , ' purple ' , ' 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 ='light blue') 23# self .can. grid (row =1, column =1, columnspan =3, 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 ='beige', 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# f ill= ' black ' , width=0 ) ) 31# 32# def changeCouleurs (self) : 33# """Affichage des couleurs correspondant a la valeur entree""" 34# self .vlch = self . entree . get ( ) # cette methode renvoie une chaine 35# try: 36# v = float (self . vlch) # conversion en valeur numerigue 37# except : 38# err =1 # erreur : entree non numerigue 39# else : 40# err =0 13. Classes et interfaces graphiques 163 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 chiffre significatif : 48# li[0] = int(v/ordgr) # partie entiere 49# decim = v/ordgr - li[0] # partie decimale 50# # extraction du second chiffre significatif : 51# = int (decim* 10 +.5) # +.5 pour arrondir correctement 52# # nombre de zeros a accoler aux 2 chiffres significatif s 53# li[2] = logv -1 54# # Coloration des 3 lignes : 55# for n in range (3) : 56# self . can. itemconf igure (self . ligne [n] , fill =self . cc [li [n] ] ) 57# 58# def signaleErreur (self ) : 59# self .entree . configure (bg ='red') # colorer le fond du champ 60# self .root. after (1000, self .videEntree) # apres 1 seconde, effacer 61# 62# def videEntree (self ) : 63# self .entree . configure (bg =' white') # retablir le fond blanc 64# self .entree . delete (0 , len (self .vlch) ) # enlever les car. presents 65# 66# # Programme principal 67# if name == ' main ' : 68# from Tkinter import * 69# from math import loglO # logarithmes en base 10 70# f = Application () # instanciation de l'objet application Commentaires • Ligne 1 : La classe est definie comme une nouvelle classe independante (elle ne derive d'aucune classe parente preexistante, mais seulement de object, « ancetre » de toutes les classes). • Lignes 2 a 14 : Le constructeur de la classe instancie les widgets necessaires : espace graphique, U- belles et boutons. Afin d'ameliorer la lisibilite du programme, cependant, nous avons place l'instan- ciation du canevas (avec le dessin de la resistance) dans une methode distincte : dessineResistanceO. Veuillez remarquer aussi que pour obtenir un code plus compact, nous ne memorisons pas les bou- tons et le libelle dans des variables (comme cela a ete explique a la page 83), parce que nous ne sou- haitons pas y faire reference ailleurs dans le programme. Le positionnement des widgets dans la fe- netre utilise la methode grid() decrite a la page 80. • Lignes 15-17 : Le code des couleurs est memorise dans une simple liste. • Ligne 18 : La derniere instruction du constructeur demarre l'application. Si vous preferez demarrer l'application independamment de sa creation, vous devez supprimer cette ligne, et reporter l'appel a mainloopO au niveau principal du programme, en ajoutant une instruction : f .root.mainloopO a la ligne 71. • Lignes 20 a 30 : Le dessin de la resistance se compose d'une ligne et d'un premier rectangle gris clair, pour le corps de la resistance et ses deux fils. Trois autres rectangles figureront les bandes co- lorees 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 11 Q). 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). 164 Apprendre a programmer avec Python • Lignes 45-46 : Les mathematiques viennent a notre secours pour extraire de la valeur numerique son ordre de grandeur (c'est-a-dire l'exposant de 10 le plus proche). Veuillez consulter un ouvrage 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). • 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 souhai- tons encore qu'il soit correctement arrondi. Pour ce faire, il suffit d'ajouter une demi unite au pro- duit 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 re- cherchee. • 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 in- dices li[l], li[2] et li[3] qui contiennent les 3 chiffres correspondants. 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 Q. Pour ces valeurs, le premier anneau colore devra rester noir, les deux autres indiqueront la valeur en Q et dixiemes d' Q. 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 resis- tance, 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. Petit train : heritage, echange conformations entre classes Dans l'exercice precedent, nous n'avons exploite qu'une seule caracteristique des classes : I'encapsula- tion. 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. 13. Classes et interfaces graphiques 165 Comprenez bien toutefois qu'il ne s'agit pas de veritables variables globales. Elles restent en effet stric- tement confinees a l'interieur de l'objet, et il est deconseille de vouloir y acceder de l'exterieur 56 . 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 encapsulates dans ces objets. On les appelle pour cette rai- son des attributs d'instance. Nous allons a present passer a la vitesse superieure, et realiser une petite application sur la base de plu- sieurs classes, afin d'examiner comment differents objets peuvent s'echanger des informations par I'in- termediaire de leurs methodes. Nous allons egalement profiter de cet exercice pour vous montrer com- ment vous pouvez definir la classe principale de votre application graphique par derivation d'une classe Tkinter preexistante, mettant ainsi a profit le mecanisme ^heritage. □C □□□ ©in o o o o o o o o Train Hello Le projet developpe ici est tres simple, mais il pourrait constituer une premiere etape dans la realisation d'un logiciel de jeu : nous en fournissons d'ailleurs des exemples plus loin (voir page 213). 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 petits person- nages apparaissent a certaines fenetres des wagons. 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 WagonO, independante, permettra d'instancier dans le canevas 4 objets-wagons simi- laires, dotes chacun d'une methode perso(). Celle-ci sera destinee a provoquer 1' 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. Implementation 1# 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) : 56 Comme nous l'avons deja signale precedemment, Python vous pennet 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 l'intermediaire de methodes specifiques (l'interface). 166 Apprendre a programmer avec Python off aer 4 n 4 +. ' _ a 1 -P \ • init (sell) . Q4 »ff Tk . init (self) # constructeur de la classe parents 1 r\n _ A 1 £ . 1 1 I — 1 , ' ______ / _ A 1 -P T.T-i ^4-1, X 7 C I- A 4 A>1~ 4— 1 "J ~ V^_> — " t.tVi 4 4- A " \ 5611 . Can -Lanvas iselr , wiatfi —llo, neignt -lju , Oy — wnite ) self . can . pack ( side =TOP, padx =5, pady = 5) n 94 Button(self, text ="Train" , command =self . dessine) .pack(side — T TP TPT' \ — llCiJC 1 ) i *a4 J. Jff Button(self , text = "Hello" , command =self . coucou) .pack(side - -T LTT \ n 44 1 -ff XOff aer dessine (self ) '. 1 K4 loff " ins tanciation de 4 wagons dans le canevas" J. 'ff self . wl = Wagon ( self . can , 10, 30) 1 «_ J-off sen .wz — wagon \ sen . can , ±j\j , ■LSff _a1 P T-T__-a_i /_a1 £ a — i __ ICfl *3 n \ sen .wj — wagon (sen . can, -.du, juj 904 _:Uff self . w4 = Wagon ( self . can , 370, 30) _:-ff "ff def coucou (self) : 9^4 "apparition de personnages dans certaines fenetres" 944 .. -ff self . wl . perso (3) # ler wagon, 3e fenetre --Off self .w3 .per so (1) # 3e wagon, le fenetre 9 64 -- Off self .w3. perso (2) # 3e wagon, 2e fenetre 974 -- 'ff self .w4 .perso (1) # 4e wagon, le fenetre 9P. 4 ..off 9 Q4 -- »ff class Wagon (object) : T 04 J U ff def init (self, canev, x, y) : •31 4 JJ-ff "dessin d'un petit wagon en dans le canevas " T94 # memorisation des parametres dans des variables d' instance -Off self .canev, self .x, self .y = canev, x, y J -ff # rectangle de base : 95x60 pixels T R4 -Off canev. create_rectangle (x, y, x+95, y+60) K4 JOff # 3 fenetres de 25x40 pixels, ecartees de 5 pixels : •374 J 'ff for xf in range (x+5, x+90 , 30) : ooff canev. create_rectangle (xf, y+5 , xf+25, y+40) •3Q4 jyff # 2 roues de rayon egal a 12 pixels : - Uff cercle (canev, x+18, y+73, 12) 41 4 -Iff cercle (canev, x+77, y+73, 12) X 94 4 "34 4-Sff def perso(self , fen) : 44.4 "apparition d'un petit personnage a la fenetre " 4 R4 -Off # calcul des coordonnees du centre de chague 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 . mainloop ( ) Commentaires • Lignes 3 a 5 : Nous projetons de dessiner une serie de petits cercles. Cette petite fonction nous fa- cilitera 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. 17 Comme nous l'avons explique au chapitre 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 correspon- dante. Ceci pourrait etre programme plus elegamment a l'aide d'une boucle et d'une liste, mais nous le laissons ainsi pour ne pas alourdir inutilement les explications qui suivent. 57 Nous verrons plus loin que Tkinter autorise egalement de construire la fenetre principale d'une application par 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 176). 13. Classes et interfaces graphiques 167 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 moins la reference du canevas, ainsi que les coordonnees souhaitees. Ces considerations nous font egalement entre- voir que lorsque nous definirons la classe WagonO, un peu plus loin, nous devrons associer a sa me- thode constructeur un nombre egal de parametres afin de 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 ses methodes. II s'agit-la du mecanisme central de la programmation par objets : Les objets sont des entites programmees qui s'echangent des messages et interagissent par I'intermediaire de leurs methodes. Idealement, la methode coucouO devrait comporter quelques instructions complementaires, les- quelles 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. • Lignes 29-30 : La classe WagonO ne derive d'aucune autre classe preexistante. Etant donne qu'il s'agit d'une classe d'objets graphiques, nous devons cependant 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 exer- cice, vous pourriez bien evidemment ajouter encore d'autres parametres : taille du dessin, orienta- tion, couleur, vitesse, etc. • Lignes 31 a 51 : Ces instructions ne necessitent guere de commentaires. La methode perso() est do- tee 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 argu- ment egal a 4 ou 5, par exemple, ce qui produira des effets incorrects. • Lignes 53-54 : Pour cette application, contrairement a la precedente, nous avons prefere separer la creation de l'objet app, et son demarrage par invocation de mainloopO, dans deux instructions dis- tinctes (en guise d'exemple). Vous pourriez egalement condenser ces deux instructions en une seule, laquelle serait alors : Application () .mainloopO, et faire ainsi l'economie d'une variable. Exercice 13.6 Perfectionnez le script decrit ci-dessus, en ajoutant un parametre couleur au constructeur de la classe WagonO, 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 objec- tif, ajoutez aussi un parametre couleur a la fonction cercleO). A cette meme classe WagonO, ajoutez encore une methode allumerO, qui servira a changer la couleur des 3 fenetres (initialement noires) en jaune, afin de simuler l'allumage d'un eclairage in- terieur. Ajoutez un bouton a la fenetre principale, qui puisse declencher cet allumage. Profitez de l'ame- lioration de la fonction cercleO pour teinter le visage des petits personnages en rose (pink), leurs yeux et leurs bouches en noir, et instanciez les objets-wagons avec des couleurs differentes. 13.7 Ajoutez des correctifs au programme precedent, afin que Ton puisse utiliser n'importe quel bouton dans le desordre, sans que cela ne declenche une erreur ou un effet bizarre. 168 Apprendre a programmer avec Python OscilloGraphe : un widget personnalise Le projet qui suit va nous entrainer encore un petit peu plus loin. Nous allons y constniire 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 mouve- ment 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 l'elongation d'un tel mobile au cours du temps est toujours de la forme e = A sin(2n"/ 1 + cp) , dans laquelle e represente l'elongation du mobile a tout instant t . Les constantes A, /et

from oscillo import * »> gl = OscilloGraphe () »> gl.pack() Apres importation des classes du module oscillo, nous instan- cions un premier objet gl, de la classe OscilloGrapheO. Puisque nous ne fournissons aucun argument, l'objet pos- sede 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=200 , 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 traceCourbeO associee a ce widget. Etant donne que nous ne lui fournissons aucun argu- ment, la sinusoide qui apparait correspond aux valeurs pre- vues par defaut pour les parametres A, f et cp. 170 Apprendre a programmer avec Python »> g3 = OscilloGraphe (larg=220) »> g3 . configure (bg= 'white ' , bd=3, relief=SUNKEN) »> g3 . pack (padx=5 , pady=5 ) »> g3 . traceCourbe (phase=l . 57 , coul= ' purple ' ) »> g3 . traceCourbe (phase=3 . 14 , coul= ' dark green ' ) Pour comprendre la configuration de ce troisieme widget, il faut nous rappeler que la classe OscilloGrapheO a ete construite par derivation de la classe CanvasO. Elle herite done 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 traceCourbeO, a laquelle nous fournissons des arguments pour la phase et la couleur. Exercice 13.8 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. II est temps a present que nous analysions la structure de la classe qui nous a permis d'instancier tous ces widgets. Nous avons done enregistre cette classe dans le module oscillo.py (voir page 168). Cahier des charges Nous souhaitons definir une nouvelle classe de widget, capable d'afficher automatiquement les gra- phiques elongation /temps correspondant a divers mouvements vibratoires harmoniques. Ce widget doit pouvoir etre dimensionne a volonte au moment de son instantiation. II doit faire appa- raitre deux axes cartesiens X et Y munis de fleches. L'axe X representera l'ecoulement du temps pen- dant une seconde au total, et il sera muni d'une echelle comportant 8 intervalles. Une methode traceCourbeO sera associee a ce widget. Elle provoquera le trace du graphique elongation/ temps pour un mouvement vibratoire, dont on aura fourni la frequence (entre 0.25 et 10 Hz), la phase (entre 0 et 2TT radians) et 1'amplitude (entre 1 et 10 ; echelle arbitraire). Implementation • Ligne 4 : La classe OscilloGrapheO est creee par derivation de la classe CanvasO. 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 CanvasO. • 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 : Comme nous l'avons deja dit a plusieurs reprises, le constructeur d'une classe derivee doit presque toujours commencer par activer le constructeur de sa classe parente. Nous ne pou- vons en effet heriter toute la fonctionnalite de la classe parente, que si cette fonctionnalite a ete ef- fectivement mise en place et initialisee. Nous activons done le constructeur de la classe CanvasO 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) . Et comme cela a egalement deja ete explique (cf. page 155), nous devons transmettre a ce construc- teur la reference de l'instance presente (self) comme premier argument. 13. Classes et interfaces graphiques • Ligne 1 1 : 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 inter- valles, que Ton visualise sous la forme de 8 petits traits verticaux. • Ligne 21 : La methode traceCourbeO 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 64. • Lignes 23 a 31 : 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 et e ainsi trouvees sont mises a l'echelle et transformees en co- ordonnees x, y aux lignes 27 et 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.9 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.10 Comme les widgets de la classe CanvasO dont il derive, votre widget peut integrer des indica- tions 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 OscilloGrapheO, puis relancez le script : self. create text(130, 30, text = "Essai", anchor =CENTER) 172 Apprendre a programmer avec Python 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 la figure de gauche page 171. 13.11 Vous pouvez completer encore votre widget en y faisant apparaitre une grille de reference plu- tot 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 de la page 171 . 13.12 Completez encore votre widget en y faisant apparaitre des reperes numeriques. 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 soigneusement ce module, car vous l'integrerez bientot dans un projet plus complexe. Pour l'instant, vous allez construire 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. Presentation du widget Scale 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 rutilisateur 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 rutiliser dans une fenetre : from Tkinter import * | Reglage : ■ ---ll .v,v,v.v,'::| j -25 0 25 50 75 100 125 Valeur actuelle = 0 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 horizon- tale (comme dans notre exemple) ou verticale (option orient = VERTICAL). Les options from_ (attention : n'oubliez pas le caractere 'souligne', lequel est necessaire afin d' eviter la confusion avec le mot reserve from !) 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 updateLabelO, dans notre exemple. 13. Classes et interfaces graphiques 173 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. 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 mo- dule, 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 58 . Nous attirons votre attention sur le fait que le code ci-dessous peut etre raccourci de differentes manieres (nous y re- viendrons). Nous ne l'avons pas optimise d'emblee, parce que cela necessiterait d'y incorporer un concept supplementaire (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 fonctionne- ment. Vous devriez obtenir une fenetre semblable a celle-ci : 7i -|n|x| Frequence (Hz) : Phase (degres) : Amplitude : r AA ill LLI 1.0 3.0 5.0 7.0 9.0 -180 -90 0 90 1 -3.0-3.14159265359-4.0 180 1 3 5 7 9 1# 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 : 11# self.chk = IntVar() # ' ob jet-variable ' Tkinter 12# Checkbutton (self , text= ' Af f icher ' , 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) :', from_=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 :', f rom_=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 =float(p) 35# self .phase = pp*2*pi/360 # conversion degres -> radians 'Vous pourriez bien evidemment aussi enregistrer plusieurs classes dans un meme module. 174 Apprendre a programmer avec Python 36# 37# 38# 39# 40# 41# 42# 43# 44# 45# 46# 47# 48# 49# 50# 51# 52# 53# 54# #### Code pour tester la classe : ### if def setftmplitude (self , a): self . ampl = float (a) self . event_generate ( '' ) name == main ' : def af f icherTout (event=None) : root = Tk() fra = ChoixVibra (root ,' navy ' ) fra .pack (side =TOP) lab = Label (root, text ='test') lab. pack () root. bind ( '' , aff icherTout) root . mainloop ( ) lab. configure (text = ' %s - %s - %s - %s' % self . event_generate ( ' ' ) (fra.chk.get() , fra.freq, fra. phase, fra. ampl)) Ce panneau de controle permettra a vos utilisateurs de regler aisement la valeur des parametres indiques (frequence, phase et amplitude), lesquels pourront alors servir a commander l'affichage de graphiques elongation/ temps dans un widget de la classe OscilloGrapheO construite precedemment, comme nous le montrerons dans Implication 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 re- ceptionner 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 IntVarO, laquelle fait partie du module Tkinter au meme titre que les classes similaires DoubleVarO, StringVarO et BooleanVarO. Toutes ces classes permettent de definir des variables Tkinter, lesquels sont en fait des objets, mais qui se cora- portent comme des variables a l'interieur des widgets Tkinter (voir ci-apres). Ainsi l'objet reference dans self.chk contient l'equivalent d'une variable de type entier, dans un for- mat utilisable par Tkinter. Pour acceder a sa valeur depuis Python, il faut utiliser des methodes spe- cifiques 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 mettra 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 expli- que (les fonctions ou 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. 13. Classes et interfaces graphiques 175 • Lignes 26 a 40 : Les 4 widgets definis dans les lignes precedentes possedent chacun une option com- mand. Pour chacun d'eux, la methode invoquee dans cette option command est differente : la case a cocher active la methode setCurveO, le premier curseur active la methode setFrequencyO, le second curseur active la methode setPhaseO, et le troisieme curseur active la methode setAmplitudeO. Re- marquez bien au passage que l'option command des widgets Scale transmet un argument a la me- thode 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 59 , en faisant ap- pel 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'utilisa- teur 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 pro- duire nous-memes un tel evenement au clavier a titre de test, lorsque le moment sera venu de veri- fier 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 mo- dule par quelques lignes de code au niveau principal. Ces lignes permettent de tester le bon fonc- tionnement 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 aisement 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 ChoixVibraO 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 afficherToutO. 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 ChoixVibraO, chaque fois que l'utilisateur modifie l'etat de l'un ou l'autre des trois curseurs, ou celui de la case a cocher. 59 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 70 : Programmes pilotes par des evenements. 176 Apprendre a programmer avec Python • Concue seulement pour effectuer un test, la fonction afficherToutO ne fait rien d'autre que provo- quer l'affichage des valeurs des variables associees a chacun de nos quatre widgets, en (re)configu- rant l'option text d'un 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 as- socie 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 pro- pagation 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 jus- qu'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.13 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 configured. Essayez par exemple de faire en sorte que le panneau de controle soit entoure d'une bordure de 4 pixels ayant l'aspect d'un 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.14 Si Ton assigne la valeur 1 a l'option showvalue des widgets Scaled, 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.15 L'option troughcolor des widgets Scaled 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 utili- see comme parametre lors de l'instanciation de votre nouveau widget. 13.16 Modifiez le script de telle maniere que les widgets curseurs soient ecartes davantage les uns des autres (options padx et pady de la methode packd). Integration de widgets composites dans une application synthese Dans les exercices precedents, nous avons construit deux nouvelles classes de widgets : le widget OscilloGrapheO, canevas specialise pour le dessin de sinusoides, et le widget ChoixVibrad, panneau de controle a trois cur- seurs permettant de choisir les parametres d'une vibration. Ces widgets sont desormais disponibles dans les modules oscillo.py et curseurs. py 60 . Nous allons a present les utiliser dans une application synthese : 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 ChoixVibrad. 60 I1 va de soit que nous pourrions aussi rassembler toutes les classes que nous construisons dans un seul module. 13. Classes et interfaces graphiques Frequence (Hz) : Phase (degres) : Amplitude : ■ Affile, i m ci mi 1.0 3.0 5.0 7 : 9.0 -180 -90 0 90 180 1 5 9 Frequence (Hz) : Phase (degres) : Amplitude : r m ii m I i m i 1.0 3.0 5.0 9.0 -180 -90 0 90 180 1 3 5 7 9 Frequence (Hz) : Phase (degres) : Amplitude : ■ Affichi i m m i 1.0 3.0 5.0 7.0 9.0 -180 -90 0 90 180 Le script correspondant est reproduit ci-apres. Nous attirons votre attention sur la technique mise en ceuvre pour provoquer un rafraichissement de l'affichage dans le canevas par l'intermediaire d'un evenement, chaque fois que rutilisateur 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 70). En preparant cet exemple, nous avons arbitrairement decide que l'affichage des graphiques serait de- clenche par un evenement particulier, tout a fait similaire a ceux que genere le systeme d 'exploitation lorsque rutilisateur accomplit une action quelconque. Dans la gamme (tres etendue) d'evenements pos- sibles, 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 ChoixVibraO, nous y avons done incorpore les instruc- tions 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 montreCourbesO 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'] 178 Apprendre a programmer avec Python 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 ( ) Commentaires • Lignes 1-2 : Nous pouvons nous passer d'importer le module Tkinter : chacun de ces deux mo- dules 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 nouvelle classe de widget, derivee de la classe FrameO : ainsi nous pourrons plus tard l'integrer toute entiere dans d'autres projets, si le cceur 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 pre- parer 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 OscilloGrapheO a ete obtenue par derivation de la classe CanvasO, il est toujours possible de configurer ce widget en rede- finissantles 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 1'application. Cet attribut se re- 13. Classes et interfaces graphiques 179 vele 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 wid- get tel que FrameO, par exemple (c'est ce que nous avons fait a la ligne 4), Tkinter instancie automa- tiquement 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 cet attribut master que Tkinter associe auto- matiquement 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 specifi- quement declenches par nos widgets ChoixVibraO (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 deleteO : le widget OscilloGrapheO a herite cette methode de sa classe parente CanvasO. Ensuite, de nouvelles courbes sont retracees, pour chacun des panneaux de controle dont on a co- che la case « Afficher ». Chacun des objets ainsi dessines dans le canevas possede un numero de re- ference, renvoye par la methode traceCourbeO de notre widget OscilloGrapheO. Les numeros de reference de nos dessins sont memorises dans la liste self.trace. lis permettent d'effacer individuellement chacun d'entre eux (cf. instruction de la ligne 28). • Lignes 38-40 : Les valeurs de frequence, phase et amplitude que Ton transmet a la methode traceCourbeO 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. Exercices 13.17 Modifiez le script, de maniere a obtenir l'aspect ci-dessous (ecran d'affichage avec grille de refe- rence, panneaux de controle entoures d'un sillon) : r t "f f i 1 i " " I ! i " ' i i i i i T ' 1 "1 "t " i r i " ' 1 ~r i i i i i i i i } I 1 i i i ma ixi Phase IdeaaSij: CD J \ "" ID I 1.0 3:8 5U 7.0 9.0: -130 -90 D ») 181) to i m 1.0 30 50 ?.£i HO -180 -S) 0 SI :180 . 1,0 30 SO 7.0 90 -100 -90 0 90 180 4 6 8 10 180 Apprendre a programmer avec Python 13.18 Modiflez 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.19 Aux lignes 33-35, nous recuperons les valeurs des frequence, phase et 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 l'acces aux proprietes des objets soit tou- jours pris en charge par des methodes specifiques. Pour respecter cette recommandation, ajou- tez a la classe ChoixVibraO une methode supplementaire que vous appellerez valeursO, et qui ren- verra 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.20 Ecrivez une petite application qui fait apparaitre une fenetre avec un canevas et un widget cur- seur (Scale). Dans le canevas, dessinez un cercle, dont Putilisateur pourra faire varier la taille a l'aide du curseur. 13.21 Ecrivez un script qui creera deux classes : une classe Application, derivee de Frame(), dont le constructeur instanciera un canevas de 400 X 400 pixels, ainsi que deux boutons. Dans le cane- vas, vous instancierez un objet de la classe Visage decrite ci-apres. La classe Visage servira a definir des objets graphiques censes representer des visages humains simplifies. Ces visages seront constitues d'un cercle principal dans lequel trois ovales plus petits representeront deux yeux et une bouche (ouverte). Une methode « fermer » permettra de rem- placer l'ovale de la bouche par une ligne horizontale. Une methode « ouvrir » permettra de res- tituer 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 74 pour composer une partie du code. 13.22 Exercice de synthese : elaboration d'un dictionnaire de couleurs. But : realiser un petit programme utilitaire, qui puisse vous aider a construire facilement et rapi- dement un nouveau dictionnaire de couleurs, lequel permettrait l'acces technique a une couleur quelconque par l'intermediaire de son nom usuel en francais. Contexte : En manipulant divers objets colores avec Tkinter, vous avez constate que cette bi- bliotheque 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, yellow, 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 en- codee 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 cou- rantes, consiste a representer une couleur a l'aide de trois octets, qui indiqueront respective- ment 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 colo- ree. 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 representent les 3 valeurs hexadecimales des 3 composantes rouge, vert et bleu. 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). 13. Classes et interfaces graphiques 181 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 1'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 diction- naire 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 com- portera une fenetre avec un certain nombre de champs d'entree et de boutons, afin que l'utilisa- teur puisse aisement encoder de nouvelles couleurs en indiquant a chaque fois son nom francais dans un champ, et son code hexadecimal dans un autre. Lorsque le dictionnaire contiendra deja un certain nombre de donnees, il devra etre possible de le tester, e'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 permet- tra de reconstruire le dictionnaire a partir du fichier. 13.23 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 partici- per a un travail d'equipe. A) Commencez par analyser ce script et ajoutez-y des commentaires, en particulier aux lignes marquees : #***, pour montrer que vous comprenez ce que doit faire le programme a ces em- placements : from Tkinter import * class FaceDom: def init (self, can, val, pos , taille =70): self .can =can tt *** 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 =[] tt *** pDispo = [((0,0),), ((-d,d) , (d,-d)) , ((-d,-d), (0,0), (d,d))] disp = pDispo[val -1] tt *** for p in disp: self . cercle (x +p[0], y +p[l], 5, 'red') def cercle(self, x, y, r, coul) : tt *** self .pList. append (self .can. create oval(x-r, y-r , x+r, y+r, f ill=coul) ) def ef facer (self ) : tt *** 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 182 Apprendre d programmer avec Python self. can = Canvas (self, bg='dark green', width =larg, height =haut) self . can .pack (padx =5, pady =5) ft *** bList = [("A", self.boutA), ("B", self.boutB), ("C", self.boutC), ("D", self.boutD), ("Quitter" , self .boutQuit) ] for b in bList: 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) : ft *** self . d3 . ef facer ( ) def boutQuit (self ) : self . master . destroy ( ) 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 nou- veaux des (de meme taille, plutot petits) 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 quin- conce (c'est-a-dire comme les points d'une 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'ef- facement 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 1 3 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'efface- ment 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 se- 13. Classes et interfaces graphiques 183 conde ligne, tant que cette valeur restera differente de celle du de correspondant dans la pre- miere 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 com- plet (pas seulement les points !) de tous les des affiches. 14 Et pour quelques widgets de plus... Les pages qui suivent contiennent des indications 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 com- plete sur Tkinter. Pour en savoir plus, vous devre% tot ou tard consulter des ouvrages specialises, comme Python and Tkinter programming de John E. Grayson. La programmation, c'est genial O Normal O Gras (*• Italique C Gras/ltaliqi Les boutons radio Les widgets « boutons radio » permettent de proposer a l'utilisateur un ensemble de choix mutuelle- ment 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 enfon- ce : 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 va- riable Tkinter, mais chacun d'entre eux se voit aussi attribuer une valeur particuliere. Lorsque l'utilisateur selectionne l'un des bou- tons, la valeur correspondant a ce bouton est affectee a la variable Tkinter commune. 1# 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 con tenant un petit texte : 10# self. texte = Entry(self, width =30, font ="Arial 14") 11# self .texte. insert (END, "La programmation, c'est genial") 12# self .texte. pack (padx =8, pady =8) 13# # Nom francais et nom technique des guatre styles de police : 14# stylePoliceFr =[ "Normal", "Gras", "Italique", "Gras/Italique" ] 15# stylePoliceTk =[ "normal", "bold", "italic" , "bold italic"] 16# # Le style actuel est memorise dans un ' objet-variable ' Tkinter 17# self .choixPolice = StringVar() 18# self .choixPolice. set (stylePoliceTk [0] ) 19# # Creation des quatre 'boutons radio' : 186 Apprendre a programmer avec Python 20# 21# 22# 23# 24# 25# 26# 27# 28# 29# 30# 31# 32# 33# 34# if def changePolice (self ) : "" "Remp la cement du style de la police actuelle police = "Arial 15 " + self . choixPolice . get () self . texte . configure (font =police) name == main ' : RadioDemo ( ) . mainloop ( ) for n in range (4) : bout = Radiobutton (self , bout. pack (side =LEFT, padx =5) text = stylePoliceFr [n] , variable = self . choixPolice , value = stylePoliceTk [n] , command = self . changePolice) ii ii H Commentaires • Ligne 3 : Cette fois encore, nous preferons construire notre petite application comme une classe derivee de la classe FrameO, ce qui nous permettrait eventuellement de l'integrer sans difficulte dans une application plus importante. • Ligne 8 : En general, on applique les methodes de positionnement des widgets (pack(), grid(), ou place()) apres instantiation de ceux-ci, ce qui permet de choisir librement leur disposition a l'inte- rieur des fenetres mattresses. Comme nous le montrons ici, il est cependant tout a fait possible de deja prevoir ce positionnement dans le constructeur du widget. • Ligne 11 : 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 (il s'agira en fait d'une chaine Unicode). La methode delete!) permet d'en effacer tout ou partie (cf. projet « Code des couleurs », page 161). La methode insert() permet d'inserer de nouveaux caracteres a un emplace- ment 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 nume- rique quelconque pour designer un caractere dans la chaine). • Lignes 14-15 : Plutot 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 prepa- rees dans les deux listes stylePoliceFr et stylePoliceTk : la premiere contient les petits textes qui de- vront s'afficher en regard de chaque bouton, et la seconde les valeurs qui devront leur etre asso- ciees. • 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 me- thodes 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 StringVarO, 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.changePoliceO, 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 : 14. Et pour quelques widgets de plus. 187 ( 'Arial' , 12, 'italic' ) ( 'Helvetica' , 10) ('Times New Roman', 12, 'bold italic') "Verdana 14 bold" "President 18 italic" Voir egalement les examples de la page 207. Utilisation de cadres pour la composition d'une fenetre Vous avez deja abondamment utilise la classe de wid- gets 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 fe- netre. II vous demontre egalement l'utilisation de cer- taines 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 in- dispensable. Le cadre fl contient lui-meme 6 autres cadres, qui contiennent chacun un widget de la classe Label(). Le cadre f2 contient un widget CanvasO et un widget ButtonO. Les couleurs et garnitures sont de simples options. 1# from Tkinter import * 2# 3# fen = Tk() 4# fen. title ("Fenetre composee a l'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 11# 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, 'grey 90' , 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 (f 2, width =80, height =80, bg ='white', bd =2, relief =SOLID) 26# can . pack (padx =15, pady =15) 27# bou =Button ( f 2 , text= ' Bouton ' ) 28# bou . pack ( ) 29# 30# fen . mainloop ( ) 188 Apprendre a programmer avec Python Commentaires • 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 geometryO 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 deter- minee par l'argument bg (background). Cette chaine de caracteres contient en notation hexadeci- male la description des trois composantes rouge, verte et bleue de la teinte que Ton souhaite obte- nir : 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 et 192. En application de cette technique descriptive, le noir serait obtenu avec #000000, le blanc avec #f ff ff f, le rouge pur avec #f f 0000, un bleu sombre avec #000050, etc. • Ligne 8 : Puisque nous lui appliquons la methode pack(), le cadre sera automatiquement dimension- ne par son contenu. L'option side =left le positionnera a gauche dans sa fenetre maitresse. L'op- tion 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 efficace si nous instancions ces widgets dans une liste plutot que dans des variables indepen- dantes. 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 carac- teres 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 d'une liste de tuples a I'aide d'une boucle for constitue une construction particulierement compacte, qui permet de realiser de nombreuses affectations avec un tres petit nombre d'instructions. • 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 rutilisation de la methode pack(), e'est la dimension des etiquettes qui deter- mine la taille des petits 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 eti- quette, et un autre autour de chaque petit cadre. L'option side =top positionne les 6 petits 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 14. Et pour quelques widgets de plus. 189 bordure autour du canevas. Sachez que d'autres widgets acceptent egalement ce genre de decora- tion : boutons, champs d'entree, etc. Comment deplacer des dessins a l'aide de la souris Le widget canevas est l'un des points forts de la bibliotheque grapbique 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 ou- vrage 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 des- sins en le « saisissant » a l'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 inter- face graphique est un logiciel « pilote par les evenements » (revoyez au besoin les explications de la page 70). 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). 190 Apprendre d 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 guand le bouton gauche de la souris est enfonce" self . currObject =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 . selObject = self .c.f ind_closest( self .xl, self.yl) # modification de 1 ' epaisseur du contour du dessin : self . c . itemconfig (self . selObject, width =3) # fait passer le dessin a l'avant-plan : self . c . lift (self . selObject) def mouseMove (self , event): "Op. a effectuer guand la souris se deplace, bouton gauche enfonce" x2 , y2 = event . x , event . y dx, dy = x2 -self.xl, y2 -self.yl if self . selObject: self . c .move (self . selObject, dx, dy) self.xl, self.yl = x2 , y2 def mouseUp (self , event): "Op. a effectuer guand le bouton gauche de la souris est relache" if self . selObject: self . c . itemconfig (self . selObject, width =1) self . selObject =None if name == ' main ' : Draw() .mainloopO 14. Et pour quelques widgets de plus. 191 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 consecu- tives : instanciation d'un objet de la classe definie precedemment, et activation de sa methode mainloopO (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 refe- rences dans des variables. Nous pouvons proceder ainsi parce que Tkinter conserve lui-meme une refe- rence interne pour chacun de ces objets (cf. page 83; si vous travaillez avec d'autres bibliotheques gra- phiques, 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 pos- sibilites, l'indice de la couleur choisie etant determine par la fonction randrangeO 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. 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 itemconfigO et lift() pour epaissir son contour et le faire passer a l'avant-plan. Le « transport » du dessin est assure par la methode mouseMoveO, 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 me- thode 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 selec- tionne, et il nous faut veiller egalement a memoriser les nouvelles coordonnees acquises. La methode mouseUpO termine le travail. Lorsque le dessin transporte 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. 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 de- veloppement rapide d'applications complexes. Si vous souhaitez les utiliser, sachez cependant que les 192 Apprendre a programmer avec Python 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 61 . 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 sous-repertoire . . . /Pmw/demos). Combo Box Les mega-widgets s'utilisent aisement. 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 ma- niere la plus habituelle (avec une boite de liste deroulante). Lorsque rutilisateur 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 cou- leur 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 ope- ree precedemment dans le ComboBox lui-meme (le bouton provoque l'af- fichage du nom de la derniere couleur choisie). 1# 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# couleurs = ('navy', 'royal blue', ' steelbluel ' , 'cadet blue', 11# '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_i terns = couleurs, 23# listheight = 150, 24# selectioncommand = changeCoul) 25# combo. grid (row =2, columnspan =2, padx =10, pady =10) 26# 27# f en . mainloop ( ) 6 ^'installation de la bibliotheque Pmw est decrite dans les annexes. Notez qu'il existe d'autres bibliotheques d'extension de Tkinter : Tix, par exemple, est peut-etre plus interessante desormais (mais elle n'etait pas encore disponible a l'epoque de la redaction de ce texte). 14. Et pour quelques widgets de plus. 193 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 de- truits 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 selec- tionne dans la boite de liste. Vous pourrez egalement retrouver cette selection a l'aide de la me- thode get(), comme nous le faisons a la ligne 8 pour actualiser le libelle. Remarque concernant l'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.). 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 conte- nant un ou plusieurs caracteres non-ASCII (tel qu'une lettre accentuee, par exemple), Tkinter encode cette chaine comme un objet Unicode. Suivant que votre ordinateur utilise l'encodage Latin-1 par defaut (ce qui est encore souvent le cas sous Windows), ou bien Utf-8 (cas de tous les OS recents prenant en charge les normes internationales), vous devrez peut-etre convertir la chaine en objet string pour cer- taines de vos operations d'entree-sortie. Comme explique en detail aux pages 103 et suivantes, cela peut se faire tres aisement en utilisant la fonction integree encoded. Exemple : 1# from Tkinter import * 2# 3# def imprinter () : 4# chl = e.get() # le widget Entry renvoie un objet Unicode 5# ch2 = chl . encode ( ' Utf - 8') # conversion Unicode -> string 6# print chl, type (chl) 7# print ch2 , type(ch2) 8# 9# f = Tk() 10# e = Entry (f) 11# e .pack () 12# Button (f, text ="afficher" , command =imprimer) .pack() 13# £ . mainloop ( ) Essayez ce petit script en entrant des chaines avec caracteres accentues dans le champ d'entree. Essayez encore, mais en remplacant 'Utf-8' par 'Latin-1' a ligne 5. Concluez. 194 Apprendre a programmer avec Python Scrolled Text Ce mega-widget etend les possibilites du widget Text standard, en lui asso- ciant un cadre, un libelle (titre) et des barres de defilement. Comme le demon tr era le petit script ci-dessous, il sert fondamentalement a afficher des textes, mais ceux-ci peuvent etre mis en forme et integrer des images. Vous pouvez egalement rendre « cli- quables » les elements affiches (textes ou images), et vous en servir pour de- clencher toutes sortes de meca- nismes. Dans l'application qui genere la fi- gure ci-dessus, par exemple, le fait de cliquer sur le nom « Jean de la Fontaine » provoque le defilement automatique du texte (scrolling), jus- qu'a ce qu'une rubrique decrivant cet auteur devienne visible dans le widget (voir le script correspon- dant page suivante). 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 carac- teres contenant deux valeurs numeriques reliees par un point (ex : "5.2"). Ces deux valeurs in- diquent 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 choisis- sez 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. 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. 1# 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 ( ) 11# st = Pmw. ScrolledText (fen, 12# labelpos =N, 13# label_text ="Petite demo du widget ScrolledText", 14# label_font =' Times 14 bold italic' , 15# label_f g = ' navy ' , label_pady =5 , 14. Et pour quelques widgets de plus. 195 1 c# J-Off text font — 'Helvetica 11 normal ' , text bg — ' ivory ' , 1 /# text_padx =10, text_pady =10, text_wrap = ' none ' , 1 Q# borderf rarae =1 , 1 Q& J.»ff borderf rame borderwidth =3 , ZUff borderf rame relief =S0LID, usehullsize =1 , ^ff hull_width =370, hull_height = 240) ^ Off st. pack (expand =YES, fill =BOTH, padx =8, pady =8) ^Off # Definition de balises, liaison d'un gestionnaire d'evenement au clic souris : ^Off st . tag_conf igure ( ' titre ' , foreground =' brown', font =' Helvetica 11 bold italic') ^ /# st . tag_conf igure ( ' lien ' , foreground ='blue', font =' Helvetica 11 bold') 'Off st. tag_conf igure (' cible ' , foreground =' forest green' , font =' Times 11 bold') st . tag_bind ( ' lien ' , ' ' , action) JUff Jiff titre ="""Le Corbeau et le Renard J^ff par Jean de la Fontaine , auteur f rancais J Off \ n II II II \n o4ff auteur =""" OOff Jean de la Fontaine JOff ecrivain francais (1621-1695) Jin celebre pour ses Contes en vers , Ooff et surtout ses Fables , publiees de 1668 a 1694.""" flUff # Remplissage du widget Text (2 techniques) : 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 dynamique d'une balise : 49# st.tag_add( 'lien' , '2.4', '2.23') 50# 51# fen . mainloop ( ) Commentaires • Lignes 4-7 : Cette fonction est un gestionnaire d'evenement, qui est appele lorsque l'utilisateur ef- fectue un clic de souris sur le nom de l'auteur (cf. lignes 27-29). A la ligne 6, on utilise la methode tag nextrangef) 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 et 3 e arguments (dans notre exemple, on recherche du debut a la fin du texte entier). La methode tag nextrangeO 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 d'un seul de ces index (le premier) pour activer la me- thode 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 sim- plement 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 syn- taxe qui est presentee dans ces lignes : on y voit qu'il suffit d'associer le prefixe label_ au nom de 196 Apprendre a programmer avec Python l'option que Ton souhaite activer pour definir aisement 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 prefix e text_. • Lignes 18 a 20 : II est prevu un cadre (un widget Frame) autour du widget Text. L'option border- frame = l permet de le faire apparaitre. On accede ensuite a ses options de configuration d'une ma- niere 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 possibilite serait de definir plutot les dimensions de son composant Text (par exemple a l'aide d'op- tions 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'est-a-dire 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 deux dimensions horizontale 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 « cli- quable », avec indication du gestionnaire d'evenement correspondant. • Ligne 42 : Importation de texte a partir d'un fichier. Note : il 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_delete(). 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 rutilisateur doit reussir a cliquer sur un bouton qui s'esquive sans cesse. Si vous eprouvez vraiment des difficultes pour l'attraper, commencez d'abord par dilater la fenetre. Le widget Canvas est tres versatile : il vous permet de combiner a volonte des dessins, des images bit- map, 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 ap- prendre a maitriser en priorite. 14. Et pour quelques widgets de plus. 197 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 FenPrincO, obtenue par derivation a par- tir de la classe de mega-widgets Pmw.ScrolledCanvasO. 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'ceil 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 ceuvre pourrait s'appliquer a n'importe quel autre type de widget, y compris un gros widget compo- site comme ceux que nous avons developpes precedemment. Cette grande souplesse dans le develop - pement d'applications complexes est l'un des principaux benefices apportes par le mode de program- ma tion « 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. 198 Apprendre d programmer avec Python 1# from Tkinter import * 2# import Pmw 3# from random import randrange 4# 5# Pmw. initialise ( ) 6# coul = [ ' sienna ' , ' maroon ' , ' brown ' , ' 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""" 11# 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 , 1 6 ) ] 28# self . can . create_oval (xl , yl , x2 , y2 , fill=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 . f b = 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 () 14. Et pour quelques widgets de plus. 199 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 188. • 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'interme- diaire 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 de- viennent inferieures a celles de la fenetre de visualisation. • Ligne 22 : La methode interiorO 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 randrangeO permet de tirer au hasard un nombre entier compris dans un certain intervalle (veuillez vous referer aux explications de la page 127.) • 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 sou- haite inserer le widget, et la reference de ce widget. • Ligne 36 : La methode resizescrollregionO 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 va- riable self.run 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. Barres d'outils avec bulles d'aide - expressions lambda De nombreux programmes component une ou plusieurs « barres d'outils » (toolbar) constitutes de pe- tits 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 petits 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 aisement obtenir d'autres aspects en choisissant judicieusement les options relief et bd (bordure) dans l'instruction d'ins- 200 Apprendre a programmer avec Python tanciation des boutons. En particulier, vous pouvez choisir relief = flat et bd =0 pour obtenir des petits 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 as- socier 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 1 ' , ' 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 (c'est un simple cadre) : 15# toolbar = Frame (self, bd =1) 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 l'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 =GR00VE, 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# 14. Et pour quelques widgets de plus. 201 41# def action(self, b) : 42# "I'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 bou- tons avec une seule et meme methode (ce sera la methode actionO ), mais en lui transmettant a chaque fois la reference du bouton particulier utilise, de maniere a ce que Taction accomplie puisse etre diffe- rente pour chacun d'eux. Une difficulte 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 difficulte 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 l'operation est un veritable trans- fert d'arguments. Pour illustrer cette technique, remplacez les lignes 27 a 31 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) • Voila pour le principe. Mais tout ce qui precede peut etre simplifie, en faisant appel a une expres- sion 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 expres- sion et non une instruction, on peut rutiliser 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 fonc- tion est anonyme (elle ne possede pas de nom). Par exemple, l'expression : lambda arl=b, ar2=c : bidule (arl ,ar2) 202 Apprendre a programmer avec Python renvoie la reference d'une fonction anonyme qui pourra elle-meme invoquer la fonction biduleO en lui transmettant les arguments b et c , ceux-ci etant utilises comme valeurs par defaut dans la defini- tion des parametres arl et ar2 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 ano- nyme 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 arg (avec la valeur par defaut) pour faire appel a la methode cible self.actionO, et Ton ob- tient 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 bibliographic Fenetres avec menus Nous allons decrire a present la construc- tion d'une fenetre d'application dotee de differents types de menus « deroulants », chacun de ces menus pouvant etre « deta- che » de l'application principale pour de- venir lui-meme une petite fenetre indepen- dante, comme dans 1'iUustration ci-des- sous. Cet exercice un peu plus long nous servira egalement de revision, et nous le realise- rons par etapes, en appliquant une strate- gic de programmation que Ton appelle de- veloppement incremental. Comme nous l'avons deja explique prece- demment 62 , cette methode consiste a com- mencer l'ecriture d'un programme par une ebauche, qui ne comporte que quelques lignes seulement mais qui est deja fonc- tionnelle. On teste alors cette ebauche soi- gneusement afin d'en eliminer les bugs eventuels. Lorsque l'ebauche fonctionne correctement, on y ajoute une fonctionna- lite supplementaire. 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 ef- fectue une analyse serieuse du projet, dont au moins les grandes lignes devront etre convenablement decrites dans un cahier des charges clairement redige. 62 Voir page 5 : recherche des erreurs et experimentation. 14. Et pour quelques widgets de plus. 203 // 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 (Cf. erreurs semantiques, page 5.) 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 mo- difier 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 ap- plication 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 applica- tion type incorporant plusieurs classes d'objets interactifs. 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 defi- nitive. C'est ce que nous nous sommes efforces de faire dans l'exemple ci-dessous : 1# from Tkinter import * 2# 3# class MenuBar ( Frame ) : 4# """Barre de menus deroulants" " " 5# def init (self, boss =None) : 6# Frame. init (self, borderwidth =2) 7# 8# ##### Menu ##### 9# fileMenu = Menubutton (self , text ='Fichier') 10# fileMenu. pack (side =LEFT) 11# # Partie "deroulante" 12# mel = Menu (fileMenu) 13# mel . add_command (label ='Ef facer', underline =0, 14# command = boss . ef facer) 15# mel . add_command (label =' Terminer', underline =0, 16# command = boss. quit) 17# # Integration du menu : 18# fileMenu . configure (menu = mel) 19# 20# class Application (Frame) : 21# """Application principale""" 22# def init (self, boss =None) : 23# Frame. init (self) 24# self .master . title (' Fenetre avec menus') 25# mBar = MenuBar (self ) 26# mBar. pack () 27# self. can = Canvas(self, bg='light grey', height=190, 28# width=250, borderwidth =2) 29# self .can. pack () 30# self. pack () 31# 32# def ef facer (self) : 33# self .can. delete (ALL) Apprendre a programmer avec Python 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 36# 37# 34# 35# if name == main_ app = Application () app . mainloop ( ) de menus ne comporte encore que la seule rubrique « Fi- cbier ». Cliquez sur la rubrique « Fichier » pour faire apparaitre le menu correspondant : l'option « Effacer » n'est pas en- core fonctionnelle (elle servira plus loin a effacer le conte- I.: nu du canevas), mais l'option « Terminer » devrait deja ^ MHMaaaaBMMa ^^ MMMaMMM J 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. Analyse du script La structure de ce petit programme devrait 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 6 ', le corps principal du programme (lignes 35 a 37) comporte l'ins- truction desormais classique : if name == ' main ' : Les deux instructions qui suivent consistent seulement a instancier un objet app et a faire fonctionner sa methode mainloopO. 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 MenuBarO contient la description de la barre de menus. Dans l'etat present du script, elle se re- sume a une ebauche de constructeur. • Ligne 5 : Le parametre boss receptionne la reference de la fenetre maitresse du widget au moment de son instantiation. Cette reference va nous permettre d'invoquer les methodes associees a cette fenetre maitresse, aux lignes 14 et 16. • Ligne 6 : Activation obligatoire du constructeur de la classe parente. • Ligne 9 : Instantiation d'un widget de la classe MenubuttonO, defini comme un « esclave » de self (e'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 defi- nir 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 commando pour installer dans le menu les deux items « Effacer » et « Terminer ». Nous y inte- grons 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 63 Voir page 157: modules contenant des bibliotheques de classes. 14. Et pour quelques widgets de plus. 205 suffit de frapper ce caractere au clavier pour que Taction 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'inter- mediaire du parametre boss. La methode effacerO, que nous definissons nous-meme plus loin, servi- ra 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 ve- nons 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 « es- clave » du widget Menubutton. II faut done bien proceder en trois etapes comme nous Pavons fait, en faisant appel a la methode configured. Cette methode peut etre appliquee a n'importe quel wid- get preexistant pour en modifier l'une ou l'autre option. La classe ApplicationO contient la description de la fenetre principale du programme ainsi que les me- thodes gestionnaires d'evenements qui lui sont associees. • Ligne 20 : Nous preferons faire deriver notre application de la classe FrameO, qui presente de nom- breuses options, plutot que de la classe primordiale Tk(). De cette maniere, l'application toute en- tiere 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 automatique- ment une fenetre maitresse de type Tk() pour contenir 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 prece- dent) 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 d'un widget esclave defi- ni 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. Ajout de la rubrique Musiciens Continuez le developpement de ce petit programme, en ajoutant les lignes suivantes dans le construc- teur de la classe MenuBarO (apres la ligne 18) : ##### 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, foreground= ' royal blue', background =' white', 206 Apprendre a programmer avec Python 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 ApplicationO (apres la ligne 33) : def showMusil7 (self) : self . can . create_text (10 , 10, anchor =NW, text ='H. Purcell ' , f ont= ( ' Times ' , 20, 'bold'), fill =' yellow') def showMusil8 (self) : self . can . create_text (245 , 40, anchor =NE, text ="W. A. Mozart", font = ( ' Times ', 20, 'italic'), fill ='dark green') Lorsque vous y aurez ajoute toutes ces lignes, sauvegar- dez le script et executez-le. Votre barre de menus comporte a present une rubrique supplementaire : la rubrique « Musiciens ». Le menu correspondant propose deux items qui sont affi- ches 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 petits textes sur le canevas. Analyse du script Les seules nouveautes introduites dans ces lignes concernent rutilisation 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 1'utilisation de l'option underline pour designer les caracteres correspon- dant a des raccourcis claviers (en n'oubliant pas que la numerotation des caracteres d'une chaine com- mence a partir de zero), et surtout que l'option command de ces widgets accede aux methodes de l'autre classe, par 1 'intermediate de la reference memorisee dans l'attribut boss. La methode create_text() du canevas doit etre utilisee avec deux arguments numeriques, qui sont les co- ordonnees 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 supe- rieur gauche, SE = angle inferieur droit, CENTER = centre, etc.). 14. Et pour quelques widgets de plus. 207 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 supplementaire : des menus « en cascade ». Veuillez done ajouter les lignes suivantes dans le constructeur de la classe MenuBarO : ##### 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 ApplicationO : def showRomanti (self ) : self . can . create_text (245 , 70, anchor =NE, text = "E. Delacroix", f ont =(' 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 aisement 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. 208 Apprendre a programmer avec Python Ajout de la rubrique Options La definition de cette rubrique est un peu plus compli- quee, parce que nous allons y integrer l'utilisation de va- riables 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 « Musi- ciens » 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 MenuBarO : ##### Menu <0ptions> ##### 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 . add_command (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 . relief Barre) # Integration du menu : optMenu . configure (menu = self .mo) ... ainsi que les definitions de methodes suivantes (toujours dans la classe MenuBar(j) : def reliefBarre (self ) : choix = self .relief .get () self .configure (relief = [FLAT , RAISED , SUNKEN, GROOVE , RIDGE , SOLID] [choix] ) def choixActifs (self) : p = self .actPein. get () m = self .actMusi. get () self .pein. configure (state = [DISABLED , NORMAL] [p] ) self .musi. configure (state = [DISABLED , NORMAL] [m] ) 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 1'utilisateur ne les confonde pas avec de veritables commandes. Les items de la premiere partie sont dotees de « cases a cocher ». Lorsque 1'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 encoche. Les instructions qui servent a mettre en place ce type de rubrique sont assez explicites. Elles presentent en effet ces items comme des wid- gets de type chekbutton : i Fenetre avec menus Musiciens 14. Et pour quelques widgets de plus. 209 self .mo . add_checkbutton (label = 'musiciens', command = choixActifs, variable = mbu .mel .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. Comme nous l'avons deja explique plus haut, 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 un objet-interface, que nous appellerons variable Tkinter pour simplifier 64 . C'est ainsi que dans notre exemple, nous utilisons la classe Tkinter IntVarO pour creer des objets equiva- lents a des variables de type entier. • Nous instancions done un de ces objets-variables, que nous memorisons comme attribut d'ins- tance : self.actMusi =lntVar(). Apres cette affectation, l'objet reference dans self.actMusi contient desormais l'equivalent d'une va- riable de type entier, dans un format specifique a Tkinter. • II faut ensuite associer 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 assi- gner 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 les methodes specifiques de cette classe d'objets : la methode get() pour lire, et la methode set() pour ecrire : m = self .actMusi. get () . Dans cette instruction, nous affectons a m (variable ordinaire de Python) le contenu de la variable Tkinter self.actMusi (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. Menu avec choix exclusifs La deuxieme partie du menu « Options » permet 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, l'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 tra- vail. Dans le contexte particulier de ce menu, nous souhaitons proposer 6 possibilites differentes a l'utilisa- teur. II nous faut done six « boutons radio », pour lesquels nous pourrions encoder six instructions simi- laires 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 Voir egalement page 174. 210 Apprendre a programmer avec Python 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 nou- vel item radiobutton est instancie, dont les options label et value sont extraites de la liste par l'interme- diaire des variables lab et v. Dans vos projets personnels, il vous arrivera frequemment de constater que vous pouvez ainsi rempla- cer des suites destructions 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 un exemple dans le paragraphe suivant. Tachez cependant de garder a l'esprit cette regie essentielle : un bon programme doit avant tout rester tres lisible et bien commente. Controle du flux d'execution a l'aide d'une liste Veuillez a present considerer la definition de la methode reliefBarreO. 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 : if choix ==0 : self . configure (relief =FLAT) elif choix ==1 : self . configure (relief =RAI SED ) elif choix ==2 : self . configure (relief =SUNKEN) etc. D'un point de vue strictement fonctionnel, le resultat serait exactement le meme. Vous admettrez ce- pendant que la construction que nous avons choisie est d'autant plus efficace que le nombre de possibi- lites 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 se- riez peut-etre amene a encoder plusieurs pages de elif ! Nous utilisons encore la meme technique dans la methode choixActifsO. 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 choixActifsO 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. 14. Et pour quelques widgets de plus. 211 Ces variables intermediates m et p ne servent en fait qu'a clarifier le script. II serait en effet parfaite- ment 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 peut se payer d'une certaine perte de lisibilite. Pre-selection d'une rubrique Pour terminer cet exercice, voyons encore comment vous pouvez determiner a l'avance certaines selec- tions, ou bien les modifier par programme. Veuillez done ajouter l'instruction suivante dans le constructeur de la classe Application!) (juste avant l'instruction self.packO, 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 pou- vez 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 nume- rote toujours a partir de zero). Au demarrage du programme, tout se passe done comme si l'utilisateur effectuait tout de suite un pre- mier clic sur la rubrique « Peintres » du menu « Options », ce qui a pour effet de desactiver le menu cor- respondant. 15 Analyse de programmes con ere ts Dans ce chapitre, nous allons nous efforcer d'illustrer la demarche de conception d'un programme graphique, de- puis ses premieres ebauches jusqu'd un stade de developpement relativement avance. Nous souhaitons montrer ainsi combien la programmation orientee objet peut faciliter et surtout securiser la strategie de developpement incremental que nous preconisons 65 . L 'utilisation de classes s 'impose, lorsque I'on constate qu'un projet en cours de realisation se revele nettement plus complexe que ce que I'on avait imagine au depart. Vous vivre^ certainement vous-meme des cheminements simi- laires a celui que nous decrivons ci-dessous. Jeu des bombardes Ce projet de jeu 66 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 petits 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 67 qui nous a servi pour realiser le schema de la page suivante. Voir page 5 : recherche des erreurs et experimentation, et aussi page 202 : fenetres avec menus. 66 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 aisement identifiables. II va de soi que les memes techniques de developpement peuvent s'appliquer a d'autres applications plus « serieuses ». 67 I1 s'agit d'une suite bureautique complete, libre et gratuite, largement compatible avec MS-Office, disponible pour Linux, Windows, Mac OS, 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 214 Apprendre a programmer avec Python 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 Fenetre maTtresse Les obus doivent suivre une / trajectoire parabolique >>> Bourn ! Bourn ! <<< Jouer Instructions Options Rome o I (jjL I 17 ] ( Juliette" Feu ! Hauss e f^] detir ' Feu ! Hausse ^ Quitter Commande 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. 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 des trajectoires balistiques. L'emplacement des canons est defini au debut du jeu de maniere aleatoire (tout au moins en hauteur). Apres chaque tir, les canons se deplacent (afin d'accroitre l'interet du jeu, l'ajustement des tirs etant ain- si rendu plus difficile). Les coups au but sont comptabilises. Le dessin preliminaire que nous avons reproduit ci-dessus est l'une des formes que peut prendre votre travail d' 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 im- portante. 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 mal parametree. • Trop monolithique : cela signifie que Ton a neglige de decomposer un probleme complexe en plu- sieurs sous-problemes plus simples. Par exemple, on a imbrique plusieurs niveaux successifs des- tructions composees, au lieu de faire appel a des fonctions ou a des classes. • Mal parametree : cela signifie que Ton a traite seulement un cas parti culier, 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 (sche- mas, plans, descriptions...) qui constitueront le cahier des charges. Pour les projets de grande envergure, il existe d'ailleurs des methodes d'analyse tres elaborees (L/ML, Merise...) que nous ne pouvons nous permettre de decrire ici car elles font l'objet de livres entiers. Cela etant dit, il faut malheureusement admettre qu'il est tres difficile (et meme probablement impos- sible) de realiser des le depart l'analyse tout a fait complete d'un projet de programmation. C'est seule- ment 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. 15. Analyse de programmes concrets 215 D'autre part, un projet logiciel est pratiquement toujours destine a evoluer : il vous arrivera frequem- ment de devoir modifier le cahier des charges au cours du developpement lui-meme, pas necessaire- ment parce que l'analyse initiale a ete mal faite, mais tout simplement parce que Ton souhaite 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 destructions. Veillez au contraire a decouper votre application en un certain nombre de composants parametrables bien encapsules, de telle maniere que vous puissiez aise- ment 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 seule programmation procedurale (c'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 gra- phiques, tout au long du chapitre 8. Cette facon de proceder ne se justifie cependant que pour de tout petits programmes (des exercices ou des tests preliminaires). Lorsque Ton s'attaque a un projet d'une certaine importance, la complexite des problemes qui se presentent se revele rapidement trop impor- tante, 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 analogic Tous les appareils electroniques sont constitues 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 neces- saire de connaitre leur contenu exact : seule importait leur fonction globale. Les premieres fonctions in- tegrees 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 re- gistres ou des decodeurs, qui purent a leur tour etre integres, et ainsi de suite, jusqu'aux microproces- seurs actuels. Ceux-ci contiennent dorenavant plusieurs millions de composants, et pourtant leur fiabili- te 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 cen- taines 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 aisement creer lui-meme de nouvelles classes pour encapsuler les principaux composants de son application, particulierement ceux qui y ap- paraissent en plusieurs exemplaires. Proceder ainsi est plus simple, plus rapide et plus sur que de multi- 216 Apprendre a programmer avec Python plier les blocs d'instructions similaires dans un corps de programme monolithique, de plus en plus volu- mineux et de moins en moins comprehensible. Examinons a present notre ebauche dessinee. Les composants les plus importants de ce jeu sont bien evidemment les petits canons, qu'il faudra pouvoir dessiner a differents emplacements et dans diffe- rentes 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 pro- prietes ainsi que d'un certain comportement (ce que nous voulons exprimer par la est le fait qu'il de- vront 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. 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 ai- sement un nombre quelconque de ces canons dans le jeu, ce qui nous ouvre des perspectives de deve- loppements ulterieurs. Lorsqu'une premiere implementation de la classe CanonO 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 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 rota- tion de la buse du canon, ainsi que l'une des extremites de la ligne X . epaisse qui representera cette buse. Pour terminer notre dessin, il nous restera alors a determiner les coor- donnees de l'autre extremite de cette ligne. Ces coordonnees peuvent etre calculees sans grande difficulte, a la condition de nous rememorer deux concepts fondamentaux de la trigonometrie (le sinus et le cosinus) que vous devez 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 Y angle. Le cosinus du meme angle est le rapport entre le cote adjacent a I 'angle et I'hypotenuse. Buse du canon, orientable Ainsi, dans le schema ci-contre : sin« = Y- et cos« = h b h a Pour representer la buse de notre canon, en supposant que nous connaissions sa longueur 1 et Tangle de tir CL , il nous faut done tracer un segment de ligne droite epaisse, a partir des coordonnees du centre du 15. Analyse de programmes concrets 217 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 CX , et l'ecart vertical Ay etant egal a l.sin CX . 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 CX, y + l.sin CX. 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 Panalyse que nous avons faite jusqu'a present « tient la route », en realisant un pre- mier 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 simplicite 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 CanonO 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 Tangle 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 : 1# from Tkinter import * 2# from math import pi, sin, cos 3# 4# class Canon (object) : 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 218 Apprendre a programmer avec Python Commentaires • Ligne 6 : Dans la liste des parametres qui devront etre transmis au constructeur lors de l'instancia- tion, nous prevoyons les coordonnees x et y, qui indiqueront l'emplacement du canon dans le cane- vas, mais egalement une reference au canevas lui-meme (la variable boss). Cette reference est indis- pensable : 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-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 devons faire appel a sa methode set{) (ligne 36). Ajout de methodes au prototype Notre prototype est fonctionnel, mais beaucoup trop rudimen- taire. 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 petits 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 de- vons a present nous rappeler quelques elements de physique. Comment un objet laisse a lui-meme evolue-t-il dans I'espace, si Yon neglige les phenomenes secondaires tels que la resistance de I'air ? Ce probleme peut vous paraitre complexe, mais en realite sa re- solution est tres simple : il vous suffit d'admettre que le boulet se deplace a la fois horizontalement et verticalement, et que ces deux mouvements, quoique simultanes, sont tout a fait inde- pendants 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 in- tervalles de temps reguliers, en sachant que : • Le mouvement horizontal est uniforme. A chaque iteration, il vous suffit d'augmenter graduelle- ment 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 : 15. Analyse de programmes concrets 219 Pour commencer, il faut ajouter les lignes suivantes a la fin de la methode constructeur. Elles vont ser- vir 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 presque 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 retrou- ver certaines de ses caracteristiques. Nous voulons en effet que notre classe Canon soit generaliste, c'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. 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. Ensuite, nous devons ajouter deux nouvelles methodes : l'une pour declencher le tir, et l'autre pour ge- rer l'animation du boulet une fois que celui-ci aura ete lance : 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 (e'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 l'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 coordsO (utilisee a la ligne 6) effectue un positionnement absolu ; il faut cependant lui fournir toutes les coordonnees de l'objet (comme si on le redessinait). 220 Apprendre a programmer avec Python - La methode move() (utilisee plus loin, a la ligne 17), provoque un deplacement relatif ; elle s'uti- lise avec deux arguments seulement, a savoir les composantes horizontale et verticale du depla- cement 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 vi- tesse, il nous suffit d'utiliser des relations trigonometriques tout a fait similaires a celles que nous avons deja exploitees pour dessiner la buse du canon. Le signe - utilise a la ligne 10 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'interme- diaire 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 coor- donnees 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 coordsO du canevas : utilisee cette fois avec la reference d'un objet graphique comme unique argument, elle renvoie ses quatre coordonnees dans un tuple. • Lignes 17-22 : La coordonnee horizontale de l'obus augmente toujours de la meme quantite (mou- vement 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 tra- jectoire parabolique. L'operateur += permet d'incrementer une variable : ainsi a += 3 equivaut a a = a + 3. Veuillez noter au passage que /'utilisation de cet operateur specif ique est plus efficace que la re-affectation utilisee jusqu'ici. A partir de la version 2.3, Python initialise automatiquement deux variables nommees True et False pour representer la veracite et la faussete d'une expression (notez bien que ces noms commencent tous deux par une majuscule). Comme nous l'avons fait dans le script ci-dessus, vous pouvez utiliser ces variables dans les expressions conditionnelles afin d'augmenter la lisibilite de votre code. Si vous preferez, vous pouvez cependant continuer a utiliser aussi des valeurs numeriques, comme nous l'avons fait precedemment. (cf. « Veracite/Faussete d'une expression », page 46). 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) Developpement de l'application Disposant desormais d'une classe d'objets « canon » assez bien degrossie, nous pouvons envisager l'ela- boration de l'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'ob- jets qui interagissent par I'intermediaire de leurs methodes. Plusieurs de ces objets proviendront de classes preexistantes, bien entendu : ainsi le canevas, les bou- tons, etc. Mais nous avons vu dans les pages precedentes que nous avons interet a regrouper des en- sembles 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 en- semble de cercles et de lignes mobiles que nous avons decide d'appeler « canon ». 15. Analyse de programmes concrets 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. II est d'autant plus interessant de lui consacrer une classe particuliere, que nous sa- vons d'emblee qu'il nous en faudra deux instances. II y a aussi l'application elle-meme, bien sur. En l'encapsulant dans une classe, nous en ferons notre ob- jet principal, celui qui dirigera tous les autres. Veuillez a present analyser le script ci-dessous. Vous y retrouverez la classe CanonO encore davantage developpee : nous y avons ajoute quelques attributs et trois methodes supplementaires, afin de pouvoir gerer les deplacements 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 CanonO, et deux objets de la nouvelle classe PupitreO, que nous placons dans des diction- naires 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 (object) : 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 (-l: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) 222 Apprendre d programmer avec Python O A Jl 20ff self . x2 , self.y2 = x + self.lbu * sens, y 21ff self . buse = boss. create line (self . xl , self.yl, 22ff sell .x/J , selr.yz, wiutn — 1UJ 0*3 44 2jff ff dessiner le corps du canon (cercle de couleur) : 24ff self . rc = 15 # rayon du cercle 2off self . corps = boss. create oval (x -self.rc, y -self.rc, x +self.rc, *5 £44 ^Off y +self . rc , fill =coul) 27ff # pre-dessiner un obus cache (point en dehors du canevas) ^off sen . oous — doss . create ovaii iu, j.u, j.u, j.u, 1111 — rea j 29ff self . anim = False # indicateurs d' animation 30ff self . explo = False # et d' explosion Jiff J2ff aer orienter (self , angle) : J Jff "regler la hausse du canon" J4ff # rem: le parametre est recu en tant que chaine. •3 C4t JOff # 11 faut done le traduire en reel, puis le convertir en radians : OOff self. angle = float (angle) *pi/180 37ff # rem: utiliser la methode coords de preference avec des entiers : ooff self.x2 = int(self.xl + self.lbu * cos (self . angle) * self. sens) J9ff self.y2 = int (self.yl - self.lbu * sin (self . angle) ) ami self .boss . coords (self .buse , self.xl, self.yl, self.x2, self.y2) 41ff yi 0 # 42ff def deplacer (self , x, y) : /I "3 if 4 off "amener le canon dans une nouvelle position x, y" A A # 44ff dx, dy = x -self.xl, y -self.yl # valeur du deplacement yl c; # 4off self .boss .move (self .buse , dx, dy) x £4t 4bff self .boss .move (self . corps , dx, dy) yi "7 4f 4 /ff self.xl += dx 4Bff self.yl += dy 49ff self .x2 += dx OUff self .y2 += dy CI 41 Olff 02ff def feu (self) : C3 41 03ff "tir d'un obus - seulement si le precedent a fini son vol" 04ff if not (self .anim or self .explo) : ooff self . anim =True OOff # recuperer la description de tous les canons presents : C7JI 0 /ff self . guns = self . appli . dictionnaireCanons ( ) CQJ1 ooff # position de depart de 1 obus (c est la bouche du canon) oyff self .boss . coords (self . obus , self.x2 -3, self.y2 -3, oUff self.x2 +3, self.y2 +3) c 1 ii olff v = 17 # vitesse initiale 62ff # composantes verticale et horizontale de cette vitesse : C3 44 OJff self .vy = -v *sin (self . angle) o4ff self.vx = v *cos (self . angle) *self.sens OOff self . animer_obus ( ) ooff return True # => signaler que le coup est parti bit else : OOff return False # => le coup n ' a pas pu etre tire C Q44 69ff /Off def animer_obus (self ) : T] Ji /Iff "animer 1 ' obus (trajectoire balistique) " /2ff if self. anim: HA Ji 73ff self .boss .move (self . obus , int(self .vx) , int(self .vy) ) T >1 44 /4ff c = self .boss . coords (self . obus) # coord, resultantes ^ c 44 /Off xo , yo = c [ 0 ] +3 , c [ 1 ] +3 # coord . du centre de 1 ' obus T £44 /Off self . test_obstacle (xo, yo) # a-t-on atteint un obstacle ? / /ff self .vy += . 4 # acceleration verticale /off self .boss . after (20 , self . animer_obus) 79ff else : 80# # animation terminee - cacher 1 ' obus et deplacer les canons : 81# self. fin animation () 82# 83# def test_obstacle (self , xo, yo) : 84# "evaluer si 1 ' 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# 89# # analyser le dictionnaire des canons pour voir si les coord. # de l'un d'entre eux sont proches de celles de 1 ' obus : 15. Analyse de programmes concrets 223 n ft JI 90# for id in self. guns: # id = clef dans dictionn. 91ff gun = self . guns [id] # valeur correspondante fto JI 92# if xo < gun . xl +self.rc and xo > gun.xl -self.rc \ 9jff and yo < gun.yl +self . rc and yo > gun.yl -self.rc : QAU 94ff self . anim =False 9£>ff ft dessiner 1' explosion de 1 1 obus (cercle jaune) : 9bff self.explo — self . boss . create oval (xo -12, yo -12, 9 /ff „ „ i 1 o . . „ _i_io 1 1 _i . . n i i . 1 . . ■ _j 4_i_ _ft\ xo +±Z , yo +±Z , rill = yellow , wiatn =U) QQl 9Hff self .hit =id ff reference de la cible touchee 99ff sell . jdoss . at ter \ lou , sell . tin explosion) lUUff break 1 m ji lUlff lU.£ff aer fin explosion ( self ) : 1U off "ef facer 1' explosion ; re-initaliser 1 ' obus ,' gerer le score" lU4ff self . boss . delete (self . explo) # effacer l'explosion 105# self.explo =False # autoriser un nouveau tir lUOff $ signaler le succes a la fenetre maitresse : 1 ft "7 JJ 1U /ff self . appli .goal (self . id, self .hit) lUBff 109ff def fin animation (self ) : llOff "actions a accomplir lorsque 1 ' obus a termine sa trajectoire" 1114 self .appli. disperser() # deplacer les canons 1 1 9# 1 izff # cacher 1 ' obus (en l'expediant hors du canevas) 1 1 *3# lljff self .boss. coords (self .obus, -10, -10, -10, -10) 1 1 AH 1 1 K# llOff class Pup itre (Frame) : 1 1 llbff """Pupitre de pointage associe a un canon""" I 1 T# II /ff def init (self, boss, canon): lloff Frame. init (self, bd =3, relief =GROOVE) 1 1 a# 119ff self. score =0 1 9 ft # l^Uff self. appli =boss # ref. de 1 ' application 121ff self. canon =canon # ref. du canon associe 1 0 0 4 122ff # Systeme de reglage de 1 ' angle de tir : 1 9*3# 1^ Off self.regl =Scale(self, from_ =85, to =-15, troughcolor=canon . coul , lZ4ff command =self . orienter) l^off self . regl . set (45) # angle initial de tir 1 9*;* 1-c off self .regl. pack (side =LEFT) 1^ /ff # Etiquette d' identification du canon : 1 0 0 4 128ff Label(self, text =canon . id) .pack (side =TOP, anchor =W, pady =5) 1 9 Q# i^yff # Bouton de tir : 1 ^ ft # UUff self.bTir =Button (self , text ='Feu !', command =self.tirer) 1 "3 1 # 1 Jiff self .bTir. pack (side =BOTTOM, padx =5, pady =5) 1 Q9# loVff Label(self, text ="points") .pack () Ujff self. points =Label (self , text=' 0 ' , bg ='white') 1 Q yl U 134ff self . points . pack ( ) 135# # positionner a gauche ou a droite suivant le sens du canon : 1 Jot if canon. sens == -1: 1 51J1 13 /ff self .pack (padx =5, pady =5, side =RIGHT) 1 *3 O JJ 1 Joff else : 1 0 n tl 139ff self .pack (padx =5, pady =5, side =LEFT) 140ff 141ff def tirer (self) : 1 /I 9# 14^ff "declencher le tir du canon associe" 14jff self . canon . feu ( ) 1 A A # 144ff 1 X R# 143ff def orienter (self , angle): 1 AfM 14bff "ajuster la hausse du canon associe" 1 n# 14 /ff self . canon . orienter (angle) 1 X Q# 14off 1 A Q# 143ff def attribuerPoint (self , p) : 1 CftJl 150ff "incrementer ou decrementer le score, de

points" 151# x x tr self. score += p 152# self .points . config (text = ' %s ' % self. score) 153# 154# class Application (Frame) : 155# ' ' ' Fenetre principale de 1 ' application ' ' ' 156# def init (self) : 157# Frame. init (self) 158# self .master, title ('»»> Bourn ! Bourn ! ««< ' ) 159# self .pack () 224 Apprendre a programmer avec Python J-OUff self . jeu — Canvas (self , width —400 r height —250 , bg — ivory , lOlff dq — j r renei — oujnivcjJnj 1 XOZff sell . jeu . paCX. ipaux — o r paUy — o r Slue — ±\Jc } self . guns ={ } # dictionnaire des canons presents IDOff self . pupi = { } $ dictionnaire des pupitres presents looff # Instanciation de 2 objets canons (+1, -1 = sens opposes) : ID/ff ea 1 f mine r"Di 1 1tr"1 — PariAn / eel f -i on "Hi 1 1 w" ^ 0 0 0 fl 1 " rar] " ^ seii . guns |_ rsiiiy j — Lanon ^seir , ]eu, Diiiy , ju , zuu , x, rea j J-Doff col f minfi r"T.iniia"1 — Pa non /eol f -ion "T.inns" ^7fl 9 Ofl — 1 "hlno'M oell . UUXlo L lil 11 Llo j — y^alLKjLL \ oeii . Jell , lilll Ul o , j / u , £UU ^ l , Ul Uc / ±t>yff ff Instanciation de 2 pupitres de pointage associes a ces canons J. / Uff sell - pupi l Diiiy j — fupitre (sen , sen . guns [ oiiiy j ^ J. / Iff self . pupi [ " Linus " ] = Pupi tre ( self , self. guns [ "Linus " ] ) J. /Zff J. / Off Uel Ulbpclacl ^Sell/ . J. /fiff QcpioCel dlcdLUllcIIlcIll. IcS CJclIlCJilo J. /off for id in self . guns '. J- ' Off gun =self . guns [ id] j. / /ff $ positionner a gauche ou a droite , suivant sens du canon i noJl 1 /off if gun . sens == -1 i / »ff x — ranarange pzu , jou) i on4 J-oUff else : 1 Q 1 Ji lolff x — ranarange (zu , oU) 1 aojl lozff ff deplacement proprement dit : 1 Q *3 41 18 Jff gun . aepiacer (x , ranarange ( lou , ; ±o4ff 1 RR£ J-oOff aer goal (self , i, j) • 1 Q £Ji looff "le canon signale gu'il a atteint 1 ' adversaire " IB /ff 11 1 : — j . 1 QQ# looff self .pupi [i] . attribuerPoint (1) 189# else : 190# self .pupi [i] . attribuerPoint (-1) 191# 192# def dictionnaireCanons (self) : 193# "renvoyer le dictionnaire decrivant les canons presents" 194# return self. guns 195# 196# if name ==' main ' : 197# Application () .mainloopO 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 CanonO a l'aide d'un nom quel- conque. 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 re- ference leur widget maitre even fuel (leur « contenant »). Cette reference est done pour nous celle de l'application principale. Nous avons implemente nous-memes une technique similaire pour referen- cer 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 servi- ra a repositionner les canons au hasard apres chaque tir, ce qui augmente l'interet du jeu. • 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 pour- ront 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 de- terminer si une cible a ete touchee ou non. Cette description est geree par l'application principale, dans un dictionnaire, dont on peut demander une copie par l'intermediaire de sa methode diction- naireCanons(). • Lignes 66 a 68 : Dans cette meme perspective generaliste, il peut etre utile d'informer eventuelle- ment le programme appelant que le coup a effectivement ete tire ou non. 15. Analyse de programmes concrets 225 • Ligne 76 : Inanimation 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 obstacleO). • Lignes 79 a 81 : Nous avons vu precedemment que Ton interrompt 1'animation de l'obus en attri- buant une valeur « fausse » a la variable self.anim. La methode animer obusO cesse alors de boucler et execute le code de la ligne 81. • 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'interrup- teur d'animation est actionne, mais dans le second, on dessine une « explosion » jaune, et la refe- rence du canon touche est memorisee. La methode annexe fin explosionQ est invoquee apres un court laps de temps pour terminer le travail, c'est-a-dire effacer le cercle d'explosion et envoyer un message a la fenetre maitresse pour signaler le coup au but. • Lignes 115 a 152 : La classe PupitreO definit un nouveau widget par derivation de la classe FrameO, 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 determi- ne. La correspondance visuelle entre les deux est assuree par l'adoption d'une couleur commune. Les methodes tirer() et orienterO communiquent avec l'objet CanonO associe, par l'intermediaire des methodes de celui-ci. • Lignes 154 a 171 : La fenetre d'application est elle aussi un widget derive de FrameO. Son construc- ted instancie les deux canons et leurs pupitres de pointage, en placant ces objets dans les deux dic- tionnaires 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 possibilite d'augmenter sans effort le nombre de ces canons si necessaire, dans les develop- pements ulterieurs du programme. • Lignes 173 a 183 : Cette methode est invoquee apres chaque tir pour deplacer aleatoirement les deux canons, ce qui augmente la difficulte du jeu. Developpements complementaires Tel qu'il vient d'etre decrit, notre programme correspond deja plus ou moins au cahier des charges ini- tial, 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 modi- fier ces valeurs, nous devons veiller a modifier aussi les autres lignes du script ou ces dimensions inter - viennent (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 ju- dicieux de dimensionner le canevas a I'aide de variables, dont la valeur serait definie en un seul endroit. Ces variables seraient ensuite exploitees dans toutes les lignes destructions ou les dimensions du cane- vas interviennent. Nous avons deja effectue une partie de ce travail : dans la classe CanonO, en effet, les dimensions du ca- nevas sont recuperees a I'aide d'une 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 co- ordonnees au hasard. II serait probablement plus realiste de provoquer de veritables deplacements rela- tifs, plutot que de redefinir au hasard des positions absolues. Pour ce faire, il suffit de retravailler la me- thode deplacerO de la classe CanonO. 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. 226 Apprendre a programmer avec Python 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 meca- nisme 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. 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 permettre la mise en ceuvre 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 do- maines 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 simultane- ment (cela nous sera necessaire, si nous voulons construire une application capable de communi- quer 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 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 18) : vous y trouverez la version reseau de notre jeu de bom- bardes. 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 l'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. 15. Analyse de programmes concrets 227 Nous disposons ainsi d'un veritable 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 perfec- tionner 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 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 ' ' ' 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) 228 Apprendre a programmer avec Python OOff O / ff belt . guns — \ / ft QiLtioiiiidiic cj.tr 0 canons presents OOff self . pupi ={ } # dictionnaire des pupitres presents 69# self . specif icites () # objets dif f erents dans classes derivees /Uff /Iff aer specif 1 cites (self) : /*!# " ins tanciation des canons et des pupitres de pointage" / -iff sell . master . title ( <« Jeu des JDomoara.es >» ; /4ff ia 11st. — l v iraui , rea ) , \ Komeo , cyan ) f /Off 1 "\7-i vni n -i a << << /-»-►- -1 r-i /-»/-, H\ /M Till -iaf fa» »K1 iia'M 1 \ virginie , orange ) , \ Juliette , oiue ) j / off s = False / /ff for id, coul in id list: /off ir s . /yff sens =1 oUff else . SI Ji O J-ff sens =— 1 O^ff a f y — sell alcaL. ^bcllb ^ OJff self . guns [ id] — Canon (self . jeu , id, x, y / sens , coul) Ofiff Sell . pupi [ iu j — cohqhuj . rupiLic ^ bci x , bcii . guns [ 1Q J / ooff s = not s # changer de cote a chaque iteration OOff Q -7 4f O /ff aer aepi aleat canon (sell , id) : Q Q ooff " deplacer aleatoirement le canon " o»ff gun =self . guns [ id] yuff dx r dy = r andr ange ( — 60, 61) , r andr ange ( — 60, 61) yiff $ deplacement (avec recuperation des nouvelles coordonnees) 004* 9^ff x, y = gun . deplacer (dx , dy, True) Qqft »-3ff return x, y »4ff QCJl yoff aer coord aleat (self , s) : yoff " coordonnees aleatoires , a gauche ( s =1 ) ou a droite ( s =— 1 ) " 3 /ff y = r~andrange ( int (self . ym /2) , self, ym — 20 ) yoff ■i € e -1 ■ 11 5 1 . QQ# 3»ff X — LallUidiiye ^ 111 U. ^ ofcrll . j\x\\ ,l) t bell . & U / J-UUff else : 1 ni tt lUlff X — X alia! dliy ci \£.\J r lilt ^bcll . XIII . O / } J-U^ff return x, y 1 A *3 41 1U Jff 1 it lU4ff aer goal(self, i, j) : J-UOff " le canon n i signale cju 1 il a atteint 1 1 adversaire n j " luoff # de quel camp f ont— ils par tie chacun ? J-U /ff ti , tj = self .guns[i] .sens, self . guns [ j ] .sens 1 AQJ1 lUOff if ti ! = tj # ils sont de sens opposes : 1 n q it p = 1 % on gagne 1 point J. J-Uff else : ff ils sont dans le meme sens '. 1 1 1 ji lllff p — ff on a toucne un aine ! * llzff 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 CanonO derivee de la precedente, tout en lui conservant le meme nom. De cette maniere, les por- tions 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 11 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 de- placerO a ete surchargee). Lorsque Ton realise ce genre de modification, on s'efforce en general de 15. Analyse de programmes concrets 229 faire en sorte que la nouvelle methode effectue le meme travail que l'ancienne quand elle est invo- quee de la meme facon que l'etait cette derniere. On s'assure ainsi que les applications qui utili- saient 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 for- ceront 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 pre- voir 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 pa- rente, 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 telle maniere qu'elle puisse aisement 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 even- tuelle. La methode specificitesO contient des portions de code plus specifiques : cette methode est clairement destinee a etre surchargee dans les classes derivees eventuelles. 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 uti- liser plusieurs classes afin de construire un script bien structure. II vous montre egalement comment vous pouvez parameter une application graphique de maniere a ce que tout y soit redimensionnable. Principe Le «jeu» mis en ceuvre ici est plutot une sorte d'exercice mathematique. II se joue sur un panneau ou est represente 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/Rev erst), 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 de- vient 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. 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. 230 Apprendre a programmer avec Python Has Fichier Aide Mombre de lignes 4 Nombre de colonnes 7 □■□□I □■□I Principe du jeu A propos ... Program mati on 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 soigneusement 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 fonc- tion des redimensionnements operes par l'utilisateur, le cadre occupera a chaque fois toute la place disponible : il se presentera done au programmeur comme un rectangle quelconque, dont les di- mensions doivent servir de base au calcul des dimensions de la grille a dessiner. 15. Analyse de programmes concrets 231 • 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 cou- leurs noire et blanche). ########################################### # Jeu de ping # # References : Voir article de la revue # # , Aout 2002 # # # # (C) Gerard Swinnen (Verviers , Belgique) # # http://www.ulg.ac.be/cifen/inforef/swi # # # # Version du 29/09/2002 - Licence : GPL # ########################################### from Tkinter import * class MenuBar (Frame) : ' Bane de menus deroulants" " " def init (self, boss =None) : Frame. init (self, borderwidth =2, relief =GROOVE) ##### Menu ##### 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 chague 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 = 4 x 4 # 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 1 ' evenement a son gestionnaire : self .can. bind ("" , self .clic) self . can . pack ( ) self .init Jeu () 232 Apprendre d programmer avec Python def initJeu (self ) : "Initialisation de la liste memorisant l'etat du jeu" self.etat =[] # construction d'une liste de listes for i in range (12) : # (equivalents 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 1 ' 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) # -> etablissement 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) 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 logigue : self .etat[l] [c] = not (self .etat[l] [c] ) self . traceGrille ( ) class Ping (Frame) : """corps principal du programme""" def init (self) : Frame . init (self) self .master. geometry ("400x300") self .master . title (" Jeu de Ping") 15. Analyse de programmes concrets 233 self . mbar — MenuBar (self ) sexr . moar . pacK ^ s iae — iuf, expana — inu, 1111 — self . jeu =Panneau (self) self . jeu . pack (expand =YES , f ill=BOTH , padx =8 , pady =8) sexr . pacK ( ) aer options (self ) : "Choix du nombre de lignes et de colonnes pour la grille" opt =Toplevel (self) curii — bcaie (opt, xengtn — zuu, laoei — Nomore ae xxgnes . , orient HORIZONTAL, from =1, to =12, command =self .majLignes) curL . set ( self . j eu . nlig) ft position initiale du curseur curL . pack ( ) curH =Scale(opt, length =200, label ="Nombre de colonnes :", orient HORIZONTAL, irom — x , to — xz , coinmana — sexr . ma j Loionnes ^ curH . set (self . jeu . ncol) curH . pack ( ) aer majColonnes (self , n) : self . jeu . ncol = int(n) self . jeu . traceGrille ( ) Cat: J- Illci J JjlCJIltro ^Sell , 11) . self . j eu . nlig = int(n) self . j eu . traceGrille ( ) aer reset (sell ) : self . jeu . init Jeu ( ) self . jeu . traceGrille ( ) aer principe (self ) : "Fenetre -mess age 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 l'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. 11 devient plus difficile avec "\ "des grilles plus grandes. 11 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 l'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 == 1 main 1 : Ping() .mainloopO Rappel Si vous souhaitez experimenter ces programmes sans avoir a les reecrire, vous pouvez trouver leur code source a I'adresse : http://www.ulg.ac.be/cifen/inforef/swi/python.htm. 16 Gestion (Tune 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 ave% surement ete deja confronted a ce probleme : des donnees identiques ont ete enregistrees dans plusieurs fichiers differents. Lorsque vous souhaite^ modifier ou supprimer I'une de ces donnees, vous deve% ouvrir et modifier tous les fichiers qui la contiennent I Le risque d'er- reur 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 sys femes, mais nous n'en examinerons que deux dans nos exemples : Gadfly et MySQL. 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 instruc- tions simples, telles celles que nous avons abordees page 91. 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 difficulte augmente encore si les donnees sont repertoriees dans differents ensembles relies par un certain nombre de relations hierar- chiques, 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 sys- teme 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. 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. 236 Apprendre a programmer avec Python 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 neurons de societes specialisees (IBM, Oracle, Microsoft, Informix, Sy- base...) et sont en general vendues a des prix fort eleves. D'autres ont ete developpees dans des centres de recherche et d'enseignement universitaires (PostgreSQL, MySQL...) ; 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 don- nees prise en charge) est installee en un seul endroit, en principe sur une machine puissante (cet en- semble constituant done le serveur), alors que l'autre partie, beaucoup plus simple, est installee sur un nombre indetermine de postes de travail, appeles des clients. Les clients sont relies au serveur, en permanence ou non, par divers procedes et protocoles (eventuelle- ment par l'intermediaire d'Internet). Chacun d'entre eux peut acceder a une partie plus ou moins im- portante des donnees, avec autorisation ou non de modifier certaines d'entre elles, d'en ajouter ou d'en supprimer, en fonction de regies d'acces bien determinees, 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. Imagi- nez par exemple que vous etes l'un des utilisateurs du systeme. Pour acceder aux donnees, vous devez lancer l'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 don- nees 68 . 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 expe- die 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 expedites du client au serveur, non seulement pour extraire des donnees de la base, mais aussi pour en ajouter, en supprimer, en modifier, etc. Le langage SQL - Gadfly Etant donnee la diversite des SGBDR existants, on pourrait craindre que chacun d'eux necessite l'utili- sation d'un langage particulier pour les requetes qu'on lui adresse. En fait, de grands efforts ont ete ac- complis un peu partout pour la mise au point d'un langage commun, et il existe a present un standard bien etabli : le SQL (Structured Query Language, ou langage de requetes structure) 69 . 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 ceuvre d'un petit SGBDR realise ex- clusivement 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. 68 n 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... 69 Quelques variantes subsistent entre differentes implementations du SQL, pour des requetes tres specifiques, mais la base reste cependant la meme. 1 6. Gestion d'une base de donnees 237 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 70 . II integre un large sous-ensemble de commandes SQL. Ses performances ne sont evidemment pas comparables a celles d'un gros SGBDR specialise 71 , mais elles sont tout a fait excellentes pour la gestion de bases de donnees modestes. Absolument portable comme Python lui-meme, Gadfly fonctionnera indifferemment sous Windows, Linux ou Mac OS. 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. 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. Creation de la base de donnees Comme vous vous y attendez certainement, il suffit d 'importer le module gadfly pour acceder aux fonc- tionnalites correspondantes. Vous devez ensuite creer une instance (un objet) de la classe gadflyO : 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 evi- demment 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 serait alors ecrasee sans avertissement. 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, suppri- mer 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 ceuvre un curseur. II s'agit d'une sorte de tampon memoire intermediate, destine a memoriser tempo- rairement 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). 70 Le module Gadfly est disponible gratuitement sur Internet. Voir http://sourceforge.net/projects/gadfly. L'installation de ce module est decrite page 292. 7 '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 ambitieux tels que PostgreSQL, pour lesquels des modules clients Python existent aussi (Pygresql, par ex.). 238 Apprendre a programmer avec Python 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 ( ) 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 en- vironnantes, 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 lon- gueur variable 72 ) 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 trans - fert sur disque est active par la methode commito de la derniere ligne d'instructions. 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/gadfly") 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 se- lection 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 enre- gistrements de la table membres (le symbole * est frequemment utilise en informatique avec la significa- tion « 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 ». 72 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. 1 6. Gestion d'une base de donnees 239 Si vous preferez controler vous-meme la mise en page des informations, il vous suffit d'utiliser a sa place la methode fetchallO, laquelle renvoie une liste de tuples. Essayez par exemple : for x in cur . fetchall () : print x, x[0], x[l], x[2] Vous pouvez bien entendu ajouter des enregistrements supplementaires : cur. execute ("Insert Into Membres (Age , Nom, Taille) Values (19 , ' Ricard' , 1 . 75) " ) 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 commito precedent, en refer- mant la connexion a l'aide de l'instruction : baseDonn . close ( ) Recherches dans une base de donnees Exercice 16.1 Avant d'aller plus loin, et a titre d'exercice de synthese, nous allons vous demander de creer en- tierement 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) : Qeuvres Compositeurs comp (chatne) comp (chatne) titre (chatne) a_naiss (entier) duree (entier) ajnort (entier) interpr (chatne) 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 faci- liter 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 240 Apprendre d programmer avec Python Dans la table oeuvres, entrez les donnees suivantes : comp ci ere duree interpr V lvdlul beb qUdLIe Saloons 9(1 T. Pinnock Mozart Concerto piano N°12 *5 R M. Perahia Brahms Concerto violon N°2 40 A. Grumiaux 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 ajouter autant d'enregistrements d'eeuvres 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, veuillez consulter le corrige de l'exercice 16.1, a la page 334. 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 commitO 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/gadfly") 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() # affichage 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 ( ) Cette application tres simple n'est evidemment qu'un exemple. II faudrait y ajouter la possibilite 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 99. 1 6. Gestion d'une base de donnees 241 La requete select L'une des instructions les plus puissantes du langage SQL est la requete select, dont nous allons a pre- sent 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 re- queues 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 flltres 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.a_naiss 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 sys- teme de bases de donnees, mais en supposant cette fois qu'il s'agisse de dialoguer avec un systeme ser- veur independant (lequel pourrait etre par exemple un gros serveur de bases de donnees d'entreprise, un serveur de documentation dans une ecole, etc.). Ebauche d'un logiciel client pour MySQL Pour terminer ce chapitre, nous allons vous proposer dans les pages qui suivent un exemple de realisa- tion 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 « pen- ser comme un programmeur » lorsque vous abordez un probleme complexe. Les techniques que nous allons mettre en ceuvre 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 cri- tiquables : vous pouvez bien evidemment traiter les memes problemes en utilisant des approches diffe- rentes. 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 compor- tant plusieurs tables, qu'il puisse servir a produire des enregistrements pour chacune d'elles, qu'il per- mette 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 242 Apprendre a programmer avec Python du mot de passe « abcde ». Ce serveur peut etre situe sur une machine distante accessible via un reseau, ou localement sur votre ordinateur personnel. L'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, Ubuntu, RedHat, SuSE... II vous suffit d'installer les paque- tages MySQL-server et Python-MySQL, de demarrer le service MySQL, puis d'entrer les commandes : my sql admin -u root password xxxx Cette premiere commande definit le mot de passe de l'administrateur principal de MySQL. Elle doit etre executee par l'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) : my sql -u root mysql -P grant all privileges on * . * to jules@localhost identified by 'abcde'; grant all privileges on * . * to jules@"%" identified by 'abcde'; \q Ces commandes definissent un nouvel utilisateur « jules » pour le systeme MySQL, et cet utilisateur de- vra se connecter grace au mot de passe « abcde » (Les deux lignes autorisent respectivement l'acces lo- cal et l'acces via reseau.) Le nom d'utilisateur est quelconque : il ne doit pas necessairement corres- ponds 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; \q . . . etc . A ce stade, le serveur MySQL est pret a dialoguer avec le client Python decrit dans ces pages. 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 re- groupant 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 destructions 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 enregistre- ments. Or, l'experience montre que la structure d'une base de donnees est rarement definitive. Au cours d'un developpement, on realise souvent qu'il est necessaire de lui ajouter ou de lui retirer des champs, par- fois meme de remplacer une table mal concue par deux autres, etc. II n'est done pas prudent de pro- grammer 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 des- tructions 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 maltresse pour realiser des applications robustes : un logiciel destine au traitement de donnees devrait toujours etre construit sur la base d'un dictionnaire d'application. 1 6. Gestion d'une base de donnees 243 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 ins- taller 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 don- nees 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 l'applica- tion. Ainsi encapsulees dans l'espace de noms d'une classe, ces variables peuvent etre utilisees sans pro- bleme 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 va- riables definies de cette maniere apparalt tees clairement dans leur nom qualifie, puisque ce nom com- mence par celui de la classe contenante. Si vous choisissez, par exemple, un nom explicite tel que Glob pour la classe destinee a accueillir vos va- riables « 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 73 . 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'autee qu'un simple conteneur. Aucun objet ne sera instancie a partir de cette classe, laquelle ne comporte d'ailleurs aucune methode. Nos va- riables « 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'atteibuts de Glob(). Le nom de la base de donnees, par exemple, pourra etre reteouve partout dans la variable Glob.dbName ; le nom ou l'adresse IP du ser- veur dans la variable Glob.host, etc. : 1# class Glob (object) : 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" ) , 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 stocker 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. 244 Apprendre a programmer avec Python 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 ceuvre 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'automati- ser 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. 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 e'est cette derniere formule qui est retenue : nous avons sauvegarde la classe Glob() dans un module nomme diet 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 a pro- fit ce que nous avons appris dans les chapitres precedents, et done privilegier la programmation par ob- jets, 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 ma- niere similaire, nous ouvrirons la communication avec la base de donnees en commencant par creer un objet-interface a l'aide de la classe GestionBDO, 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 effec- tuerons nos operations sur la base de donnees par l'intermediaire des diverses methodes de l'objet-in- terface. 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# "Etablissement 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: 1 6. Gestion d'une base de donnees 245 l^ff print ' La connexion avec la base de donnees a echoue : \n ' \ J. Off ' Erreur de tec tee : \n%s ' % err 1 A U 14ff seir .ecnec — i J-Off else : 1 _# loff self . cursor = self . baseDonn . cursor ( ) # creation du curseur 1 74 J- / ff self . echec = 0 J-Off aer creerTables ( self , dicTables) : 9f)4 _Uff tlcation a€5 taUleS UcLlltco uans le UlCtlOillldlXc \Q1C1 di-)X tio^ . 914 ^lff for table in dicTables: # parcours des cles du diet. __ff Xfcrq — 1«£vEiaXIL lADLH oa y 15 twlc 9*34 ^ Off pJc - _flff 1UI UcSCl XII QlLldDlcb [_ T_ciiJ 1 tr J . ^off nomChamp = descr[0] # libelle du champ a creer 9fi4 -Off ten = descr[l] $ ^YP® o*® champ a creer 974 ^ / ff li T_.cn 1 9R4 -Off type^naiTip — xjn -uloilix 9Q4 _»ff eiii i_.cn — jt OUff $ champ ' cle primaire 1 (incremente automatiquement) "}1 4 O-Lff X-ypCwIleAIlip — X1N 1 XL\jIL£\ AUIU X_N^_\Ilil v i__i_N J. ■^94 o_ff pic — nomChamp OOff else : •344 o^ff T_.ype*wnain,p — v_^r\L-rii-.r\ \ ) T_.cn OOff req — req + ^s %s, % (nomunamp, typeunamp) OOff 11 pK O /ff req — recjL . + ^ ^R4 OOff else c o»ff req — req + uunoIKAIIni -es pK fkimaky Jxiiix \^s) ) % (pjc, pK) 4D4 o "6 T_C±IJ _L «Off self . executerReq (req) 474 fl /ff self . commit ( ) tt transfert disque 4R4 fiOff A Q # 49ff J — 4T executerReq ( self , req) : C A 11 OUff "Execution de la requete , avec detection d' erreur eventuelle" 51# try: C.94 0_ff self . cursor . execute (req) R^4 OOff z~\ /— i y~\ 4— T7 1 - - ■ 4— _ /-\ »^ - cALcpi, HiXLcpLlull , ell . S.44 Oflff fr dlllLIlcI la XfcrqutrT_.tr eX> Xfcr uleSSa^c a cl IcUl systculc cell OOff print "Requete SQL incorrecte : \n%s\nErreur detectee : \n%s"\ OOff % (req, err) C7# 0 /ff return 0 C Q 11 OOff else . 09ff return 1 OUff c 1 11 olff J — 4T aer resultatReq (self ) : o^ff "renvoie le resultat de la requete precedente (un tuple de tuples) " 63# return self . cursor . f etchall ( ) 64# 65# def commit (self) : 66# if self .baseDonn : 67# self .baseDonn. commit () # transfert curseur -> disque 68# 69# def close (self) : 70# if self .baseDonn : 71# self . baseDonn . close ( ) Commentaires • Lignes 1-2 : Outre notre propre module dict_app qui contient les variables « globales », nous impor- tons 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. 246 Apprendre a programmer avec Python • 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 numero 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 99), 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 argu- ment. 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 me- thode a la creation de champs des types integer et varchar. Libre a vous d'ajouter 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 trans- mise a la methode executerReqO pour execution. Si vous souhaitez visualiser la requete ainsi construite, vous pouvez evidemment ajouter une instruction print req juste apres la ligne 40. Vous pouvez egalement ajouter a cette methode la capacite de mettre en place les contraintes d'in- tegrite 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 l'acces 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.connectO, et l'objet curseur correspondant. Elles permettent de simplifier legerement le code du programme appelant. 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'im- porte quelle table, en construisant automatiquement les instructions d'entree adequates grace aux infor- mations tirees du dictionnaire d'application. Dans une application veritable, ce formulaire trop simpliste devrait certainement etre fortement rema- nie, et il prendrait vraisemblablement la forme d'une fenetre specialises, dans laquelle les champs d'en- tree et leurs libelles pourraient encore une fois etre generes de maniere automatique. Nous ne preten- 1 6. Gestion d'une base de donnees 247 dons 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 formu- laires 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 chague 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 (0/N) ? ") 29# if ch. upper () == "0" : 30# return 0 31# else: 32# return 1 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 entrerO 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 creer- TablesO de la classe GestionBDO decrite a la rubrique precedente. Vous pourriez bien entendu ajouter 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 1 3 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 construc- tion. 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 assem- bled et executee. Si vous souhaitez visualiser cette requete, vous pouvez bien evidemment ajouter une instruction print req juste apres la ligne 25. 248 Apprendre a programmer avec Python Le corps de l'application II ne nous parait pas utile de developper davantage encore cet exercice dans le cadre d'un manuel d'ini- tiation. 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 ! 1# ###### 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"\ 11# "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: 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) 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. resultatReqO # 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. resultatReqO # 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. 1 6. Gestion d'une base de donnees 249 • 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-28 : La classe EnregistreurO 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 entrerO de l'objet-enregistreur renvoie une valeur 0 ou 1 suivant que l'utilisateur a choisi de continuer a entrer des enregistrements, ou bien d'arreter. Le test de cette va- leur permet d'interrompre la boucle de repetition en consequence. • Lignes 35-44 : La methode executerReqO renvoie une valeur 0 ou 1 suivant que la requete a 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. 17 Applications web Vous ave% certainement dejd appris par ailleurs un grand nombre de choses concernant la redaction de pages web. Vous save% que ces pages sont des documents au format HTML, que I'on peut consulter via un reseau (in- tranet ou Internet) a I'aide d'un logiciel appele browser web ou navigateur (Netscape, Konqueror, Internet ex- plorer...). 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 I'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 I'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 sa- voir 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'in- formation : essentiellement les adresses URL des pages que I'intemaute desire consulter. 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 reserva- tion 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 ces cas la, vous vous doutez bien que 1'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 infor- mation 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 compe- tences de programmeur Python pour ajouter une telle interactivite a un site web, en y integrant de veri- tables applications. Remarque importante Ce que nous allons expliquer dans les paragraphes qui suivent sera directement fonctionnel sur /'intranet de votre etablissement scolaire ou de votre entreprise (a la condition toutefois que I'administrateur de cet intranet ait configure son serveur de maniere appropriee). En ce qui concerne /'Internet, par contre, les choses sont un peu plus compliquees. II va de soi que I'installation de logiciels sur un ordinateur serveur 252 Apprendre a programmer avec Python relie a /'Internet ne peut se faire qu'avec I'accord de son proprietaire. Si un fournisseur d'acces a I'lnternet a mis a votre disposition un certain espace ou vous etes autorise a installer des pages web « statiques » (c'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. L'interface CGI L'interface CGI (Common Gateway Interface) est un composant de la plupart des logiciels serveurs de pages web. U 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 na- vigation. 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'ini- tiation, 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, Django ou Zope, pour le programmeur desireux de developper un site web in- teractif. Une interaction CGI rudimentaire Pour la bonne comprehension de ce qui suit, nous supposerons que l'administrateur reseau de votre etablissement 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 constitue 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 ensuite. 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) : l# 2# Exercice avec Python 3# 4# 5#

6# 7#

Page Web interactive

8#

Cette page est associee a un script Python

9# 10# M ACTION="http: //Serveur/cgi-bin/input_guery .py" METHOD="post"> 11# 12# 13# 14# • Vous savez certainement deja que les balises initiales , , , <BODY>, ainsi que les balises finales correspondantes, sont communes a tous les documents HTML. Nous ne de- taillerons done pas leur role ici. 1 7. Applications web 253 • La balise <DIV> 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 niveau 2. • La ligne 8 est un paragraphe ordinaire. • Les lignes 10 a 12 contiennent le code important (pour ce qui nous occupe ici). Les balises <FORM> et </form> 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 : 1'action 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 reper- toire 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 administrates vous indiquera 74 . La ligne 11 contient la balise qui definit un widget de type « bouton d'envoi » (balise <input TYPE="submit">). 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 ins- taller ce script dans le meme repertoire que votre document HTML initial : 1# #! /usr /bin/python 2# 3# # Affichage d'un formulaire HTML simplifie : 4# print "Content-Type: text/html \n" 5# print """ 6# <H3XFONT COLOR=" Royal blue"> 7# Page web produite par un script Python 8# </FONTX/H3> 9# 10# <FORM ACTION="print_result.py" METHOD="post"> 11# <P>Veuillez entrer votre nom dans le champ ci-dessous, s.v.p. :</P> 12# <PXINPOT NAME="visiteur" SIZE=20 MAXLENGTH=20 TYPE="text"X/P> 13# <P>Veuillez egalement me fournir une phrase quelconque :</P> 14# <TEXTAREA NAME="phrase" ROWS=2 COLS=50>Mississippi</TEXTAREA> 15# <P>J ' utiliserai cette phrase pour etablir un histogramme .</P> 16# <INPUT TYPE="submit" NAME="send" VALUE="Action"> 17# </FORM> 18# """ Par exemple : http://192. 168.0. 100/cgi/Classe6A/Dupont/input_query.py 254 Apprendre a programmer avec Python 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'inter- preteur 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 naviga- teur de l'internaute, et qui 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. Un formulaire HTML pour l'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 entrer son nom. Le parametre MAXLENGTH definit une lon- gueur 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 textarea). II s'agit d'un receptacle plus vaste, destine a accueillir des textes de plusieurs lignes. Ce champ est automatique- ment 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 1 6 contient la definition du bouton qu'il faudra action- ner pour transmettre les donnees au script CGI destinataire, lequel est decrit ci-apres. 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 for- mulaire HTML est fort simple, comme vous pouvez 1' 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 f orm. 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# 1 7. Applications web 255 17# print "Content-Type: text/html \n" 18# print """ 19# <H3>Merci, %s !</H3> 20# <H4>La phrase que vous m'avez fournie etait : </H4> 21# <H3XF0NT Color="red"> %s </F0NTX/H3>" " " % (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 "<H4>Frequence de chaque caractere dans la phrase :</H4>" 30# for c, f in liste: 31# print ' le caractere <B>"%s"</B> apparait %s fois <BR>' % (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 , la- quelle permet de dialoguer avec le serveur web. • A la ligne 5, la fonction FieldStorageO 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 fau- dra lui appliquer la methode valued 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 FieldStorageO est qu'il ne possedera aucune cle pour les champs kisses 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 de la presence de chacun des index atten- dus, et e'est ce que nous faisons aux lignes 7 a 15. Exercice 17.1 Pour verifier ce qui precede, vous pouvez par exemple desactiver (en les transformant en com- mentaires) les lignes 7, 9, 10, 12, 14 et 15 du script. Si vous testez le fonctionnement de l'en- semble, 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 pro- duit. Note importante Le script etant lance par I'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 (log) du serveur web. Veuillez consulter I'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 et 15. 256 Apprendre a programmer avec Python • Aux lignes 23, 24 et 25, nous nous servons d'un dictionnaire pour construire un histogramme simple, comme nous l'avons explique a la page 133. • 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 31 se passe de commentaires. 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 utilisa- tion 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 ceuvre du langage ont seduit de nom- breux developpeurs de talent, qui ont realise des outils de developpement web de tres haut niveau. Plu- sieurs 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 colla- boratif, 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, spe- cialistes de bases de donnees, etc.). Le plus celebre de ces produits est le logiciel Zope, deja adopte par de grands organismes prives et pu- blics 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 adminis- trer 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 plu- part d'entre eux peuvent etre telecharges librement depuis 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 Mac Os. 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 la plupart d'entre eux integrent leur propre serveur web, ce qui leur permet de fonctionner egalement de maniere tout a fait autonome. Cette possibilite se revele particulie- rement 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 ceuvre 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 appli- cation Python qui soit accessible par l'intermediaire d'un simple navigateur web, via un intranet d'entre- prise (ou meme via l'lnternet, mais dans ce cas il est recommande de les faire travailler en conjonction avec Apache ou Xitami), ces applications sont faites pour vous. 1 7. Applications web 257 II en existe une grande variete : Django, Turbogears, Pylons, 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 Kar- rigell. Vous pouvez trouver ce systeme a l'adresse : http://karrigell.sourceforge.net. II s'agit d'une solu- tion de developpement web simple, fort bien documentee 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 »). Installation de Karrigell L'installation de Karrigell est un jeu d'enfant : il vous suffit d'extraire dans un repertoire quelconque le ficbier archive que vous aurez telecharge depuis l'lnternet. L' operation de desarchivage cree automati- quement un sous-repertoire nomme Karrigell-numero de version. C'est ce repertoire que nous conside- rerons comme repertoire racine dans les lignes qui suivent. Si vous ne comptez pas utiliser le serveur de bases de donnees Gadfly 75 qui vous est fourni en comple- ment de Karrigell lui-meme, c'est tout ! Sinon, entrez dans le sous-repertoire gadf ly-l .0.0 et lancez la commande : python setup. py install (sous Linux, il faut etre root). Vous devez effectuer cette ope- ration si vous souhaitez visualiser la totalite de la demonstration integree. 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 de- marrer, il est cependant conseille de jeter un petit coup d'ceil 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 numero 80. Et c'est bien ce numero de port 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 nu- meros 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. Pour ce faire, vous devez reperer la section [Server] dans Karrigell.ini (aux alentours de la ligne 75), et y activer une ligne d'instruction telle que port=8080 , ce qui activera l'utilisation du numero de port 8080. Plus tard, vous souhaiterez peut-etre encore modifier le fichier de configuration afin de modifier l'emplace- ment du repertoire racine pour votre site web (par defaut, c'est le sous-repertoire webapps ; voyez pour ce faire la section [Directories]). 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 ser- veur, 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 navi- gateur 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 76 , vous atteindrez la page d'accueil d'un site de de- monstration de Karrigell, qui est deja pre-installe dans le repertoire racine. Vous y retrouverez la docu- mentation de base, ainsi que toute une serie d'exemples. Voyez le chapitre precedent : Gadfly est un serveur de bases de donnees ecrit en Python. 258 Apprendre a programmer avec Python 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 certains 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). Ebauche de site web Essayons a present de realiser notre propre ebauche de site web. A la difference d'un serveur web clas- sique, 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 sous-repertoire webapps, ou mieux encore, dans un sous-repertoire de celui-ci, auquel vous donnerez un nom de votre choix, comme il est d'usage de le faire lorsque Ton cherche a structurer convenablement le site en construction. II vous suffira dans ce cas d'inclure le nom de ces sous-reper- toires dans les adresses correspondantes 77 . Par exemple, creez dans webapps un sous-repertoire essais, puis enregistrez-y un petit script d'une seule ligne comportant une instruction print, tel que : print "<h2>Bienvenue dans ma premiere application web !</h2>" Si vous donnez a ce petit script le nom de hello. py, il sera done sauvegarde dans : ~/Karrigell-numero_de_version/webapps/essais/hello .py Si le serveur Karrigell fonctionne, il vous suffit alors d'entrer une adresse telle que : http: //localhost: 8080/essais/hello.py, ou encore : http: //localhost: 8080/essais/hello (l'extension .py peut etre omise) dans votre navigateur web. Vous devriez y voir apparaitre un message. Cet exemple vous montre 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 ser- veur. 76 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 numero 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 nomine 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). 77 Comme nous l'avons signale a la page precedente, il est possible de changer les options par defaut de Karrigell en editant le fichier Karrigell.ini. Vous pouvez notamment choisir un autre repertoire par defaut pour les applications que vous creez, en modifiant la section [Directories]. 17. Applications web 259 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 affi- cher un petit tableau de 2 lignes et 3 colonnes, avec les instructions suivantes : print """ <TABLE BORDER= " 1 " CELLPADDING="5"> <TR> <TD> Rouge </TD> <TD> Vert </TD> <TD> Bleu </TD> </TR> <TR> <TD> 15 % </TD> <TD> 62 % </TD> <TD> 23 % </TD> </TR> </TABLE> II II II Rappelons que la balise TABLE definit un tableau. Son option BORDER specifie la largeur des bordures de separation, et CELLPADDING 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 : 1# from math import sin, cos, tan, pi 2# 3# # Construction de l'en-tete du tableau avec les titres de colonnes : 4# print " " "<TABLE BORDER="l" CELLPADDING="5"> 5# <TRXTD>Angle</TDXTD>Sinus</TDXTD>Cosinus</TDXTD>Tangente</TDX/TR>" " " 6# 7# for angle in range (0 , 62 , 10) : 8# # conversion des degres en radians : 9# aRad = angle * pi / 180 10# # construction d'une ligne de tableau, en exploitant le formatage des 11# # chaines de caracteres pour fignoler l'affichage : 12# print "<TRXTD>%s</TDXTD>%8 . 7f</TDXTD>%8 . 7f</TDXTD>%8 . 7g</TDX/TR>"\ 13# % (angle, sin (aRad), cos (aRad) , tan (aRad)) 14# 15# print "</TABLE>" • Ligne 7 : Nous nous servons de la fonction ranged pour definir la gamme d'angles a couvrir (de zero a 60 degres par pas de 10). • Ligne 9 : Les fonctions trigonometriques de Python ne- cessitent que les angles soient exprimes en radians. II faut done effectuer une conversion. • Ligne 12 : Chaque ligne du tableau comporte quatre va- leurs, lesquelles sont mises en forme a l'aide du systeme de formatage des chaines de caracteres decrit deja a la page 117 : le marqueur de conversion « %8.7f » force un affichage a 8 chiffres, dont 7 apres la « virgule » de- cimale. Le marqueur « %8 . 7g » fait a peu pres la meme chose, mais passe a la notation scientifique lorsque e'est necessaire. A ce stade, vous vous demandez peut-etre ou se situe la dif- ference entre ce que nous venons d'experimenter ici et un script CGI classique (tels ceux des pages 253 et suivantes). L'interet de travailler dans un environnement plus speci- fique tel que Karrigell apparait cependant tres vite si vous faites des erreurs. En programmation CGI classique, les messages d'erreur emis par l'interpreteur Py- thon ne s'affichent pas dans la fenetre du navigateur. lis sont enregistres dans un fichier journal du ser- veur (Apache, par exemple), ce qui ne facilite pas leur consultation. J ^ _i» File Edit View Go Bookmarks - & © . -iiici. ♦ Getting Started Angle Sinus Cosinus Tangente 0 0.0000000 1.0000000 0 1 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.191754 60 0.8660254 0.5000000 1.732051 Done 260 Apprendre a programmer avec Python En revanche, avec un outil comme Karrigell, vous disposez d'une signalisation tees efficace, ainsi que d'un outil de deboguage complet. Faites l'experience d'introduire une petite erreur dans le script ci-des- sus, et relancez votee 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 • ; - m © [□ http://localhost:8080/trigH (IS- ♦ Getting Started B Latest Headlines Error in /trigono2.py Sen pt / tri gono2 . py SyntaxError Li ne 7 for angle in range (S . 91 , 10) Traceback (most recent call last) : File "/wi ndows/D/dos_data/py thon/Karrigell-2 , 0. 5/Template . py " . line 186. in render exec pythonCode in ns File "<string>", line 7 for angle in range(6.91 .10) A SyntaxError: invalid syntax Debug | Done En cliquant sur le bouton « Debug », on obtient encore une foule d 'informations complementaires (af- fichage du script complet, variables d'environnement, etc.). Prise en charge des sessions Lorsqu'on 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 diffe- rentes pages (l'exemple type etant le remplissage d'un Caddie au cours de la consultation d'un site com- mercial), 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 ca- ches (balises <input TYPE="hidden">), mais ce serait complique et tees contraignant. II est preferable que le serveur soit dote d'un mecanisme specifique, qui attribue a chaque client une session particuliere. Karrigell realise cet objectif par l'intermediaire de cookies. Lorsqu'un nouveau visiteur du site s'identifie, le serveur genere un cookie (e'est-a-dire un petit paquet d'informations) 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 par- court les autees pages du site, son navigateur renvoie a chaque fois le contenu du cookie au serveur, ce qui permet a celui-ci de l'identifier et de retrouver l'objet-session qui lui correspond. 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!) : 1 7. Applications web 261 oSess = Session () Si vous etes au debut de la session, Karrigell genere un identifiant unique (une chaine de caracteres pseudo-aleatoire), le place dans un cookie et envoie celui-ci au navigateur web. II memorise le meme identifiant dans l'objet-session. A partir de ce moment, vous pouvez ajouter un nombre quelconque d'attributs a cet objet-session : oSess.nom = "Jean Dupont" oSess.age = 17 . . . etc . . . Dans les autres pages , vous procedez de la meme maniere, tout en sachant que l'instruction : oSess = Session () ne produit plus cette fois un nouvel objet, mais restitue au contraire l'objet-session correspondant a l'utilisateur en ligne. En effet : lorsqu'il demande l'acces a ces autres pages, le navigateur de l'utilisateur envoie au serveur son identifiant de session (via le cookie). Cet identifiant, deja connu du serveur, lui permet de retrouver l'objet-session correspondant, cree en debut de session. Tout ceci se passe de ma- niere quasi-transparente pour vous, le programmeur : il vous suffit de savoir que toutes les informations concernant l'utilisateur en ligne sont pour vous de simples attributs de l'objet-session. Vous pouvez done aisement acceder aux valeurs de ces attributs, et aussi en ajouter de nouveaux : oSess = Session () # recuperer l'objet-session indique par le cookie om = oSess . nom # retrouver la valeur d'un attribut existant oSess . article = 49137 # ajouter un nouvel attribut Les objets-sessions prennent aussi en charge une methode close{), qui a pour effet d'effacer l'informa- tion 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 1 000 e . Exemple de mise en oeuvre Sauvegardez les trois petits 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 : 1# # Affichage d'un formulaire d' inscription : 2# 3# print """ 4# <H3>Veuillez vous identifier, SVP :</H3> 5# 6# <FORM ACTION = "sessionTest2 .py"> 7# Votre nom : <INPUT NAME = "nomClient"> <BR> 8# Votre prenom : <INPUT NAME = "prenomClient"> <BR> 9# Votre sexe (m/f) : <INPOT NAME = "sexeClient" SIZE ="1"> <BR> 10# <INPUT TYPE = "submit" VALUE = "OK"> 11# </FORM>""" Le suivant sera nomme sessionTest2.py. C'est le script mentionne dans la balise d'ouverture du formu- laire 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 peut retrouver les valeurs entrees par l'utilisateur dans les differents champs du for- mulaire, par l'intermediaire d'un dictionnaire de requete situe dans la variable d'environnement QUERY de Karrigell 78 : 1# obSess = Session () 2# 3# obSess.nom = QUERY [ "nomClient" ] 4# obSess .prenom = QUERY [ "prenomClient" ] ^^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 FieldStorage() du module cgi. Veuillez consulter la documentation de Karrigell si vous souhaitez obtenir des explications plus detaillees. 262 Apprendre a programmer avec Python Off n kc aQQ cava — nTTTTRY T " cflvarl i onf " 1 6# 7# if obSess . sexe . upper ( ) == "M" : 8# vedette ="Monsieur" 9# else : 10# vedette ="Madame" 11# print "<H3> Bienvenue, %s %s </H3>" % (vedette , obSess . nom) 12# print "<HR>" 13# print """ 14# <a href = "sessionTest3 .py"> Suite.. .</a>" " " • 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 et 5, on recupere les valeurs entrees dans les champs du formulaire precedent, en utilisant leurs noms comme cles d'acces au dictionnaire de requetes. Ce dictionnaire est cree et gere automatiquement par Karrigell. • 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 a j outer des attributs 3# suiviSess . prix = 43.67 4# 5# print """ 6# <H3> Page suivante </H3> <HR> 7# Suivi de la commande du client : <BR> %s %s <BR> 8# Article n° %s, Prix : %s <HR> 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 : File Edit View Go Bookmarks Toe + © © fD~0(5~~ ♦ Getting Started B Latest Headlines Veuillez vous identifier, SVP : Votre nom : |Dupont Votre prenom : [Charles Votre sexe (ru/f) : [m OK | Done File Edit View Go Bookmarks loc ♦Getting Started & Latest Headlines Bienveuue, Monsieur Dupont Suite... Done Comme attendu, les informations entrees dans le formulaire sont transmises a la deuxieme page. A pre- sent, 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 directe de donnees (puis- qu'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. 1 7. Applications web 263 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 recuperer l'objet-session cree dans le script precedent. File Edit View Go Bookmarks " <? - ; - & © f iry n ♦ Getting Started S Latest Headlines Page suivaute 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 partir de la classe SessionO est l'objet-session regenere. II contient les informations sauvegardees a la page prece- dente, et on peut lui en ajouter d'autres dans des attributs supplementary s. 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 rutilisateur 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 generera celle-ci. Les informations devront cette fois etre affichees dans un tableau : Norn Prenom Sexe Article Prix Dupont Charles m 12345 43.67 Durand Louise f 6789 12.75 etc . 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 (disponible en francais) et les exemples fournis avec le produit. Comme nous l'avons deja signale plus haut, rinstallation de Karrigell inclut 1'installation du systeme de bases de donnees Gadfly. Vous pouvez done tres rapidement et tres aisement realiser un site interactif permet- tant 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 de- vienne pas gigantesque. Si vous cherchez a realiser un site web tres ambitieux, prenez egalement la peine d'etudier d'autres offres logicielles, comme par exemple Django, Pylons, TurboGears, Twisted, CherryPy, Zope... associes a Apache pour le systeme serveur, et SQLite, MySQL ou PostgreSQL pour le gestionnaire de bases de don- nees. Vous aurez compris qu'il s'agit d'un domaine tres vaste, ou vous pourrez exercer votre creativite fort longtemps. . . 18 Communications a travers un reseau Le developpement extraordinaire de I'lnternet 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'inter- connexion de deux programmes, qui leur permette de s'echanger des informations basiques (donnees numeriques, chaines de caracteres...) par I'intermediaire d'un reseau. Pour ce qui va suivre, nous supposerons done que vous collabore^ 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 sy steme d'exploitation n'a pas d'importance : vous pouve^par exemple installer I'un des scripts Python de'erits ci-apres sur un poste de travail fonctionnant sous Linux, et le /aire dialoguer avec un autre script mis en auvre sur un poste de travail confie aux bons soins d'un systeme d'exploitation different, tel que Mac OS ou Windows. Vous pouve^ e'galement experimenter ce qui suit sur une seule et meme machine, en mettant les differents scripts en auvre dans des fenetres independantes. Les sockets Le premier exercice qui va vous etre propose consistera a etablir une communication entre deux ma- chines seulement. L'une et l'autre pourront s'echanger des messages a tour de role, mais vous constate- rez cependant que leurs configurations ne sont pas symetriques. Le script installe sur l'une de ces ma- chines 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 79 . II guette en permanence l'arrivee de requetes expediees par les clients potentiels en direction de cette adresse, par I'intermediaire d'un port de communication bien de- termine. Pour ce faire, le script correspondant doit mettre en ceuvre un objet logiciel associe a ce port, que Ton appelle un socket. Depuis une autre machine, le logiciel client tente d'etablir la connexion en emettant une requete appro- priee. 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 79 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 un ouvrage sur les reseaux pour en savoir davantage. 266 Apprendre a programmer avec Python 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 ma- chines, comme si on les avait connectees l'une a Pautre par 1'intermediaire d'un fil (les deux ports de communication respectifs jouant le role des deux extremites de ce fil). L'echange d'informations pro- prement 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 1'intermediaire d'objets interfaces que Ton ap- pelle done des sockets. Ceux-ci peuvent mettre en ceuvre deux techniques de communication diffe- rentes 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. 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 inter- connectes par 1'intermediaire d'un reseau local. C'est une technique particulierement aisee a mettre en ceuvre, et elle permet un debit eleve pour l'echange de donnees. L'autre technologie (celle des paquets) serait preferable pour les communications expedites via l'inter- net, en raison de sa plus grande fiabilite (les memes paquets peuvent atteindre leur destination par diffe- rents 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 livre. Le premier 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 ajouter 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 en tamer 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 l'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) Etablissement de la connexion : 25# connexion, adresse = mySocket. accept () 26# print "Client connecte, adresse IP %s, port %s" % (adresse[0], adresse[l]) 27# 28# # 5) Dialogue avec le client : 18. Communications d trovers un reseau 267 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> ") 3 6# 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 ( "<R>ecommencer <T>erminer ? ") 45# if ch. upper () =='T': 46# break Commentaires • Ligne 4 : Le module socket contient toutes les fonctions et les classes necessaires pour construire des programmes communicants. Comme nous allons le voir dans les lignes suivantes, l'etablisse- ment 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 deci- male 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 supe- rieure a 1024. • Lignes 9-10 : Premiere etape du mecanisme d'interconnexion. On instancie un objet de la classe so- cket(), 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 communi- cation. 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 pre- sent 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 accepto, le socket attend inde- finiment 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 inputo pour attendre une entree clavier. Si une requete est receptionnee, la methode accepto renvoie un tuple de deux elements : le premier est la reference d'un nouvel objet de la classe socketo 80 , qui sera la veritable interface de communication entre le 80 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 l'on laissera fonctionner en 268 Apprendre a programmer avec Python 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 recvQ 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, ils sont transmis lorsque la meme methode recv() est appelee a nouveau. • 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 cha- cune revolution de ce dialogue. • Lignes 39 a 42 : Sixieme etape. Fermeture de la connexion. Construction d'un client rudimentaire Le script ci-dessous definit un logiciel client complementaire du serveur decrit dans les pages prece- dentes. 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. permanence pour receptionner les requetes qui continuent a arriver en provenance de nouveaux clients. 18. Communications d trovers un reseau 269 • Lignes 12 a 18 : On ne cree cette fois qu'un seul objet socket, dont on utilise la methode connectO pour envoyer la requete de connexion. • Lignes 20 a 33 : Une fois la connexion etablie, on peut dialoguer avec le serveur en utilisant les me- thodes send() et recv() deja decrites plus haut pour celui-ci. Gestion de plusieurs taches en parallele a l'aide des threads Le systeme de communication que nous avons elabore dans les pages precedentes est vraiment tres ru- dimentaire : d'une part il ne met en relation que deux machines, et d'autre part il limite la liberte d'ex- pression 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 destructions rencontre une fonction inputo, par exemple, il ne se passe plus rien tant que 1'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 entre-temps sur votre ordinateur, car les systemes d'exploi- tation modernes sont multi-taches. Les pages qui suivent sont destinees a vous expliquer comment vous pouvez introduire cette fonction- nalite multi-tache 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 precedente. 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 rawjnputo) ; • 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 simulta- nees. 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 mes- sages, sans Pobligation 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 pa- rallele 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 neces- saire 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 ceuvre 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* 1 . 81 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 l'aide d'un meme script Python (operation fork), mais l'explication de cette technique depasse largement le cadre de ce livre. 270 Apprendre a programmer avec Python Dans un programme d'ordinateur, les threads sont des flux destructions qui sont menes en parallele (quasi- simultanement), tout en partageant le meme espace de noms global. En fait, le flux destructions 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, 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. Client gerant remission et la reception simultanees Nous allons maintenant mettre en pratique la technique des threads pour construire un systeme de chat s2 simplifie. Ce systeme sera constitue 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 pre- sent les instructions de creation de deux objets threads (aux lignes 49 et 50), dont on demarre la fonc- tionnalite aux deux lignes suivantes. Ces objets threads sont crees par derivation, a partir de la classe ThreadO 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 <reception> se termine ici . 22# # On force la fermeture du thread <emission> : 23# th_E._Thread stop() 24# print "Client arrete . Connexion interrompue . " 25# self . connexion . close () 26# 27# class ThreadEmission (threading . Thread) : S2 Le « chat » est l'occupation qui consiste a « papoter » par l'intermediaire d'ordinateurs. Les canadiens francophones ont propose le ternie de clavardage pour designer ce « bavardage par claviers interposes ». 18. Communications d trovers un reseau 271 ^off objet thread gerant 1' emission des messages""" ^»ff def init (self, conn) : JUff threading . Thread . init (self) Jiff self . connexion = conn # ref. du socket de connexion J^ff JJff def run (self) : J^ff while 1 : ■331 message emis = raw_input() "3 (T « self . connexion . send (message emis) Q »7 Ji J /ff O Q W Joff # Programme principal - Etablissement de la connexion : J3tt connexion = socket . socket (socket .AF INET, socket. SOCK STREAM) fi Uff try : connexion . connect ( (host, port)) except socket. error : 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 = ThreadEmission (connexion) 50# th R = ThreadReception (connexion) 51# th _E. start () 52# th _R. start () Commentaires • Remarque generale : Dans cet exemple, nous avons decide de creer deux objets threads indepen- dants 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 au- cun interet a limiter le nombre de threads. Au contraire : a partir du moment ou Ton decide d'utili- ser 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 ThreadO, 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 ThreadO 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 d'une 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'instruc- tions 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 pro- gramme se termine lui aussi. II nous faut done forcer la fermeture de l'autre objet thread, celui que 272 Apprendre a programmer avec Python nous avons mis en place pour gerer remission des messages. Cette fermeture forcee peut etre obte- nue a 1'aide de la methode Thread_stop() 83 . • Lignes 27 a 36 : Cette classe definit done un autre objet thread, qui contient cette fois une boucle de repetition perpetuelle. II ne pourra done se terminer que contraint et force par la methode de- crite au paragraphe precedent. A chaque iteration de cette boucle, le flux destructions s'inter- rompt 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 : Instantiation et demarrage des deux objets threads enfants. Veuillez noter qu'il est recommande de provoquer ce demarrage en invoquant la methode integree starto, plutot qu'en fai- sant 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). Serveur gerant les connexions de plusieurs clients en parallele 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# ' ' '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# "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 XVI). 18. Communications d trovers un reseau 273 # Fermeture de la connexion : JUff self .connexion. close () # couper la connexion cote serveur •31 £ ■3 J-ff del conn client [nom] # supprimer son entree dans le dictionnaire print "Client %s deconnecte . " % nom JJff # Le thread se termine ici # Initialisation du serveur - Mise en place du socket : JUff mySocket = socket. socket (socket. AF INET, socket. SOCK STREAM) J lit try: O Q # mySocket. bind ( (HOST, PORT)) except socket. error : fiUff print "La liaison du socket a l'adresse choisie a echoue." fl-Lff sys.exit() print "Serveur pret, en attente de requetes ..." 4Jff mySocket. listen (5) A A # # Attente et prise en charge des connexions demandees par les clients : conn client = { } # dictionnaire des connexions clients wmie i . connexion, adresse = mySocket. accept () # Creer un nouvel objet thread pour gerer la connexion : 50# th = ThreadClient (connexion) 51# th. start () 52# # Memoriser la connexion dans le dictionnaire : 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[l]) 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 rai- sons : la premiere est que nous devrons pouvoir ajouter 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 aisement d'un identifiant unique pour chaque connexion, lequel pourra servir de cle d'acces dans un dictionnaire. Cet identifiant nous sera en effet fourni automatique- ment par la classe ThreadO. • Lignes 47 a 51 : Le programme commence ici une boucle de repetition perpetuelle, qui va constam- ment attendre l'arrivee de nouvelles connexions. Pour chacune de celles-ci, un nouvel objet ThreadClientO 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 ThreadClientO que de connexions, et que tous ces objets fonctionneront en parallele. La methode getNameO peut alors etre utilisee au sein d'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 par- ticulier. II faut done pour cela une boucle de repetition perpetuelle, qui ne s'interrompra qu'a la re- 274 Apprendre a programmer avec Python ception 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 utili- sons 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 d'ou il provient. • Ligne 31 : Lorsque nous fermons un socket de connexion, il est preferable de supprimer sa refe- rence 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). Jeu des bombardes, version reseau IClient Thread- a connecte, adresse IP 192. 168. U .235, port 3493S. Client Thread-4 connecte, adre3Se IP 192.168.0.235, port 34936. Client Thread-5 connecte, adresse IP 192 168 0.235, port 34937. [client 'rhread-6 connecte, adresse IP 192. 168. 0 235, port 34938. I— 1 Client Thread-7 connecte, adresse IP 192.168.0.235, port 34939. / EThread-3 ■ Threail-5 points 84 I points □ a H I Feu i | points points 4 B Thread-4 Fe,i! i points 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 tech- niques 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 (Mac OS <> Linux <> Windows !). 18. Communications d trovers un reseau 275 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 ca- non03.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 judicieusement de l'importation et de l'heritage de classes. Du module canon04, nous allons reutiliser la classe CanonO telle quelle, aussi bien pour le logiciel serveur que pour le logiciel client. De ce meme module, nous importerons egalement la classe AppBombardesO, dont nous ferons deriver la classe maitresse de notre application serveur : AppServeurO. Vous constate- rez plus loin que celle-ci produira elle-meme la sous-classe AppClientO, toujours par heritage. Du module canon03, nous recupererons la classe PupitreO 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 ThreadClientsO, dont une instance surveillera en permanence le socket desti- ne a receptionner les demandes de connexion de nouveaux clients, et la classe ThreadConnexionO, 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 Papplication 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 developpe- ment, 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. 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 efficaces ou plus simples. Vous savez deja que les messages echanges sont de simples chaines de caracteres. Prevoyant que cer- tains de ces messages devront transmettre plusieurs informations a la fois, nous avons decide que cha- cun d'eux pourrait comporter plusieurs champs, que nous separerons a l'aide de virgules. Lors de la re- ception de l'un quelconque de ces messages, nous pourrons alors aisement recuperer tous ses compo- sants 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 entre aste- risques sont ceux qui sont recus du serveur ; les autres sont ceux qui sont emis par le client lui-meme : 276 Apprendre a programmer avec Python 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,* 15# *mouvement_de,Thread-2 ,407,191,* 16# *depart_de,Thread-2* 17# *nouveau_canon, Thread- 5, 502, 276,-1, dark green* • Lorsqu'un nouveau client demarre, il envoie une requete de connexion au serveur, lequel lui expe- die en retour le message : « serveur OK ». A la reception de ce dernier, le client repond alors en en- voyant lui-meme : « client OK ». Ce premier echange de politesses n'est pas absolument indispen- sable, 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 sup- plemental contenant simplement « le_votre » (comparez par exemple la ligne 5 avec la ligne 1 7, 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 1'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 11 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 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. 18. Communications d trovers un reseau 277 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 nou- veau 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 11). Programme serveur : premiere partie Vous trouverez dans les pages qui suivent le script complet du programme serveur. Nous vous le pre- sentons 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 esti- merez 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 semble- ront utiles (par exemple, une proposition de choisir les coordonnees de la machine hote au demarrage, une barre de menus, etc.) : l# ####################################################### 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 l'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 <sc>, 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" 44# self .bTir. config (state =D I SAB LED) 45# self .regl. config (state =DISABLED) 46# 47# def activer (self ) : 278 Apprendre d programmer avec Python 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 construite par derivation de la classe de meme nom importee du module ca- non03. Elle herite done toutes les caracteristiques de celle-ci, mais nous devons surcharger 84 ses me- thodes tirer() et orienter(). • Dans la version monoposte du logiciel, en effet, chacun des pupitres pouvait commander directe- ment 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 desac- tives, mais les indications fournies obeissent aux injonctions qui leur sont adressees par l'applica- tion principale. • Cette nouvelle classe PupitreO sera egalement utilisee telle quelle dans chaque exemplaire du pro- gramme client. Dans la fenetre de celui-ci comme dans celle du serveur, tous les pupitres seront af- fiches comme des repetiteurs, mais l'un d'entre eux cependant sera completement fonctionnel : ce- lui qui correspond au canon du joueur. • Toutes ces raisons expliquent egalement Pappatition des nouvelles methodes activerO, desactiverO, reglageO 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 ThreadConnexionO 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 destructions 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 ceuvre concrete du protocole de communication decrit dans les pages precedentes (cer- tains messages etant cependant generes par les methodes depl_aleat_canon() et goal() de la classe App- ServeurO 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] 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 : 84 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 153). 18. Communications d trovers un reseau 279 774 / 'ff if cli ! — nom : 7R4 / Off message — cicpdi l cat; , ■ss ^ noiii 7Q4 / yff Scii . e±]jp . conn LiiciiL |_ c 11 j . send ^mcssaijc j OUff Sell . c-PP • VcllOU ■ leleaSe \ j R1 4 olff ft x exniei ie pieseni. uiicau R94 o — ff brssk o Off «1 4 4T J-jl- 11-1 4 on 4- rtVII . eiii aetj — cuent. vji\ 04ff signaler au nouveau client les canons deja enregistres ooff msg ="canons , 11 o off f or g in self . app . guns : Q74 O 'ff gun = self . app . guns [g] QQ4 OOff me* /~r — m — — ITIO— , .0, — . CL — .0, — . Q. _ H Q. \ msg — itisg + ^ss , ^s , , ^s , ^ss , ^ \ Q Q# O 3ff ■ mm t ^"1 mm v1 mm ttI *^nn cane mm 1 \ V y un . iu , y un . ai , y un . y i , y un . oeiio f y un . luui/ an4 self . app . verrou . acquire ( ) Q1 4 yiff self . connexion . send (msg) Q94 y— ff ft a^teiiULe un accuse tie ieC@px.icn \ win. ^ ^ Off Sell • LUilllCAlUXl , ICLV \ J.KJ \J ) a A it self . app . verrou . release ( ) QCJl ysff ff a jouter un canon dans 1 1 espace de jeu serveur . <3fift :7utt ft 1 ct iiie uuuuc xii v u^ucc lcii v u.lc xes uaiauL . uu L-anun 01 ee • Q74 y <ff a f y , sens ^ ccui — Sell . app . ajuutci canon \nom/ now yoff ft signaler les caract. de ce nouveau canon a tous les y yff ^ clients de j a connec tes lUUff self . app . verrou . acquire ( ) i ni 4 lUlff for cli in self . app . conn client: i r\ o 4 lU_ff msg — nouveau canon , %s , , %s , , %s % \ i m4 J-U off (nom r x f y r sens , coul ) i n 4 4 lU4ff pour le nouveau client , a j outer un champ indi quant i ns4 lUOff ft que le me s sage ccnceine son piopie canon lUOff if cli == nom : 1U 'ff msg =msg +" , le votre" lUBff self . app . conn client [cli] . send (msg) i n Q4 luyff self . app . verrou . release ( ) HUff em ceo — reu . lllff self . app . tir canon (nom) 1 1 94 ft oignaiei ce uii a tuus les aux.ies clients 1 1 ^4 J. J. off self . app . verrou . acquire ( ) 1 1 44 114ff for cli in self . app . conn client: 1 1 R4 HDff if cli ! = nom : llOff message = "tir de,%s," % nom I 1 74 II 'ff self . app . conn client [cli] . send (message) 1 1 Q4 lloff self . app . verrou . release ( ) 1 1 Q4 ll^ff ell 1 UcU Ullcll Lcl IZUff x. = ms yLiisnt . spilt \ , ) IZlff ft Peut - etre recu plusieurs angles . Utiliser le dernier t self . app . orienter canon (nom, t [ -2 ] ) 1 9*34 1-Off # Signaler ce changement a tous les autre s clients 1 944 lZ4ff self . app . verrou . acquire ( ) 1 9 R4 1-Jff for cli in self . app . conn client: l^Off if cli ! = nom : 1 974 1Z /ff ff virgule a la fin , car messages parf ois groupes i IDA 128ff ________ 1 1 _ _ _, n _ q, — o, _, ii o, / _ _ j_ r — 1 \ message = angle ,%s,.s, % (nom, t [ -_ J ) 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# Synchronisation de threads concurrents a l'aide de verrous (thread locks) Au cours de votre examen du code ci-dessus, vous aurez certainement remarque la structure particu- liere des blocs d'instructions par lesquelles le serveur expedie un meme message a tous ses clients. Considerez par exemple les lignes 74 a 80. 280 Apprendre a programmer avec Python La ligne 75 active la methode acquired d'un objet « verrou » qui a ete cree par le constructeur de l'appli- cation principale (voir plus loin). Cet objet est une instance de la classe Lock(), laquelle fait partie du mo- dule 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 sollici- te, cette fois pour sa methode released. A quoi cet objet-verrou peut-il done bien servir ? Puisqu'il est produit par une classe du module threa- ding, 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. En- suite, 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 par- faitement 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 me- thode acquired 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 released du verrou, ce qui le fera repasser dans l'etat de- verrouille. En effet, si un autre thread concurrent essaie d'activer lui aussi la methode acquired 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 sert. Lorsque le verrou est deverrouille, l'un des threads en attente (il peut en effet y en avoir plusieurs) re- prend alors son activite tout en refermant le verrou, 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 released est invoquee. II faut done toujours veiller a ce que chaque thread qui active la methode acquired du verrou avant d'acceder a une ressource, active egalement sa methode re- leased peu apres. Pour autant que tous les threads concurrents respectent la meme procedure, cette technique simple em- peche 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. 18. Communications d trovers un reseau 281 Programme serveur : suite et fin Les deux classes ci-dessous completent le script serveur. Le code implemente dans la classe ThreadClientsO est assez similaire a celui que nous avions developpe precedemment pour le corps d'ap- plication du logiciel de Chat. Dans le cas present, toutefois, nous le placons dans une classe derivee de ThreadO, parce que devons faire fonctionner ce code dans un thread independant de celui de Implica- tion principale. Celui-ci est en effet deja completement accapare par la boucle mainloopO de l'interface graphique 85 . La classe AppServeurO derive de la classe AppBombardesO 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 signale 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 ca- nevas, 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[l]) 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 1 ' 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 referme la fenetre : 173# self .bind ( '<Destroy>' ,self . f ermer_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) 185# scroll. pack (side =RIGHT, fill =Y) 186# st. pack () 85 Nous detaillerons cette question quelques pages plus loin, car elle ouvre quelques perspectives interessantes. Voir : optimiser les animations a I'aide des threads, page 287. 282 Apprendre d programmer avec Python 18 /ff 1 nnlt 188ff # par tie serveur reseau 189ff self . conn client = { } ft dictionn . des connexions clients 19Uff self . verrou =threading > . Lock ( ) ft verrou pour synchroniser threads 191ff ft Initialisation du serveur - Mise en place du socket : 1 CIO 41 192ff connexion = socket . socket (socket .AF 1NET, socket. SOCK STREAM) 19Jff try: i a a # 194ff connexion . bind ( (self .host, self .port) ) 195ff except socket . error : 19bff txt — .La liaison ou socJcet a i note ^s, port ^s a \ 19 /ff ecnoue . \n ^ (sen .nost, seir.port) 198ff self . avis . insert (END , txt) 1 OQJt 199ff sen . accuen — in one 2UUff else . ft demarrage du thread gue ttant la connexion des clients 202ff self.accueil = ThreadClients (self , connexion) off self . accueil . start ( ) 2U4ff 205# def depl aleat canon (self, id) : 2Ubff "deplacer aleatoirement le canon <id>" ^yi /ff x, y = AppBombardes . depl aleat canon (self , id) 208ff # signaler ces nouvelles coord, a tous les clients : ^U9ff self . verrou . acquire ( ) 91 n# 21uff for cli in self .conn client: 9114 Z llff message = "mouvement de, %s , %s , %s , " % (id, x, y) 212ff self .conn client [cli] . send (message) 91 T+i self . verrou . release ( ) 91 Z14ff 91 R# ^loff del goal (self, i, j) : 91 ait " le canon <i> signale qu'il a atteint l'adversaire 91 T4t 21 /ff AppBombardes . goal (self , i, j) 91 Pit ^loff ff Signaler les nouveaux scores a tous les clients : 219ff self . verrou . acquire ( ) 22uff for cli in self .conn client: 991 a "iff msg =' scores , ' 222ff for id in self.pupi: 9 9 *3 41 22 Jff sc = self .pupifid] . valeur_score () 9 9 ^ 41 224ff msg = msg +"%s;%s," % (id, sc) 225ff self . conn_client [ cli] .send (msg) 22bff time . sleep (. 5) # pour mieux separer les messages 9 9 T 41 22 /ff self . verrou . release ( ) 99Q4t 228ff o o a 44 229ff def ajouter canon(self, id): "instancier un canon et un pupitre de nom <id> dans 2 dictionn." 9*51 ^ J Iff # on alternera ceux des 2 camps 9 *3 9 4* 2 J2ff n = len (self . guns) 9T3 4i if n %2 ==0: 2 J4ff sens = -1 2J5ff else : 9 "3 £C44 2 jbff sens = 1 O *3 T 44 2 J7# x, y = self . coord_aleat (sens) 2 J8ff coul =('dark blue', 'dark red', 'dark green', 'purple', 2J9ff 'dark cyan', 'red', 'cyan', 'orange', 'blue', 'violet') [n] 240ff self . guns [id] = Canon (self . jeu, id, x, y, sens, coul) 241ff self .pupi [id] = Pupitre (self , self . guns [id] ) 94944 Z4Zff self .pupi [id] .inactiver() return (x, y, sens, coul) 9 /I A # 244ff 94 R4i ^43ff def enlever_canon (self , id): 9 yi £44 24bff "retirer le canon et le pupitre dont 1 ' identif iant est <id>" 94'7 4i Z4 /ff if self . active == 0 : # la f enetre a ete ref ermee 248# return 249# self . guns [id] .effacer() 250# del self . guns [id] 251# self .pupi [id] . destroy () 252# del self .pupi [id] 253# 254# def orienter_canon (self , id, angle): 255# "regler la hausse du canon <id> a la valeur <angle>" 256# self . guns [id] . orienter (angle) 18. Communications d trovers un reseau 283 257# 258# 259# 260# 261# 262# 263# 264# 265# 266# 267# 268# 269# 270# 271# 272# 273# 274# 275# 276# 277# 278# 279# 280# 281# 282# if name == ' main ' : AppServeur (host, port, largeur, hauteur) .mainloop () def def def def af ficher (self , txt) : "afficher un message dans la zone de texte self . avis . insert (END , txt) enregistrer_connexion (self , conn, it): "Memoriser la connexion dans un dictionnaire" self . conn_client [it] = conn self .pupi [id] . reglage (angle) tir_canon (self , id): "declencher le tir du canon <id> self .guns [id] .feu() fermer_threads (self , evt) : "couper les connexions existantes et fermer les threads" # couper les connexions etablies avec tous les clients for id in self. conn client: self . conn_client [id] . send ( ' fin ' ) # forcer la fermeture du thread serveur qui attend les requetes if self .accueil != None: self . accueil ._Thread stop ( ) self. active =0 # empecher acces ulterieurs a Tk ii ii Commentaires • Ligne 173 : II vous arrivera de temps a autre de vouloir « intercepter » l'ordre de fermeture de l'ap- plication que l'utilisateur declenche en quittant votre programme, par exemple parce que vous vou- lez 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 <Destroy>, 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 (wid- get Scrollbar) a un widget Text (vous pouvez faire de meme avec un widget Canvas), sans faire appel a la bibliotheque PMW 86 . • Ligne 190 : Instantiation de l'objet-verrou permettant de synchroniser les threads. • Lignes 202-203 : Instantiation de l'objet thread qui attendra en permanence les demandes de connexion des clients potentiels. • Lignes 205 a 213, 215 a 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 li- mite le nombre de clients a 10, ce qui devrait suffire. 'Voir : Python Mega Widgets, page 194. 284 Apprendre a programmer avec Python Programme client Le script correspondant au logiciel client est reproduit ci-apres. Comme celui qui correspond au ser- veur, 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 CanonO et PupitreO a l'identique, ainsi qu'une forme derivee de la classe AppServeurO. 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 ThreadSocketO (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# 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# 38# 39# 40# 41# 42# 43# 44# 45# 46# host, port = '192.168.0.235 largeur, hauteur = 700, 400 ####################################################### # Jeu des bonibard.es - partie cliente # # (C) Gerard Swinnen, Liege (Belgique) - Juillet 2004 # # Licence : GPL # # Avant d'executer ce script, verifiez que l'adresse, # # le numero de port et les dimensions de 1 ' espace de # # jeu indiquees ci-dessous correspondent exactement # # a ce qui a ete defini pour le serveur. # ####################################################### from Tkinter import * import socket, sys , threading, time from canon_serveur import Canon, Pupitre, AppServeur class AppClient (AppServeur) : def init (self, host, port, larg_c, haut_c) : AppServeur. init (self, host, port, larg_c, haut_c) def activer_pupitre_personnel (self , id): self .id =id self .pupi [id] .activer() def imposer_score (self , id, sc) : self .pupi [id] . valeur_score (int (sc) ) def tir_canon (self , id): r = self . guns [id] . feu () if r and id == self .id: self . connex . signaler_tir ( ) def specif icites (self ) : "preparer les objets specif iques de la partie client" self .master . title ( '«< Jeu des bombardes »>') self .connex =ThreadSocket (self , self .host, self. port) self . connex . start ( ) self . id =None def ajouter_canon (self , id, x, y, sens, coul) : "instancier 1 canon et 1 pupitre de nom <id> dans 2 dictionnaires" self . guns [id] = Canon (self . jeu, id, int(x) ,int(y) ,int(sens) , coul) self .pupi [id] = Pupitre (self , self . guns [id] ) self .pupi [id] .inactiver() 35000 # dimensions de 1' espace de jeu # identifiant recu du serveur # renvoie False si enraye 18. Communications d trovers un reseau 285 47# def deplacer_canon (self , id, x, y) : 48# "note: les valeurs de x et y sont revues 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 <id> a la valeur <angle>" 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 converti en une liste : 90# t =msg_recu . split ( ' , ' ) 91# if t[0] =="" or t[0] =="fin": 92# # fermer 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. split (' ; ') 104# self .app.ajouter_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[l] ) 109# elif t[0] ==' angle': 110# # il se peut que 1 ' on ait recu plusieurs infos regroupees . 111# # on ne considere alors que la premiere : 112# self .app. orienter_canon (t[l] , t[2]) 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. 286 Apprendre a programmer avec Python 117# # ceux qui restent sont eux-memes des listes : 118# lc = 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 [1] ,t[2] ,t[3] ) 125# elif t[0] =="depart_de" : 12 6# self .app. enlever_canon(t[l] ) 127# 128# # Le thread <reception> 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# 14 1# # Programme principal : 142# if name ==' main ' : 143# AppClient (host, port, largeur, hauteur) .mainloop ( ) 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 ef- fectivement 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 nou- veau 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 supplementaire (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). 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 algo- rithmes mis en place pour les interpreter. 18. Communications d trovers un reseau 287 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 ex- plorer ! Exercices 18.1 Simplifiez le script correspondant au client de chat decrit a la page 270, 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 213 et sui- vantes), en ne gardant qu'un seul canon et un seul pupitre de pointage. Ajoutez-y une cible mo- bile, 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). Utilisation de threads pour optimiser les animations. Le dernier exercice propose a la fin de la section precedente nous suggere une methodologie de deve- loppements 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 flui- dite de ces animations. Pour arriver a ce resultat, vous devrez abandonner la technique de temporisa- tion que vous avez exploitee jusqu'ici, mais celle que vous allez utiliser a sa place est finalement plus simple ! Temporisation des animations a l'aide de after() Dans toutes les animations que nous avons decrites jusqu'a present, le « moteur » etait constitue a chaque fois par une fonction contenant la methode after(), laquelle est associee d'office a tous les wid- gets 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 l'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 com- ponent toujours une sorte de moteur qui « tourne » continuellement en tache de fond : ce dispositif se met en route lorsque vous activez la methode mainloopO de votre fenetre principale. Comme son nom l'indique fort bien, cette methode met en ceuvre 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 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 70), 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 mainloopO, et e'est done ce gestionnaire central qui declenche l'appel de fonc- tion que vous souhaitez, apres un certain intervalle de temps. 288 Apprendre a programmer avec Python La technique d'animation utilisant la methode after() est la seule possible pour une application fonction- nant toute entiere sur un seul thread, parce que c'est la boucle mainloopO qui dirige l'ensemble du com- portement 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 mainloopO resterait suspendue, ce qui aurait pour consequence que durant cet intervalle de 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 mainloopO, la tempo- risation que Ton peut programmer a l'aide de after() ne peut pas etre tees 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 afterO reside dans la structure de la boucle d'ani- mation (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. Temporisation des animations a l'aide de time.sleep() Vous pouvez ignorer les limitations de la methode afterO evoquees ci-dessus, si vous en confiez l'ani- mation de vos objets graphiques a des threads independants. En procedant ainsi, vous vous liberez de la tutelle de mainloopO, 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 cceur 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 autees threads). Pour ce faire, vous ferez appel a la fonction sleepO du module time. Cette fonction permet de suspendre l'execution du thread courant pendant un certain intervalle de temps, pendant lequel les autees threads et applications continuent a fonctionner. La temporisation ainsi produite ne depend pas de mainloopO, et par consequent, elle peut etre beaucoup plus courte que celle que vous autorise la me- thode after(). Attention : cela ne signifie pas que le rafraichissement de l'ecran sera lui-meme plus rapide, car ce ra- fraichissement continue a etre assure par mainloopO. Vous pourrez cependant accelerer fortement les differents mecanismes que vous installez vous-meme dans vos procedures d'animation. Dans un logi- ciel de jeu, par exemple, il est frequent d'avoir a comparer periodiquement les positions de deux mo- biles (tels qu'un projectile et une cible), afin de pouvoir entreprendre une action lorsqu'ils se rejoignent (explosion, ajout de points a un score, etc.). Avec la technique d'animation decrite ici, vous pouvez ef- fectuer 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 afterO, vous devez lui indiquer la temporisation souhaitee en millisecondes, sous la forme d'un argument entier. Lorsque vous faites appel a la fonction sleepO, 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. Communications d trovers un reseau 289 Exemple concret Le petit script reproduit ci-dessous illustre la mise en ceuvre de cette technique, dans un exemple volon- tairement 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 » mainloopO est lance comme d'habitude sur le thread principal. Le constructeur de l'application instancie un canevas contenant le dessin d'un cercle, un bou- ton 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 me- thode run(). Marche 1# 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# from Tkinter import * from math import sin, cos import time , threading class App ( Frame) : def init (self) : Frame . init (self) self .pack () can =Canvas (self , width =400, height =400, bg = ' ivory ' , bd =3 , relief =SUNKEN) can . pack (padx =5, pady =5) cercle = can . create_oval (185, 355, 215, 385, fill ='red') tb = Thread_balle (can, cercle) Button(self, text ='Marche', command =tb. start) .pack (side =LEFT) # Button (self , text =' Arret ' , command =tb . stop) . pack (side =RIGHT) # arreter 1 ' autre thread si 1 ' on f erme la f enetre : self . bind ( ' <Destroy> ' , tb . stop) class Thread_balle (threading. Thread) : def init (self, canevas, dessin): threading . Thread . init (self) self. can, self. dessin = canevas, dessin self . anim =1 def run (self) : a = 0.0 while self. anim == 1: a += .01 Apprendre a programmer avec Python 29# 30# 31# 32# 33# 34# def stop (self, evt =0) : self . anim =0 x, y = 200 + 170*sin(a), 200 +170*cos(a) self . can . coords (self . dessin, x-15, y-15, x+15, y+15) time . sleep (0 . 010) 35# 36# App() .mainloopO Commentaires • Lignes 13-14 : Afln de simplifier notre exemple au maximum, nous creons l'objet thread charge de l'animation, directement dans le constructeur de l'application principale. Cet objet thread ne de- marrera cependant que lorsque l'utilisateur aura clique sur le bouton « Marche », qui active sa me- thode 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 moins 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-31, ce qui termine la methode run(). L'animation s'arrete, mais le thread qui la gerait s'est termine lui aussi. Si vous es- sayez de le relancer a l'aide du bouton « Marche », vous n'obtenez rien d'autre qu'un message d'er- reur. • Lignes 26 a 31 : Pour simuler un mouvement circulaire uniforme, il suffit de faire varier continuel- lement la valeur d'un angle a. Le sinus et le cosinus de cet angle permettent alors de calculer les co- ordonnees x et y du point de la circonference qui correspond a cet angle 87 . A chaque iteration, Tangle ne varie que d'un centieme de radian seulement (environ 0,6°), et il fau- dra 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 di- minuant cette valeur, mais vous ne pourrez guere descendre en dessous de 1 milliseconde (0.001 s), ce qui n'est deja pas si mal. Rappel Vous pouvez vous procurer le code source de tous nos exemples sur le site : http://www. ulg. ac. be/cifen/inforef/swi/python. htm. Vous y trouverez notamment, dans un fichier nomme canon cibles_multi.py, un petit programme de jeu dans lequel l'utilisateur doit tirer au canon sur une serie de cibles mobiles qui deviennent de plus en plus rapides et nombreuses au cours du temps. Ce jeu utilise les techniques d'animation expliquees ci-dessus. 87 Vous pouvez trouver quelques explications complementaires a ce sujet a la page 216. Annexe A Installation de Python Si vous souhaite^ essay er Python sur votre ordinateur personnel, n'hesite^pas : ['installation est tres facile (et parfaitement reversible). 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 15 janvier 2008, il s'agissait de la version 2.6.1 - Fichier a telecharger : Python-2.6.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 l'installation est terminee, vous pouvez effacer le contenu du repertoire temporaire. Sous Linux Vous avez probablement installe votre systeme Linux a l'aide d'une distribution telle que Ubuntu, SuSE, RedHat... Installez simplement les paquetages Python qui en font partie, en n'omettant pas Tkinter (parfois installe en meme temps que la Python Imaging Library). Sous Mac OS Vous trouverez differentes versions de Python pour Mac OS 9 et Mac OS X jusqu'a la version 10.2 sur le site web de Jack Jansen : http://homepages.cwi.nl/~jack/macpython. Pour les versions plus recentes de Mac OS X, rendez-vous sur http://python.org/download/mac. Installation des Python mega-widgets Visitez le site web : http://pmw.sourceforge.net et cliquez sur le lien « Download the latest version of Pmw as tar.gz file (with full documentation) » pour telecharger le fichier correspondant. Decompressez 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:\Python26. 292 Apprendre a programmer avec Python Sous Linux, il s'agira vraisemblablement de /usr/lib/python. Installation de Gadfly (systeme de bases de donnees) Depuis le site http://sourceforge.net/projects/gadfly, telechargez le paquetage gad fly. Zip. zip. II s'agit d'un fichier archive compresse. Copiez ce fichier dans un repertoire temporaire. 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'est tout ! Vous pouvez eventuellement ameliorer les performances, en effectuant 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. Sous Linux En tant qu'administrateur (root), choisissez un repertoire temporaire quelconque et decompressez-y le fichier archive a du navigateur de fichiers. Entrez dans le sous-repertoire qui s'est cree automatiquement. Lancez la commande : python setup. py install C'est tout ! 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. Annexe B Solutions des exercices Pour quelques exercices, nous ne fournissons pas de solution. EJforce^-pous de les trouver sans aide, meme si cela vous semble difficile. C'est en effet en vous acharnant sur de tels problemes que vous apprendre^ 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 Exercice 4.3 : »> s = 1 »> 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 chaque mois une duree identique de 30 jours) nspm = nspj * 30 # Nombre d' annees contenues dans la duree fournie : na = nsd / nspa # division <entiere> nsr = nsd % nspa # n. de sec. restantes # Nombre de mois restants : nmo = nsr / nspm # division <entiere> nsr = nsr % nspm # n. de sec. restantes # Nombre de jours restants : 294 Apprendre d programmer avec Python nj = nsr / nspj # division <entiere> nsr = nsr % nspj # n. de sec. restantes # Nombre d'heures restantes : nh = nsr / 3600 # division <entiere> nsr = nsr % 3600 # n. de sec. restantes # Nombre de minutes restantes : nmi = nsr /60 # division <entiere> 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 f ourni 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)" Annexe B. Solutions des exercices 295 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 # variante : a, b = 1. , 1 »> while b<65: print b, a a,b = a*2, 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" , 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 # 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 i = 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 : 296 Apprendre d programmer avec Python 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 ' , 1 Decembre ' ] # Affichage : i = 0 while i < len(t2) : print t2 [i] , i = i + 1 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 memorisera 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 Annexe B. Solutions des exercices 297 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 # Af fichage : 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 : 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 () ) print "Veuillez entrer le cote c c = float (raw_input () ) d = (a + b + c)/2 # demi -perimetre s = sqrt(d* (d-a) * (d-b) * (d-c) ) # aire (suivant formule) print "Longueur des cotes =" , a, b, c 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 298 Apprendre d programmer avec Python Exercice 6.8 : # Traitement de nombres entiers compris entre deux limites print "Veuillez entrer la limite inferieure : " , a = input ( ) print "Veuillez entrer la limite superieure : " , b = input ( ) s = 0 # somme 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 : # Annee s 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 : # 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 : 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 chague 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. Annexe B. Solutions des exercices 299 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." 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 = float (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 super ieure 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 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) 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 300 Apprendre d programmer avec Python else : return n3 # test : printmaximum(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] 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 : Exercice 7.14 : def volBoite(xl =10, x2 =10, x3 =10): "Volume d'une boite parallelipipedique" return xl * x2 * x3 # test : print volBoite() Exercice 7.15 : def volBoite(xl =-1, x2 =-1, x3 =-1) : "Volume d'une boite parallelipipedique" if xl == -1 : return xl # aucun argument n ' a ete f ourni elif x2 == -1 : return xl**3 # un seul argument -> boite cubique elif x3 == -1 : return xl*xl*x2 # deux arguments -> boite prismatique else : return xl*x2*x3 Annexe B. Solutions des exercices 301 # 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 toute petite phrase", " ", "*") print changeCar ("Ceci est une toute petite phrase", " ", "*", 8, 12) print changeCar ("Ceci est une toute petite phrase", " ", "*", 12) 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) 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 302 Apprendre a programmer avec Python Variante : IHHHBH 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) 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 de Button (base , text Button (base , text Button (base , text Button (base , text Button (base , text base . mainloop ( ) 5 boutons : 1 , command = al) 2 , command = a2) 3 , command = a3) 4 , command = a4) 5 , command = a5) .pack (side =LEFT) .pack (side =LEFT) .pack (side =LEFT) .pack (side =LEFT) .pack (side =LEFT) Exercices 8.9 et 8.10 : # Dessin d'un damier, avec placement de pions au hasard from Tkinter import * from random import randrange # generateur de nombres aleatoires def damier () : "dessiner dix lignes de carres avec decalage alterne" y = 0 Annexe B. Solutions des exercices 303 while y < 10: if y % 2 == 0 : # une fois sur deux, on x = 0 # commencera la ligne de else : # carres avec un decalage x = 1 # de la taille d'un carre ligne_de_carres (x*c, y*c) y += 1 def ligne_de_carres (x, y) : "dessiner une ligne de carres, en partant de x, y" i = 0 while i < 10: can . create_rectangle (x, y, x+c, y+c, fill='navy') i += 1 x += c*2 # 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 ajouter_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 ( 1 0 ) * c cercle (x, y, c/3, 'red') ##### Programme principal : ############ # Tachez de bien "parametrer" vos programmes, comme nous l'avons # fait dans ce script. Celui-ci peut en effet 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 1 ) can .pack (side =T0P, 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 =ajouter_pion) b2. pack (side =RIGHT, padx =3, pady =3) fen . mainloop ( ) # 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'exercant 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]) 304 Apprendre d programmer avec Python # 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 = forceG(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 gauche 1 () : 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(l, -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 # astre = [0]*2 # liste servant a memoriser les references des dessins x =[50., 350.] # liste des coord. X de chaque astre (a l'ecran) y =[100., 100.] # liste des coord. Y de chaque astre step =10 # "pas" de deplacement initial # Construction de la fenetre : fen = Tk() fen. title (' Gravitation universelle suivant Newton') # Libelles : valMl = Label (fen, text="Ml = " +str(ml) +" kg") valMl . grid (row =1, column =0) valM2 = Label (fen, text="M2 = " +str(m2) +" kg") 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 [1] = 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) Annexe B. Solutions des exercices 305 Button (f ra2 , Button (f ra2 , Button (f ra2 , Button (f ra2 , text="<-" text="->" text=" A " , fg ='blue' , text="v" , fg ='blue' , fg ='blue', command =gauche2) .pack (side =LEFT) fg ='blue', command =droite2) .pack (side =LEFT) command =haut2) .pack (side =LEFT) command =bas2) .pack (side =LEFT) fen . mainloop ( ) GiayftaHoo untv«w > effe sutvairt Mew! cm JJJ 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*1.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 :').grid(row =0, column =0) # "variable Tkinter" associee au champ d' entree. Cet "ob jet-variable" # assure 1 ' interface entre TCL et Python (voir notes, page 165) varTC =StringVar ( ) champTC = Entry (fen, textvariable =varTC) champTC . bind ( "<Return>" , convFar ) champTC . grid (row =0, column =1) # Initialisation du contenu de la variable Tkinter : varTC. set("100.0") Label(fen, text='Temp. Fahrenheit :').grid(row =1, column =0) varTF =StringVar ( ) champTF = Entry (fen, textvariable =varTF) champTF . bind ( "<Return>" , convCel) champTF. grid (row =1, column =1) varTF.set("212.0") fen . mainloop ( ) 250 Temp. Fahrenheit : 177 0 306 Apprendre a programmer avec Python 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 trigono. 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) 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() fen. 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 ( ) 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 Annexe B. Solutions des exercices 307 # variation de la vitesse verticale (toujours vers le bas) : v = v + dv # 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 ca 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 ( ) • r 308 Apprendre d programmer avec Python Exercice 8.33 (Jeu du serpent) Nous ne fournissons ici qu'une premiere ebauche du script : le principe d'animation du « serpent ». Si le cceur vous en dit, vous pouvez continuer le developpement pour en faire un veritable jeu, mais c'est du travail ! : from Tkinter import * ff — -— Definition de guelgues gestionnaires d'evenements def start_it () : "Demarrage de 1 ' animation" gj.ooaJ. riag if flag ==0: flag =1 move ( ) def stop_it () : "Arret de 1' animation" global flag flag =0 def go_left (event =None) : "delacement vers la gauche" global dx , dy dx, dy = -1, 0 def go right (event =None) : global dx, dy dx, dy = 1, 0 def go up (event =None) : "deplacement vers le haut" global dx, dy dx, dy = 0, -1 def go down (event =None) : global dx, dy dx, dy = 0, 1 def move () : "Animation du serpent par recursivite" global flag # Principe du mouvement opere : on deplace le carre de gueue , dont les # caracteristigues sont memorisees dans le premier element de la liste # <serp>, de maniere a l'amener en avant du carre de tete, dont les # caracteristigues sont memorisees dans le dernier element de la liste. # On definit ainsi un nouveau carre de tete pour le serpent, dont on # memorise les caracteristigues en les ajoutant a la liste. # 11 ne reste plus gu'a ef facer alors le premier element de la liste, # et ainsi de suite ... c = serp[0] # extraction des infos concernant le carre de gueue eg = c[0] # ref. de ce carre (coordonnees inutiles ici) 1 =len(serp) # longueur actuelle du serpent (= n. de carres) c = serp[l-l] # extraction des infos concernant le carre de tete xt, yt = c[l], c[2] # coordonnees de ce carre # Preparation du deplacement proprement dit. # (cc est la taille du carre. dx & dy indiguent le sens du deplacement) : x< 3, yg = xt+dx*cc, yt+dy*cc # coord, du nouveau carre de tete # Verification : a-t-on atteint les limites du canevas ? : if xg<0 or xg>canX-cc or yg<0 or yg>canY-cc: flag =0 # => arret de 1 ' animation can. create text(canX/2, 20, anchor =CENTER, text ="Perdu !!!", fill ="red", font="Arial 14 bold") can. coords (eg, xg, yg, xg+cc, yg+cc) # deplacement effectif serp . append ( [eg, xg, yg] ) # memorisation du nouveau carre de tete del(serp[0]) # effacement (retrait de la liste) # Appel recursif de la fonction par elle-meme (=> boucle d'animation) : if flag >0: Annexe B. Solutions des exercices 309 # === Programme principal : ======== # Variables globales modifiables par certaines fonctions : flag =0 # commutateur pour 1 ' animation dx , dy = 1 , 0 # indicateurs pour le sens du deplacement # Autres variables globales : canX, canY = 500, 500 # dimensions du canevas x, y, cc = 100, 100, 15 # coordonnees et cote du premier carre # Creation de l'espace de jeu (fenetre, canevas, boutons ...) : fen =Tk() can =Canvas(fen, bg ='dark gray', height =canX, width =canY) can . pack (padx =10, pady =10) boul =Button(fen, text="Start" , width =10, command =start_it) boul .pack (side =LEFT) bou2 =Button(fen, text="Stop" , width =10, command =stop_it) bou2 .pack (side =LEFT) # Association de gestionnaires d'evenements aux touches flechees du clavier : fen .bind ( "<Left>" , go_left) # Attention : les evenements clavier fen. bind ("<Right>" , go_right) # doivent toujours etre associes a la fen. bind ("<Up>" , go_up) # fenetre principale, et non au canevas fen. bind ("<Down>" , go_down) # ou a un autre widget. # Creation du serpent initial (= ligne de 5 carres) . # On memorisera les infos concernant les carres crees dans une liste de listes serp =[] # liste vide # Creation et memorisation des 5 carres : le dernier (a droite) est la tete. i =0 while i <5 : carre =can . create_rectangle (x, y, x+cc, y+cc, fill="green") # Pour chague carre, on memorise une petite sous-liste contenant # 3 elements : la reference du carre et ses coordonnees de base : serp. append ( [carre, x, y] ) x =x+cc # le carre suivant sera un peu plus a droite i =i+l 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 <Enter>) : ") 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) 310 Apprendre d programmer avec Python of . close () nomF = raw_input ( ' Norn du fichier a traiter : ') choix = raw_input ( ' Entrez "e" pour ecrire, "c" pour consulter les donnees : ') if choix == ' e ' : ecr ireDansFichier ( ) else : 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 ( ) Exercice 9.4 : # Triplement des espaces dans un fichier texte . # Ce script montre egalement comment modifier le contenu d'un fichier # 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("Nom du fichier : ") fichier = open (NomF, 'r+') # ' r+ ' = mode read/write lignes = fichier . readlines () # lire toutes les lignes n=0 while n < len (lignes) : lignes [n] = triplerEspaces (lignes [n] ) n =n+l fichier . seek (0) # retour au debut du fichier fichier . writelines (lignes) # reenregistrement f ichier. close () Exercice 9.5 : # Mise en forme de donnees numerigues . # Le fichier traite est un fichier texte dont chague ligne contient un nombre # reel (sans exposants et encode sous la forme d'une chaine de caracteres) def valArrondie (ch) "representation die du nombre presente dans la chaine ch" Annexe B. Solutions des exercices 311 £ = 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 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 () Exercice 9.6 : # Comparaison de deux f ichiers , 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 f ichiers" , 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') f iB = 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) # compteur de caracteres et "drapeau" # lecture d'un caractere dans chacun # des deux fichiers # difference trouvee 312 Apprendre d programmer avec Python fiA. close () fiB. close () f iC. close () 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 <Enter> pour terminer) :" while 1 : nom = raw input ( "Nom : " ) 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 <Enter> si c'est correct, sinon <n> ") 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) Exercice 9.9 : # Aj outer des informations dans le fichier du club def traduire (ch) : "convertir 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 <Enter> pour terminer) :" # Affichage des donnees deja presentes dans la liste : i = 0 while i < len(tt) : print tt [i] , i = i +1 Annexe B. Solutions des exercices 313 print while 1 : daNai = raw_input ( "Date de naissance : ") sexe = raw_input("Sexe (m ou f) : ") print daNai , sexe ver = raw_input ( "Entrez <Enter> si c'est correct, sinon <n> ") 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 f d. write ("\n") # caractere de fin de ligne f Source = raw_input ( ' Nom du f ichier source : ' ) fDest = raw_input ( ' Nom du f ichier destinataire : ') fs = open (f Source, 'r') fd = open (fDest, 'w') while 1 : ligne = f s . readline () # lire une ligne du f ichier 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 f ichier dest. fd. close () f s. close () Exercice 9.10 : # Recherche de lignes particulieres dans un fichier texte : def chercheCP (ch) : "recherche dans ch la portion de chaine con tenant le code postal" i, f, ns =0,0,0 #ns est un compteur de codes # cc = " " # chaine a construire while i < len(ch) : if ch[i] ="#": ns = ns +1 if ns ==3 : # le CP se trouve apres le 3e code # f = 1 # variable "drapeau" (flag) elif ns ==4 : # inutile de lire apres le 4e code # break elif f ==1 : # le caractere lu fait partie du cc = cc + ch[i] # CP recherche -> on memorise i = i +1 return cc nomF = raw input ("Nom 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 314 Apprendre d programmer avec Python Remarque importante Dans les scripts ci-apres, nous supposons que votre poste de travail est configure de maniere a utiliser I'encodage Utf-8 par defaut (versions recentes de Linux, par exemple). Si votre poste de travail utilise un mode d'encodage plus ancien (cas de nombreuses installations de Windows), vous devrez y remplacer chaque occurrence de Exercice 10.2 (decoupage d'une chaine en fragments) : # -*- coding: Utf8 -*- def decoupe(ch, n) : "decoupage de la chaine ch en une liste de fragments de n caracteres" ch =ch. decode ("Utf 8") # Conversion 'string' => 'Unicode' d, f = 0, n # indices de debut et de fin de fragment tt = [] # liste a construire while d < len(ch) : if f > len (ch) : # on ne peut pas decouper au-dela de la fin f = len(ch) fr = ch[d:f] # decoupage d'un fragment tt. append (fr) # ajout du fragment a la liste d, f = f, f +n # indices suivants return tt def inverse (tt): "rassemble les elements de la liste tt dans 1 ' 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 ="abcdefghi jklmnopqrstuvwxyzl23456789aeiouaeiduaei6u" print ch liste = decoupe(ch, 5) print liste print inverse (liste) Exercices 10.3 & 10.4 : # -*- coding: Utf 8 -*- # Rechercher 1' indice d'un caractere donne dans une chaine def trouve(ch, car, deb=0) : "trouve 1 ' indice du caractere car dans la chaine ch" ch = ch. decode ("Utf 8") # conversion 'string' => 'Unicode' 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 # Test : print trouve ("Coucou c'est moi", "z") print trouve ("Juliette & Romeo", "&") print trouve ("Cesar & Cleopatre" , "r", 5) Exercice 10.5 : # -*- coding: Utf 8 -*- # Comptage des occurrences d'un caractere donne dans une chaine Annexe B. Solutions des exercices 315 "trouve 1 ' indice du caractere car dans la chaine ch" ch = ch. decode ("Utf 8") # conversion 'string' => 'Unicode' car = car. decode ("Utf 8") i, nc = 0, 0 # initialisations while i < len(ch) : if ch[i] == car: nc = nc +1 # caractere est trouve -> on incremente le compteur i = i + 1 return nc # Test : print compteCar ("ananas au jus", "a") print compteCar ("Gedeon est deja la", "e") print compteCar ("Gedeon est deja la", "a") Exercice 10.6 : # -*- coding: Utf8 -*- # Traitement et conversion de lignes dans un petit fichier texte def traiteLigne (ligne) : "convertit une ligne de 'Latinl' en 'Utf8', avec insertion de -*-" ligne = ligne . decode ( "Latinl" ) # conversion 'string' => 'Unicode' newLine = u" " # nouvelle chaine Unicode a construire c, m = 0, 0 # initialisations while c < len (ligne) : # lire tous les caracteres de la ligne if ligne [c] == " ": # Le caractere lu est un espace. # On ajoute une 'tranche' a la chaine en cours de construction : newLine = newLine + ligne [m:c] + "-*-" # On memorise dans m la position atteinte dans la ligne lue : m = c + 1 # ajouter 1 pour "oublier" 1' espace c = c + 1 # Ne pas oublier d' ajouter la 'tranche' suivant le dernier espace : newLine = newLine + ligne [m: ] # Renvoyer la chaine construite, reconcertie en 'string' Utf 8 return newLine . encode ( "Utf 8" ) # Programme principal : nomFS = raw_input("Nom du fichier source (Latin-1) : ") nomFD = raw_input ( "Nom du fichier destinataire (Utf -8) : ") f s = open (nomFS , ' r ' ) # ouverture des 2 f ichers fd = open (nomFD, 'w') while 1 : # boucle de traitement li = f s . readline () # lecture d'une ligne 'source' if li == " " : break # detection de fin de fichier f d. write (traiteLigne (li) ) # traitement + ecriture fd. close () f s. close () Exercice 10.7 : prefixes, suffixe = "JKLMNOP", "ack" for p in prefixes : print p + suffixe Exercice 10.8 : def compteMots (ch) : "comptage du nombre de mots dans la chaine ch" if len(ch) ==0: return 0 nm = 1 # la chaine comporte au moins un mot for c in ch: if c == " " : # il suffit de compter les espaces 316 Apprendre d programmer avec Python return nm # Test : print compteMots ("Les petits ruisseaux font les grandes rivieres") Exercice 10.9 : def chif f re (car) : "renvoie <vrai> si le caractere 'car' est un chif f re" # Cette fonction accepte indif feremment des caracteres 'string' ou # 'Unicode', car les identifiants numeriques associes aux chif f res sont # les memes dans toutes les normes d'encodage. if car in "0123456789": return 1 else : return 0 # Test : print chiffre('d') , chif f re ( ' 7 ' ) , chif f re (u ' 5 ' ) , chif f re (u ' e ' ) Exercice 10.10 : # -*- coding :Utf 8 -*- def majuscule (car) : "renvoie <vrai> si car est une majuscule" car = car .decode ("Otf 8") # conversion string -> Unicode if car in u"ABCDEFGHI JKLMNOPQRSTUVWXYZAEEEEQilAUO" : return 1 else : return 0 # Test : Exercice 10.11 : # _*_ coding :Utf 8 -*- def chaineListe (ch) : "convertit la chaine ch en une liste de mots" ch = ch. decode ("Otf 8") # conversion string => Unicode liste, ct = [], u"" # ct est une chaine temporaire Unicode for c in ch: # examiner tous les caracteres de ch if c == " " : # lorsqu'on rencontre un espace, on ajoute la chaine temporaire I a la liste, apres 1 ' avoir reconvertie en string : liste. append (ct. encode ("Otf 8") ) ct = u"" # ... et on re-initialise la chaine temporaire else : # les autres caracteres examines sont ajoutes a la chaine temp. : ct = ct + c # Ne pas oublier le mot restant apres le dernier espace ! : if ct: # verifier si ct n'est pas une chaine vide liste. append (ct. encode ("Otf8") ) return liste # renvoyer la liste ainsi construite # Tests : li = chaineListe ("Rene est un garcon au caractere heroique") print li for mot in li : print mot, "-" , Annexe B. Solutions des exercices 317 Exercice 10.12 (utilise les deux fonctions definies dans les exercices precedents) : # -*- coding:Utf8 -*- from exercice_10_10 import majuscule from exercice_10_ll import chaineListe txt = "Le prenom de cette Dame est Elise" 1st = chaineListe (txt) # convertir la phrase en une liste de mots for mot in 1st: # analyser chacun des mots de la liste # Pour extraire le premier caractere du mot, il faut passer par Unicode, # sinon les caracteres accentues ne seront pas corrects : motU = mot. decode ("Utf 8") # conversion -> Unicode prem = motU [ 0 ] # extraction du premier caractere prem = prem. encode ( "Utf 8" ) # re-conversion -> string if majuscule (prem) : # test de majuscule print mot # Variante plus compacte , utilisant la composition : for mot in 1st: if majuscule (mot. decode ("Utf 8") [0] . encode (" Utf 8" ) ) : Exercice 10.13 (utilise les deux fonctions definies dans les exercices precedents) : # -*- coding: Utf8 -*- from exercice_10_10 import majuscule from exercice_10_ll import chaineListe def compteMaj (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 # Pour tester le premier caractere du mot, il faut passer par Unicode, # sinon les lettres accentuees ne seront pas traitees correctement : if majuscule (mot. decode ("Utf 8") [0] . encode (" Utf 8" ) ) : c = c +1 return c # Test : phrase = "Les filles Tidgoutt se nomment Josephine, Justine et Corinne" Exercice 10.14 (table des caracteres ASCII) : # -*- coding: Utf- -8 -*- # Table des codes ASCII c = 32 # premier code ASCII <imprimable> while c < 128 : # dernier code strictement ASCII = 127 print "Code", c, ":", unichr(c) II II r r Exercice 10.16 (echange des majuscules et des minuscules) : # -*- coding: Utf 8 -*- def convMajMin (ch) : "echange les majuscules et les minuscules dans la chaine ch" ch = ch. decode ("Utf 8") # conversion -> Unicode nouvC = u" " # chaine a construire for car in ch: code = ord(car) # les codes des ma j . et min . sont separes de 32 unites : if code >= 65 and code <= 91: # majuscules ordinaires code = code + 32 318 Apprendre d programmer avec Python elif code >= 97 and code <= 122 : # minuscules ordinaires code = code - 32 nouvC = nouvC + unichr(code) # renvoi de la chaine construite, reconvertie en string : re turn nouvC . encode ( " Utf 8 " ) # test : print convMajMin ( "Romeo et Juliette") Exercice 10.18 (tester si un caractere donne est une voyelle) : # -*- coding: Utf -8 -*- def voyelle (cu) : "teste si le caractere Unicode <cu> est une voyelle" if cu in u"AEIOUYAEEEEii606aeiouyaeeeeiiouu" : return 1 else : return 0 # Test : print voyelle (u"g") , voyelle (u"0") , voyelle (u"a" ) , voyelle (u"E" ) Exercice 10.19 (utilise la fonction definie dans le script precedent) : # -*- coding: Utf -8 -*- from exercice_10_18 import voyelle def compteVoyelles (chu) : "compte les voyelles presentes dans la chaine Unicode chu" n = 0 for c in chu : if voyelle (c) : n = n + 1 return n # Test : phrase ="Maitre corbeau sur un arbre perche" nv = compteVoyelles (phrase . decode ("Utf 8" ) ) print "La phrase", phrase, "compte", nv, "voyelles." Exercice 10.20 : # _*_ coding: Utf -8 -*- si = u"aeeeeii66uuu" s2 = u"" for c in si : code =ord(c) s2 = s2 + unichr(code -32) print si Exercice 10.21 : # -*- coding: Utf -8 -*- # Conversion en majuscule du premier caractere de chaque mot dans un texte fiSource = raw_input ( "Nom du fichier a traiter (Latin-1) : ") fiDest = raw_input("Nom du fichier destinataire (Utf-8) : ") fs = open (fiSource , 'r') fd = open (fiDest, 'w') while 1 : ch = fs . readline () . decode ("Latinl" ) # lecture d'une ligne if ch == " " : Annexe B. Solutions des exercices 319 break # fin du fichier ch = ch. title () # conversion des initiales en maj . f d. write (ch. encode ("Utf 8") ) # transcription fd. close () fs . close () 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 () Exercice 10.24 : # -*- coding :Utf-8 -*- # Fusion de lignes pour former des phrases fiSource = raw_input ( "Nom du fichier a traiter (Latin-1) : ") fiDest = raw_input("Nom du fichier destinataire (Utf-8) : ") fs = open (fiSource , 'r') fd = open (fiDest, 'w') # On lit d'abord la premiere ligne : chl = f s . readline () .decode ("Latinl" ) # On lit ensuite les suivantes , en les fusionnant si necessaire : while 1 : ch2 = f s. readline () .decode ("Latinl") if not ch2 : # Rappel : une chaine vide est consideree break # comme "fausse" dans les tests # 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] in u"ABCDEFGHIJKLMNOPQRSTUVWXYZAAEEEEIl6uUC" : fd.write(chl.encode("Utf8") ) chl = ch2 # Sinon, on la fusionne avec la precedente, en veillant a en # enlever au prealable le ou les caractere(s) de fin de ligne. # (Pour les fichiers texte sous Windows, il faut en enlever 2) : else : chl = chl[:-l] + " " + ch2 # Attention : ne pas oublier de transcrire la derniere ligne : fd.write(chl.encode("Utf8") ) fd. close () fs . close () Exercice 10.25 (caracteristiques de spheres) : # Le fichier de depart est un fichier <texte> dont chague 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" 320 Apprendre d programmer avec Python d = float (d) # conversion de 1 ' argument (=chalne) 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 # af f iche 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 () fs. close () Exercice 10.26 : # Mise en forme de donnees numerigues # Le fichier traite est un fichier <texte> dont chague 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 <float>, puis arrondi fd. write (str (n) + "\n") # enregistrement fd. close () f s . 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 ch ="" for i in range (n) : v = m * (i+1) ch = ch + "%4d" % (v) return ch de multiplication par m" # calcul d'un des termes # formatage a 4 caracteres Annexe B. Solutions des exercices 321 # 15 premiers termes seulement for a in nt: print tableMulti(a, 15) Exercice 10.30 (simple parcours d'une liste) : # -*- coding:Utf-8 -*- 1st = ['Jean-Michel', 'Marc', 'Vanessa', 'Anne', 'Maximilien ' , 'Alexandre-Benoit' , 'Louise'] for e in 1st: # Le comptage des caracteres n'est garanti correct qu'en Unicode : print "%s : %s caracteres" % (e, len(e. decode ("Utf 8") ) ) 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() Exercice 10.33 (afficher tous les jours d'une annee) : ## Cette variante utilise une liste de listes ## ## (que 1 ' on pourrait aisement 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 chague 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 1 , ' Novembre 1 , ' Decembre ' ] ] j our = [ ' Dimanche ' , ' Lundi ' , ' Mardi ' , ' Mer credi 1 , ' Jeudi ' , ' Vendredi ' , ' Samedi ' ] ja, jm, js, m = 0, 0, 0, 0 while ja <365: ja, jm = ja +1, jm +1 js = (ja +3) % 7 if jm > mois[0] [m] : jm, m = 1, m+1 # ja # js # print jourfjs], jm, mois[l][m] jour dans 1' annee, jm = jour dans le mois jour de la semaine . Le decalage ajoute permet de choisir le jour de depart # element m de 1' element 0 de la liste # element m de 1' 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]] c, d = c+2, d+1 # ! 1 ' element insere doit etre une liste 322 Apprendre d programmer avec Python 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 1 element 0 ) : for i in range (1 , 1000) : if lst[i] : print i, Exercice 10.43 (Test du generateur de nombres aleatoires, page 126) : 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 1 ' 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 chague 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, 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 : Annexe B. Solutions des exercices 323 carte. append ("%s de %s" % (str(val), coul) ) # Tirage au hasard : while 1 : k = raw_input ( "Frappez <c> pour tirer une carte, <Enter> pour terminer ") if k =="" : break r = randrange (52) Exercice 10.45 : Creation et consultation d'un dictionnaire def consultation () : while 1 : nom = raw_input ( "Entrez le nom (ou <enter> 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] , item[l] 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 <enter> pour terminer) : ") if nom == " " : break age = int (raw_input ( "Entrez 1 ' 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: 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 1 , 'Mouse ' : ' Souris ' , 'Keyboard' : 'Clavier' , 'Hard disk' : 'Disgue dur' , ' Screen ' : ' Ecran ' } print dico 324 Apprendre d programmer avec Python Exercice 10.47 : histogramme # -*- coding :Utf -8 -*- # Histogramme des frequences de chaque lettre dans un texte nFich = raw_input ( ' Nom du fichier : ') fi = open (nFich, 'r') # Conversion du fichier en une chaine de caracteres Unicode . # Suivant 1 ' encodage du fichier source , activer 1 ' une ou 1 ' autre ligne : #texte = f i. read () .decode ("Utf 8") texte = f i. read () .decode ("Latinl") f i . close () print texte dico ={} for c in texte: # afin de les regrouper, on convertit c = cupper () # toutes les lettres en majuscules dico[c] = dico. get (c, 0) +1 liste = dico. items () liste . sort () for car, freq in liste: print u"Caractere %s : %s occurrence (s) . " % (car, freq) Exercice 10.48 : # -*- coding: Utf -8 -*- # Histogramme des frequences de chaque mot dans un texte nFich = raw_input ( ' Nom du fichier a traiter : ') fi = open (nFich, 'r') # Conversion du fichier en une chaine de caracteres Unicode . # Suivant 1 ' encodage du fichier source , activer 1 ' une ou 1 ' autre ligne : texte = f i. read () .decode ("Utf 8") #texte = f i. read () .decode ("Latinl") f i . close () # afin de pouvoir aisement separer les mots du texte , on commence # par convertir tous les caracteres non-alphabetiques en espaces : alpha = u"abcdefghi jklmnopqrstuvwxyzeeaucaeiouaeiou" lettres = u"" # nouvelle chaine a construire (Unicode) for c in texte : c = c. lower () # conversion de chaque caractere en minuscule if c in alpha: lettres = lettres + c else : lettres = lettres + ' ' # 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 Annexe B. Solutions des exercices 325 for item in liste : print item[0] , itemfl] Exercice 10.49 : # -*- coding: Utf-8 -*- # Encodage d'un texte dans un dictionnaire nFich = raw_input ( ' Nom du fichier a traiter : ') fi = open (nFich, 'r') # Conversion du fichier en une chaine de caracteres Unicode . # Suivant 1 ' encodage du fichier source , activer 1 ' une ou 1 ' autre ligne : texte = fi.read() .decode ("Utf 8") #texte = fi.read() . decode ("Latinl") fi . 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 = u"abcdefghi jklmnopqrstuvwxyzeeaucaeiouaeiou" # Construction du dictionnaire : dico ={ } # Parcours de tous les caracteres du texte : i =0 # indice du caractere en cours de lecture im =-1 # indice du premier caractere du mot mot = u" " # 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. alphabetique => on est a 1 ' interieur d'un mot mot = mot + c if im < 0 : # memoriser 1 ' indice du premier caractere du mot im =i else : # car . non-alphabetique => fin de mot if mot != u"": # 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 (im) # ajout d'un indice a la liste else: # mot rencontre pour la le fois : dico [mot] =[im] # creation de la liste d' indices mot =u"" # preparer la lecture du mot suivant im =-1 i += 1 # indice du caractere suivant # Affichage du dictionnaire, en clair : listeMots =dico . items () # Conversion du dico en une liste de tuples listeMots.sort() # tri alphabetique de la liste for clef, valeur in listeMots: print clef, ":", valeur Exercice 10.50 : Sauvegarde d'un dictionnaire (complement de I'ex. 10.45). def enregistrement() : fich = raw_input ( "Entrez le nom du fichier de sauvegarde : ") ofi = open (fich, "w") # parcours du dictionnaire entier, convert! 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], valeurfl])) ofi . close () def lectureFichier () : fich = raw_input ( "Entrez le nom du fichier de sauvegarde : ") try: ofi = open (fich, "r") except: print "*** fichier inexistant ***" 326 Apprendre d programmer avec Python return while X : ligne = of i . readline ( ) if ligne ==' 1 c # detection de la fin de fichier break enreg = ligne. split ("@") # restitution d'une liste [cle, valeur] cle = enreg [0] valeur = enreg [1] [:-l] # elimination du caractere de fin de ligne data = valeur. split ("#") # restitution d'une liste [age, taille] age, taille = int (data [0] ) , float (data [1] ) dico[cle] = (age, taille) # reconstitution du dictionnaire of i . close () 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 ( ) Exercice 10.51 : Controle du flux d'execution a I'aide d'un dictionnaire Cet exercice complete le precedent. On ajoute encore deux petites fonctions, et on reecrit le corps prin- cipal 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 def autre () : print "Veuillez frapper 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 f ichier\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 dif ferente pour # chague choix, par 1 ' intermediaire du dictionnaire <fonc> : if fonc. get (choix, autre) () : break # Rem : toutes les fonctions appelees ici renvoient <None> par defaut, Exercice 12.1 : class Domino (object) : def init (self, pa, pb) : self .pa, self .pb = pa, pb def af f iche_points (self) : print "face A :", self .pa, print "face B :", self.pb def valeur (self) : Annexe B. Solutions des exercices 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 to tale des points", vt Exercice 12.2 : class CompteBancaire (object) : def init (self, nom ='Dupont', solde =1000): self .nom, self .solde = nom, solde def depot(self, somme) : self . solde = self . solde + somme def retrait(self , somme): self. solde = self. solde - somme def af f iche (self ) : print "Le solde du compte bancaire de %s est de %s euros." %\ Exercice 12.3 : class Voiture (object) : 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 ( ) 328 Apprendre d programmer avec Python a3.affich e _tout() Exercice 12.4 : class Satellite (object) : 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 si. energie () si . impulsion (500 , 15) si . af f iche_vitesse () print si. energie () Exercices 12.5-12.6 (classes de cylindres et de cones) : # Classes derivees - polymorphisme class Cercle (object) : 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 # 1 ' 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 () Annexe B. Solutions des exercices 329 Exercice 12.7 : # Tirage de cartes from random import randrange class JeuDeCartes (object) : """Jeu de cartes""" # attributs de classe (communs a toutes les instances) : couleur = ('Pique', 'Trefle', '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 l ces emplacements : self . carte [hi ] , self . carte [h2 ] = self . carte [h2] , self . carte [hi] def tirer(self): "Tirage de la premiere carte de la pile" t = len (self . carte) # verifier qu'il reste des cartes if t >0: carte = self . carte [0] # choisir la premiere carte du jeu del (self. carte [0] ) # la retirer du jeu return carte # en renvoyer copie au prog, appelant else : return None # facultatif ### Programme test : if name == ' main ' : jeu = JeuDeCartes () # instanciation d'un objet jeu.battre() # melange des cartes for n in range (53) : # tirage des 52 cartes : c = jeu.tirer() if c == None : # il ne reste aucune carte print 'Termine ! 1 # dans la liste else : print jeu. nom carte (c) # valeur et couleur de la carte Exercice 12.8 : # 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 330 Apprendre d programmer avec Python # 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 12.9 : # _*_ coding :Utf -8 -*- from exercice_12_02 import CompteBancaire class CompteEpargne (CompteBancaire) : def init (self, nom ='Durand', solde =500): CompteBancaire. init (self, nom, solde) self.taux =.3 # taux d'interet mensuel par defaut def changeTaux (self , taux): self . taux =taux def capitalisation (self , nombreMois =6): print "Capitalisation sur %s mois au taux mensuel de %s %%." %\ (nombreMois, self.taux) for m in range (nombreMois) : self. solde = self. solde * (100 +self . taux) /100 Exercice 13.6 : from Tkinter import * def cercle(can, x, y, r, coul ='white'): "dessin d'un cercle de rayon <r> en <x,y> dans le canevas <can>" 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, 'dark green') self.w3 = Wagon (self. can, 250, 30, 'maroon') self.w4 = Wagon (self. can, 370, 30, 'purple') def coucou (self ) : "apparition de personnages dans certaines fenetres" self . wl .perso (3) # ler wagon, 3e fenetre self . w3 .perso (1) # 3e wagon, le fenetre self .w3 .perso (2) # 3e wagon, 2e fenetre self .w4 .perso (1) # 4e wagon, le fenetre def eclai34 (self) : "allumage de l'eclairage dans les wagons 3 & 4" self . w3 . allumer ( ) self . w4 . allumer ( ) class Wagon (object) Annexe B. Solutions des exercices 331 def init (self, canev, x, y, coul ='navy'): "dessin d'un petit wagon en <x,y> dans le canevas <canev>" # memorisation des parametres dans des variables d' instance self .canev, self .x, self .y = canev, x, y # rectangle de base : 95x60 pixels canev. create_rectangle (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 <fen>" # 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.22 : # Dictionnaire de couleurs Nom de la couleur |ocre Exists deja ? |#FFCC00 Test Ajouter la couleur au dictionnaire Enregistrer le dictionnaire Restaurer le dictionnaire from Tkinter import * # Module donnant acces aux boites de dialogue standard pour # la recherche de fichiers sur disgue : 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 332 Apprendre d programmer avec Python # 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) frSup. pack (padx =5, pady =5) f rlnf =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 ajouteCoul (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 petits 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 ) : "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 objet-f ichier ouvert en ecriture : ofi =asksaveasfile (filetypes=[ ("Texte" , " . txt") , ("Tous" ,"*")]) for clef, valeur in self . dico . items () : of i. write ("%s %s\n" % (clef, valeur)) ofi . close () def restaure (self) : Annexe B. Solutions des exercices 333 "restaurer le dictionnaire a partir d'un fichier de memorisation" # La fonction ci-dessous renvoie un objet-f ichier ouvert en lecture : ofi =askopenfile(filetypes=[("Texte",".txt") , ("Tous" ,"*")] ) lignes = ofi . readlines ( ) for li in lignes : cv = li. split () # extraction de la cle et la valeur corresp. self .dico[cv[0] ] = cv[l] ofi . close () Exercice 13.23 (variante 3) : from Tkinter import * from random import randrange from math import sin, cos, pi class FaceDom( object) : 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) H = failles/^ # 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 tracerjpoints (self , val): # creer les dessins de points correspondant a la valeur val : disp = self .pDispo [val -1] for p in disp : self . cer cle (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, f ill=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 l'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 334 Apprendre d programmer avec Python if len (self . des) : return # car les dessins existent deja i 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_points (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 () Exercice 16.1 (Creation de la base de donnees « musique ») : import gadfly connex = gadf ly . gadfly () connex . startup ( "musique" , "E : /Python/essais/ gadfly" ) 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 (<Enter> pour terminer) : ") if ran ==" : 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) # Af f ichage 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 (<Enter> pour terminer) : ") if nom == ' ' : break tit = raw_input ("Titre de 1 ' 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) Annexe B. Solutions des exercices 335 # Affichage des donnees entrees, pour verification cur . execute ( " select * from oeuvres " ) print cur . pp ( ) 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 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 <angle> est recu en tant que chaine . # il f aut 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 l'objet cible : self. cible = self .boss .master . cible if self . anim ==0 : self .anim =1 # position de depart de 1 ' 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 1 ' obus (trajectoire balistique) " # positionner 1 ' 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 : 336 Apprendre d programmer avec Python # 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 1 ' obus a atteint une cible ou les limites du jeu" if self.yo >self.yMax or self.xo <0 or self.xo >self .xMax: 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 1 ' 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 . f in_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: """objet 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) : Annexe B. Solutions des exercices * H ii H """objet thread gerant 1' animation de la cible" def init (self, app, cible) : Thread. init (self) self. cible = cible # objet a deplacer self .app = app # ref. de la fenetre d' application self .sx, self.sy = 6, 3 # increments d'espace et de self .dt =300 # 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 lorsgue 1 ' on f erme la fenetre : self .bind ( ' <Destroy> ' , self . f ermer_threads) def goal (self) : "la cible a ete touchee" self .pup . attribuerPoint (1) self . tc . accelere ( ) def fermer_threads (self , evt) : "arreter le thread d' animation de la cible" self .tc. stop () Index A after (methode) 89,287 alias 124 animation 85, 88, 287 Apache 256 appartenance a une sequence 112 application web 251 arguments 53, 65 B barre d'outils avec bulles d'aide (exemple) 199 base de donnees 235 client pour MySQL 241 dictionnaire d'application 242 la requete select 241 le modele client/serveur 236 mise en ceuvre avec Gadfly 237 recherches dans une base de donnees 239 bloc destructions 1 9 boucle 24, 96, 110 bouton radio (widget) 185 break (instruction) 96 bytecode 3 c cahier des charges 161, 230 calculatrice 10, 76 canevas 72 caractere accentue 29, 36, 193 CGI 252 chaine de caracteres 103 classement des caracteres 113 concatenation, repetition 109 conversion de type 117 encodage Utf-8 105 encodage/decodage 106 extraction de fragments 108 fonctions integrees 116 formatage 117 indicage 35, 103 methodes specifiques 114 operations elementaires 38 premiere approche 34 sequences non modifiables 112 types string et Unicode 104 classe 137 attributs d'instance 140 bibliotheques de classes 157 classes et interfaces graphiques 161 definition d'une classe 138 echange d'informations 1 64 espaces de noms 150 heritage 145 methode 145 methode constructeur 147 objet comme valeurs de retour 143 objet compose d'objets 142 polymorphisme 153 similitude et unicite 141 structure (resume) 155 utilite 137 classe Canon (exemple) 216 client reseau multi-taches 270 rudimentaire 268 code des couleurs (exemple) 161 Combo Box (widget) 1 92 commentaire 28, 230 communication a travers un reseau 265 compilation et interpretation 3 composition d'instructions Tkinter 83 des instructions 16 conditionnelle (execution) 18 controle du flux d'execution 17, 134, 210 curseur (exemple) 172 D debogage 4 debug 4 definir une fonction 51 une methode 145 deplacer des dessins (exemple) 189 derivation (des classes) 138 dessins alternes (exemple) 74 detection d'un clic 78 developpement incremental 202,213 340 Apprendre d programmer avec Python dictionnaire 129 acces aleatoire 132 cles particulieres 132 controle du flux d'execution via un dictionnaire creation 129 histogramme 1 33 methodes specifiques 130 operations sur les dictionnaires 130 parcours 131 division entiere 10 encapsulation 137, 162 encodage par defaut 30 entier (type de donnee) 31 erreur a l'execution 5 de syntaxe 4 semantique 5 etiquette (pour arguments) 65 evenement 70, 78, 176 exception 99 execution conditionnelle 18 experimentation 5, 169 expression 15 F faussete 46 fenetre 67 fenetre avec menus (exemple) 202 Fibonacci (suite de) 26 fichier 91 ecriture et lecture sequentielles 94 enregistrement de variables 98 nom de fichier 93 repertoire courant 93 texte 97 utilite 91 float (type de donnee) 33 flux d'execution (controle du) 1 7 fonction originale 51 predefinie 43 for (instruction) 110, 122, 188 forme d'importation 93 formulaire HTML 254 Frame (widget) 187 134 G Gadfly 236 graphique (interface) 67 grid (methode) 80 H heritage 151, 164 I if (instruction) 18 importer 44, 61, 93 in (instruction) 110,112 input (fonction) 43 instantiation 68, 137 instructions composees 19 imbriquees 20 repetitives 23 integer (type de donnee) 31 interface graphique 67,161 interpretation (et compilation) 3 jeu de Ping 229 jeu des bombardes 213 version reseau 274 K Karrigell 257 lambda (expression) 199 langage formel 6 Latin-1 29,107,193 lignes colorees (exemple) 72 liste 118 concatenation, multiplication 123 controle du flux d'execution via une liste 210 copie 123 creation, avec range() 122 definition 119 insertion d'elements 121 methodes specifiques 119 modification 119 modification (slicing) 121 parcours, a l'aide de for 122 premiere approche 39 suppression, remplacement d'elements 121 test d'appartenance 123 long (type de donnee) 31 M mainloop 70 metaprogrammation 201 mode interactif 9 module 157 de fonctions 44, 60 turtle 45, 60 multi-taches 269 N nombres aleatoires 125, 127 Index o operateurs 10, 15 de comparaison 19 oscilloGraphe (exemple) 168 P pack (methode) 69, 75, 81 parametres 52, 53, 54, 64 parcours d'une sequence 110 petit train (exemple) 1 64 pickle (module) 99 polymorphisme 138 positionnement d'un clic 78 PostgreSQL 236 priorite des operations 15 procedure 57 programme concret 213 protocole de communication 275 prototypage 216 Python Mega Widgets 191 range (fonction) 122 raw_input (fonction) 44 recursivite 88 reel (type de donnee) 33 reseau 265 reserve (mot) 11 s Scale (widget) 172 script 27, 59 CGI 254 Scrolled Canvas (widget) 196 Scrolled Text (widget) 194 sequence destructions 17 serveur reseau elementaire 266 reseau multi-taches 272 web 256 sessions (serveur web) 260 sockets 265 SQL 236 string (type) 34, 104, 113 structure de donnees 103 T threads 269 concurrents 279 time.sleep (pour animations) 288 Tkinter 67,79 try (instruction) 99 tuples 128 typage des parametres 64 dynamique 123 types de donnees 31 u Unicode norme 29, 104 type 104, 108, 113, 193 Utf-8 29, 105, 107, 193 valeur par defaut (de parametres) 64 variable affectation 12 multiple 14 parallele 14 afficher la valeur 1 3 assignation 12 d'instance 140 globale 55, 137, 162 locale 55 mots reserves 1 1 nom de variable 1 1 separateur decimal 14 typage 13 virgule flottante (type) 33 vrai ou faux 46 w web 251 while (instruction) 24, 48 widget 68,79, 168, 176 composite 173 Gerard Swjnnen De formation scientifique, Gerard Swinnen a enseigne la physique, la chimie et la biologie, et developpe une serie de logiciels de simulation experimental et devaluation scolaire. Sollicite pour mettre en ceuvre une filiere d'enseignement secondaire (entree sur I'apprentissage de I'informatique, il a accepte de construire un cours de programmation specifiquement adapte a ce public. « Ce que j'affirme, c'est que I'ap- prentissage de la programmation a sa place dans la formation generate des jeunes, car c'est une extraordinaire ecole de logique, de rigueur, et mime de courage. » Quel meilleur choix pour apprendre la programmation qu'un langage moderne et elegant tel que Python, aussi bon pour le developpement d'applications web que pour la realisation de scripts systeme ou I'analyse de fichiers textuels ? Un support de cours repute et adopte par de nombreux enseignants, avec 40 pages d'exercices corriges Reconnu et utilise par les enseignants de nombreuses ecoles et IUT, complete d'exercices accompagnes de leurs corriges, cet ouvrage original et erudit est une reference sur tous les fondamentaux de la programmation : choix d'une structure de donnees, parametrage, modularity orientation objet et heritage, conception d'interface, multithreading et gestion d'evenements, protocoles de communication et gestion reseau, formulaires web et CGI, bases de donnees... jusqu'd la desormais indispensable norme Unicode (le format UTF-8). A qui s'adresse ce livre ? • Aux etudiants en BTS et IUT Informatique et a leurs enseignants ; A tous les autodidactes ferus de programmation qui veulent decouvrir le langage Python. Au sommaire Preface. Pour le professeur qui souhaite un support de cours. Choisir un langage de programmation. Distribution de Python. Penser comme un programmeur. Langage machine, langage de programmation. Compilation et interpreta- tion. Mise au point d'un programme. Langages naturels et langages formels. Donnees et variables. Noms de variables et mots reserves. Affectation. Typage. Operateurs et expressions. Priorite des operations. Composition. Controle du flux d'execution. Sequence ^instructions. Execution conditionnelle. Operateurs de comparaison. Blocs constructions. Instructions imbriquees. Quelques regies de syntaxe Python. Boucles. Reaffectation. Premiers scripts - ou comment conser- ver nos programmes ? Principaux types de donnees. Les listes (premiere approche). Fonctions. Interaction avec I'uti- lisateur. Importer un module de fonctions. Veracite/faussete d'une expression. Definir une fonction. Variables locales, variables globales. « Vraies » fonctions et procedures. Utilisation des fonctions dans un script. Valeurs par defaut des para- metres. Arguments avec etiquettes. Interfaces graphiques. Premiers pas avec Tkinter. Programmes pilotes par des evenements. Les classes de widgets Tkinter. Controler la disposition des widgets. Animation. Recursivite. Manipuler des fichiers. Ecriture et lecture sequentielle dans un fichier. Gestion des exceptions : les instructions try - except - else. Approfondir les structures de donnees. Les chames de caracteres. Le point sur les listes : tuples, dictionnaires. Classes, objets, attributs. Passage d'objets comme arguments. Objets composes d'objets. Objets comme valeurs de retour d'une fonction. Classes, methodes, heritage. La methode « constructeur ». Espaces de noms des classes et ins- tances. Heritage et polymorphisme. Modules contenant des bibliotheques de classes. Interfaces graphiques. Boutons radio. Cadres. Python Mega Widgets. Barres d'outils avec bulles d'aide - expressions lambda. Fenetres avec menus. Analyse de programmes concrets. Gestion d'une base de donnees. One base de donnees simple avec Gadfly. Ebaucne d'un logiciel client pour MySQL. Applications web. Pages web interactives. interface CGI. Un serveur web en pur Python ! Communications a travers un reseau. Les sockets. Construction d'un serveur et d'un client elementaires. Gerer plusieurs taches en parallele d I'aide des threads. Connexions de plusieurs clients en parallele. Jeu des bombardes, version reseau. Utilisation de threads pour optimiser les animations. Installation sous Windows, Linux, et Mac OS. Solutions des exercices. Annexes.