ISN, passe ton FAQ !

36 Q/R Python et Tkinter pour passer son Bac

Raphaël SEBAN

Ce tutoriel s'adresse en priorité aux élèves de Terminale S option ISN (Informatique et Sciences du Numérique), mais rien n'empêche quiconque d'en faire bon usage.

Nous n'allons pas plonger dans le énième cours théorique sur le qui que quand quoi de la bonne pratique pour programmer ISNement en langage Python.

Vous avez des profs pour cela.

Non, ici, nous aborderons les questions que vous vous (et nous) posez tout au long de l'année scolaire et nous lèverons aussi quelques-unes des erreurs classiques de débutants que vous serez immanquablement appelés à commettre tôt ou tard.

Commentez

- Titre : ISN, passe ton FAQ !

- Auteur : Raphaël SEBAN

- Parution : 6 août 2014

- Public visé :

- Licence : Le contenu de cet article est rédigé par Raphaël SEBAN et est mis à disposition selon les termes de la Licence Creative Commons Attribution - Partage dans les Mêmes Conditions 3.0 non transposé. Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright ® 2013 Developpez.com.

Sommaire

- I. Présentation

- II. Questions

- II-A. Questions générales

- II-B. Questions Python

- II-C. Questions Tkinter

- III. Réponses

- III-A. Réponses générales

- III-A-1. Comment on fait pour insérer du code dans un message de forum ?

- III-A-2. J'ai posté une question sur un forum il y a deux minutes et personne ne m'a encore répondu ! C'est quoi ce bazar ?

- III-A-3. Je pose mes questions sur un forum, mais personne ne me répond jamais. Pourquoi ?

- III-A-4. J'ai envie de rien foutre. Vous pouvez m'aider ?

- III-A-5. J'ai tout fait comme on m'a dit, mais ça ne marche pas ! C'est quoi le malaise ?

- III-A-6. J koz SMS c cool non ?

- III-A-7. Laure tôt graff est moa sa fé deu. Hou ai leu praublêm ?

- III-A-8. Moi, je préfère quand c'est untel qui me répond sur le forum, l'autre, là, je l'aime pas ! Pourquoi c'est pas toujours le même qui m'aide ?

- III-A-9. Je galère comme un sourd sur un problème. Je peux vous contacter par MP (Messagerie Privée) ?

- III-A-10. J'ai écrit un super programme, mais mon prof m'a collé une mauvaise note, je ne comprends pas ?

- III-A-11. Mon prof dit qu'il ne sait pas. Au secours ! Quelqu'un pour m'aider ?

- III-A-12. Le bac est dans 10 jours et je suis grave à la bourre ! Vous pouvez me refiler les soluces pour mon projet ?

- III-B. Réponses Python

- III-B-1. J'ai des erreurs incompréhensibles. Pourtant, j'ai vérifié : mon script est bien juste. Je ne comprends pas ?

- III-B-2. J'ai des caractères bizarres qui s'affichent dans ma console. Comment ça se fait ?

- III-B-3. J'ai une erreur « UnicodeDecodeError: 'ascii' codec can't decode byte 0x... in position ...:ordinal not in range(128) ». C'est quoi ça ?

- III-B-4. J'ai un message d'erreur « Traceback... », mais je n'y comprends rien ! Vous pouvez m'aider ?

- III-B-5. J'ai un message « Syntax Error » sur une ligne qui pourtant est juste. Comment ça se fait ?

- III-B-6. J'ai cette erreur « NameError: name '...' is not defined ». Pourquoi ?

- III-B-7. J'ai ce message : « UnboundLocalError: local variable '...' referenced before assignment ». Ça veut dire quoi ?

- III-B-8. J'ai bien défini ma variable globale et pourtant ça ne marche pas. Comment est-ce possible ?

- III-B-9. Python me dit des gros mots : « TypeError: 'int' object is not callable ». Qu'est-ce que je n'ai pas fait correctement ?

- III-B-10. Un coup ça marche, un coup ça ne marche pas. Je n'y comprends plus rien ! Vous pouvez m'aider ?

- III-B-11. J'ai entendu parler de récursivité. C'est quoi ?

- III-B-12. Je voudrais faire un .exe avec mon programme Python. C'est possible ?

- III-C. Réponses Tkinter

- III-C-1. J'ai fait un programme avec une fenêtre Tkinter, mais elle ne s'affiche pas. Pourquoi ?

- III-C-2. En voulant afficher plusieurs fenêtres Tk(), mon programme ne fonctionne plus. Comment ça se fait ?

- III-C-3. En essayant le script de mon collègue sur mon ordi, j'ai une erreur « ImportError: no module named tkinter ». Qu'est-ce qui ne va pas ?

- III-C-4. Mon widget s'affiche, mais je n'arrive pas à le modifier. Une piste ?

- III-C-5. Y a plus rien qui s'affiche ! C'est quoi ce bazar ?

- III-C-6. J'ai bien affecté une fonction à mon bouton et ça ne marche pas. Que se passe-t-il ?

- III-C-7. Quand je clique sur le bouton, on dirait que mon programme se fige un moment et ensuite tout redevient normal. Pourquoi ?

- III-C-8. Mes collègues utilisent Pygame avec Tkinter pour leur projet. Je peux faire pareil ?

- III-C-9. Pour mon jeu, on m'a dit d'utiliser une boucle Tkinter. C'est quoi ?

- III-C-10. J'ai plusieurs boutons qui doivent faire la même chose à une valeur près. Comment puis-je faire ?

- III-C-11. Je voudrais utiliser une seule Scrollbar pour deux canvas en même temps. C'est possible, ça ?

- III-C-12. Les Scrollbars de Tkinter sont vraiment trop moches ! Y a pas mieux ?

- IV. Remerciements

I - Présentation

ISN (Informatique et Sciences du Numérique) est un nouvel enseignement de spécialité en Terminale Scientifique S proposé depuis la rentrée 2012 et qui nous vaut depuis l'arrivée de nouveaux visiteurs, notamment sur les forums Python. Pour connaître la teneur de ce nouvel enseignement, vous pouvez consulter le programme de la spécialité ISN sur le Bulletin Officiel.

Découvrez aussi ISN sur le site de l'ONISEP.

Cette FAQ (Foire Aux Questions) est articulée autour de trois axes : une question, une réponse et une section « je veux comprendre », pour approfondir la question.

De temps à autre, vous aurez droit à un petit bonus « aller plus loin » qui s'adresse aux plus mordus d'entre vous, ne vous étonnez donc pas que cette rubrique soit quelque peu pointue.

Si vous êtes des bolos, vous ne lirez que les réponses.

Si vous êtes des bolas, vous lirez les questions et les réponses.

Enfin, si vous êtes des badass, vous lirez les questions, les réponses et les approfondissements.

Vous pouvez télécharger l'archive zip contenant tous les scripts de démonstration de ce tutoriel en version Python2 et Python3 : scripts-python.zip

À vous de jouer !

Bonne lecture.

II - Questions

II-A - Questions générales

1. Comment on fait pour insérer du code dans un message de forum ?

2. J'ai posté une question sur un forum il y a deux minutes et personne ne m'a encore répondu ! C'est quoi ce bazar ?

3. Je pose mes questions sur un forum, mais personne ne me répond jamais. Pourquoi ?

4. J'ai envie de rien foutre. Vous pouvez m'aider ?

5. J'ai tout fait comme on m'a dit, mais ça ne marche pas ! C'est quoi le malaise ?

6. J koz SMS c cool non ?

7. Laure tôt graff est moa sa fé deu. Hou ai leu praublêm ?

8. Moi, je préfère quand c'est untel qui me répond sur le forum, l'autre, là, je l'aime pas ! Pourquoi c'est pas toujours le même qui m'aide ?

9. Je galère comme un sourd sur un problème. Je peux vous contacter par MP (Messagerie Privée) ?

10. J'ai écrit un super programme, mais mon prof m'a collé une mauvaise note, je ne comprends pas ?

11. Mon prof dit qu'il ne sait pas. Au secours ! Quelqu'un pour m'aider ?

12. Le bac est dans 10 jours et je suis grave à la bourre ! Vous pouvez me refiler les soluces pour mon projet ?

II-B - Questions Python

1. J'ai des erreurs incompréhensibles. Pourtant, j'ai vérifié : mon script est bien juste. Je ne comprends pas ?

2. J'ai des caractères bizarres qui s'affichent dans ma console. Comment ça se fait ?

3. J'ai une erreur « UnicodeDecodeError: 'ascii' codec can't decode byte 0x... in position ...:ordinal not in range(128) ». C'est quoi ça ?

4. J'ai un message d'erreur « Traceback... », mais je n'y comprends rien ! Vous pouvez m'aider ?

5. J'ai un message « Syntax Error » sur une ligne qui pourtant est juste. Comment ça se fait ?

6. J'ai cette erreur « NameError: name '...' is not defined ». Pourquoi ?

7. J'ai ce message : « UnboundLocalError: local variable '...' referenced before assignment ». Ça veut dire quoi ?

8. J'ai bien défini ma variable globale et pourtant ça ne marche pas. Comment est-ce possible ?

9. Python me dit des gros mots : « TypeError: 'int' object is not callable ». Qu'est-ce que je n'ai pas fait correctement ?

10. Un coup ça marche, un coup ça ne marche pas. Je n'y comprends plus rien ! Vous pouvez m'aider ?

11. J'ai entendu parler de récursivité. C'est quoi ?

12. Je voudrais faire un .exe avec mon programme Python. C'est possible ?

II-C - Questions Tkinter

1. J'ai fait un programme avec une fenêtre Tkinter, mais elle ne s'affiche pas. Pourquoi ?

2. En voulant afficher plusieurs fenêtres Tk(), mon programme ne fonctionne plus. Comment ça se fait ?

3. En essayant le script de mon collègue sur mon ordi, j'ai une erreur « ImportError: no module named tkinter ». Qu'est-ce qui ne va pas ?

4. Mon widget s'affiche, mais je n'arrive pas à le modifier. Une piste ?

5. Y a plus rien qui s'affiche ! C'est quoi ce bazar ?

6. J'ai bien affecté une fonction à mon bouton et ça ne marche pas. Que se passe-t-il ?

7. Quand je clique sur le bouton, on dirait que mon programme se fige un moment et ensuite tout redevient normal. Pourquoi ?

8. Mes collègues utilisent Pygame avec Tkinter pour leur projet. Je peux faire pareil ?

9. Pour mon jeu, on m'a dit d'utiliser une boucle Tkinter. C'est quoi ?

10. J'ai plusieurs boutons qui doivent faire la même chose à une valeur près. Comment puis-je faire ?

11. Je voudrais utiliser une seule Scrollbar pour deux canvas en même temps. C'est possible, ça ?

12. Les Scrollbars de Tkinter sont vraiment trop moches ! Y a pas mieux ?

III - Réponses

III-A - Réponses générales

III-A-1 - Comment on fait pour insérer du code dans un message de forum ?

R : Bouton (#) en haut à droite de l'éditeur de messages.

Vous pouvez aussi insérer manuellement les balises phpBB [code]…[/code] dans le texte de votre message de forum.

Je veux comprendre :

Certains langages comme Python sont particulièrement sensibles aux indentations. Si vous copiez/collez votre code directement dans le flux du message - sans le délimiter par [code]…[/code] - vous modifiez le sens même du code, en le rendant difficile à analyser par les intervenants qui souhaitent vous aider.

De plus, les balises [code]…[/code] permettent aux intervenants de copier/coller vos portions de code chez eux, de les tester, de les corriger, puis de vous retourner un résultat assez rapidement.

Exemple SANS balises :

Exemple AVEC balises :

III-A-2 - J'ai posté une question sur un forum il y a deux minutes et personne ne m'a encore répondu ! C'est quoi ce bazar ?

R : relax Max, c'est un forum, pas un tchat.

Si vous voulez tout tout de suite, ne posez pas vos questions sur un forum, allez plutôt sur un tchat.

Je veux comprendre :

Le principe du forum, c'est justement de laisser aux intervenants le temps de chercher une réponse non seulement adéquate, mais aussi - tant qu'à faire - la plus pertinente possible.

Et ça, ça demande du temps. Alors patience, une réponse peut parfois mettre trois jours avant d'arriver…

Même si en réalité, dans la plupart des cas, vous obtiendrez une réponse en moins d'une heure.

III-A-3 - Je pose mes questions sur un forum, mais personne ne me répond jamais. Pourquoi ?

R : Aidez-nous à vous aider.

Un forum a ses règles de bienséance. Avez-vous pris le temps de les lire en arrivant ?

Et puisque vous êtes là, faites donc un saut à cette page, elle vaut le détour.

Je veux comprendre :

Parce qu'on n'arrive pas au beau milieu d'une gare en criant à tue-tête « hé ! Vous pourriez m'aider quand même ! », de même on ne poste pas n'importe comment sur un forum d'entraide.

Quelques règles à respecter :

- la politesse : c'est la moindre des choses que de commencer son message par « bonjour » (au moins au premier message posté dans un même fil de discussion), d'éviter aussi de tutoyer les intervenants comme si vous aviez élevé les bœufs ensemble ou encore de vous moquer de celles et ceux qui tentent de vous aider ; vous avez le droit de ne pas être d'accord, mais dites-le avec les formes, toujours poliment et - pensez-y tout de même - l'internet n'oublie jamais rien. Le droit à l'oubli peut mettre des années et vous ruiner une carrière prometteuse ;

- expliquer ce qu'on a cherché sur internet - au moins un peu - notamment parmi les ressources de Developpez.com avant de demander de l'aide : montrez que vous n'êtes pas des assistés, vous avez bûché dur avant de crier à l'aide, n'est-ce pas ?

- préciser le contexte : votre problème concerne Python2 ? Python3 ? Tkinter ? Un module Python en particulier ? Un concept de programmation ? Sur quel OS avez-vous rencontré ce problème ? Windows ? Mac ? Linux ? Autre ? N'hésitez pas à publier les messages d'erreur que vous avez eus sur votre console, ça peut aiguiller les intervenants dans leur diagnostic ;

- décrire votre problème : non seulement c'est un point crucial pour bien vous faire comprendre, mais de plus, vous devez vous attacher à présenter votre problème le plus clairement possible sans vous étaler sur des kilomètres et des kilomètres - c'est tout un art, croyez-moi !

- publier le code qui pose problème : et s'il vous plaît, pas juste les lignes que vous pensez poser problème, tout le code concerné par le(s) bug(s) ; si vous n'êtes pas familier des forums, voyez aussi la question « Comment on fait pour insérer du code dans un message de forum ? » ;

- formuler des remerciements avant et après que l'on vous a rendu service : un simple « merci » suffit et pourtant, si vous omettez de remercier une fois que votre problème a été résolu, vous risquez fort de passer pour un goujat et il n'est pas dit qu'on veuille encore vous aider la fois d'après ;

- cliquer sur le bouton « Résolu » en bas de page, une fois que votre problème a trouvé solution ou que vous estimez que le fil de discussion doit être clos parce que vous êtes satisfait(e) des réponses obtenues.

Pour en savoir plus, découvrez la nétiquette sur wikipedia.

III-A-4 - J'ai envie de rien foutre. Vous pouvez m'aider ?

R : Qui veut aider un glandu dans la salle ? Des volontaires ? Non ?

Une erreur fréquente chez les novices sur un forum : donner l'impression que vous attendez que les intervenants fassent le travail à votre place.

Faites un effort ! Présentez-vous correctement.

Je veux comprendre :

C'est une erreur malheureusement courante chez les nouveaux venus sur un forum : publier une demande sans avoir montré que l'on a d'abord cherché sur internet avant d'appeler à l'aide, ne pas faire preuve du minimum de politesse de rigueur, ne pas publier au moins un bout de code prouvant que l'on a travaillé la question ou pire encore, réellement demander à ce que l'on vous fournisse un bout de code pour résoudre votre problème !

N'oubliez pas que les intervenants n'ont pas que vous à aider sur un forum, qu'ils travaillent pour la plupart à côté de leur activité d'entraide bénévole et - surtout - qu'ils ne sont jamais tenus de vous répondre.

Même avec les meilleures intentions du monde, si vous ne faites aucun effort de présentation, vous allez passer pour des mufles et vous risquez de ne plus vous faire aider par la suite.

Attention donc aux maladresses de premier contact sur un forum, elles peuvent finir par vous nuire sur le long terme.

Si ce n'est pas déjà fait, jetez un œil à la question « Je pose mes questions sur un forum, mais personne ne me répond jamais. Pourquoi ? »

III-A-5 - J'ai tout fait comme on m'a dit, mais ça ne marche pas ! C'est quoi le malaise ?

R : C'est comme demander « quelle est la différence entre un lavabo ? »

Si vous ne précisez pas un peu plus votre question, comment voulez-vous qu'on vous aide ?

Je veux comprendre :

Mettez-vous à la place de celles et ceux qui vont vous lire et relisez votre texte plusieurs fois avant de le publier : êtes-vous bien sûr(e) qu'on va vous comprendre ?

Plus vous êtes confus(e) dans vos propos et plus vous risquez de rester sans réponse.

Si vous n'arrivez pas à formuler votre question, peut-être qu'une personne de votre entourage peut vous y aider ?

Si ce n'est pas déjà fait, jetez un œil à la question « Je pose mes questions sur un forum, mais personne ne me répond jamais. Pourquoi ? »

III-A-6 - J koz SMS c cool non ?

R : On peut aussi causer brainfuck, si tu veux.

On perdra encore plus de temps à essayer de se comprendre.

Je veux comprendre :

Le langage abrégé type SMS n'est pas forcément compréhensible pour vos interlocuteurs. Dites-vous que celles et ceux qui proposent leur aide sur les forums n'ont très probablement pas votre âge ni peut-être même votre environnement culturel. Pour eux, le SMS est une curiosité de zoo qu'ils tenteront de décrypter une fois, deux fois, mais pas trois, d'autant plus que cette écriture abrégée est proscrite par les règles du forum.

Et vous resterez seul(e), dans votre désert linguistique, à vous lamenter ainsi : « Je pose mes questions sur un forum, mais personne ne me répond jamais. Pourquoi ? »

Une question indéchiffrable peut appeler une réponse incompréhensible.

Heureusement pour nous, notre belle langue, le français, permet de s'exprimer sur de vastes champs sémantiques et lexicaux - eh quoi ? Vous ne comprenez plus le français à présent ? - aussi, vous redécouvrirez sans doute - et non sans quelque émoustillement - le plaisir caché de s'ouïr dans la beauté féroce d'une verve bien menée.

Nan, je plaisante. Exprimez-vous dans un français simple, cela suffira amplement.

Évitez seulement les anglicismes à tout crin, les abréviations sibyllines, le langage SMS bolosse et tout devrait se passer au mieux dans le meilleur des mondes.

III-A-7 - Laure tôt graff est moa sa fé deu. Hou ai leu praublêm ?

R : Tu t'es vu quand t'as relu ?

Certaines fautes d'orthographe ou de grammaire peuvent littéralement changer le sens d'une phrase et (faire) dire des choses totalement incompréhensibles pour celui ou celle qui tente de vous aider.

Je veux comprendre :

« Le programe est la faute que je vœu vous sinifé est que j'ai l'erreur du tribut error. Pouvez m'aider ? »

Vous n'avez rien compris ?

Ça tombe bien, nous non plus.

Une question indéchiffrable peut appeler une réponse incompréhensible.

Alors, s'il vous plaît, faites un effort, nous n'avons pas toujours le décodeur pour voir dans vos têtes.

III-A-8 - Moi, je préfère quand c'est untel qui me répond sur le forum, l'autre, là, je l'aime pas ! Pourquoi c'est pas toujours le même qui m'aide ?

R : Pas de favoritisme, s'il vous plaît !

Même si le courant passe forcément mieux avec certain(e)s qu'avec d'autres, gardez-vous bien de manifester vos préférences.

Un forum est un lieu public d'échange, d'entraide, pas le creuset des bas instincts de notre espèce.

Je veux comprendre :

Chaque intervenant apporte une part importante de compétences et de savoir-faire. Chacun sa manière, chacun sa façon de s'exprimer.

En revanche, le demandeur d'aide ne devrait jamais négliger aucun de ces nombreux horizons. Un jour, c'est untel qui aide, le lendemain, c'est tel autre. La richesse humaine dans sa diversité, c'est aussi savoir parfois ravaler sa fierté et faire l'effort d'écouter les personnes que l'on n'apprécie pas forcément.

Mon boulanger a une tête pas possible ! Heureusement pour moi, son pain est vraiment bon.

Vous avez votre opinion ? Tant mieux. Mais gardez-la pour vous-même, vous êtes sur un forum pour apprendre, avant toute chose.

Et puis, aussi surprenant que cela puisse paraître, beaucoup de mésententes naissent à la suite de malentendus. Une histoire de malentendants qui finit en dialogue de sourds, vous dis-je, en somme.

C'est entendu ?

Profitez-en pour agrandir le champ de votre (jeune) conscience, jetez donc un œil furtif à la question « Je pose mes questions sur un forum, mais personne ne me répond jamais. Pourquoi ? »

III-A-9 - Je galère comme un sourd sur un problème. Je peux vous contacter par MP (Messagerie Privée) ?

R : Pas de questions techniques par MP, s'il vous plaît !

La MP est là uniquement pour les prises de contact ou les questions d'ordre général.

Vous avez le forum pour toute question technique.

Je veux comprendre :

Il est considéré comme très impoli de contacter un intervenant en particulier pour l'interpeller sur l'un ou l'autre de vos problèmes techniques. Non seulement vous réduisez vos chances d'être aidé(e) par un grand nombre de compétences diverses - comme sur un forum public - mais de plus, vous contredisez la raison même d'être d'un forum : si on vous aide par MP, à quoi sert d'avoir un forum dans ce cas ?

III-A-10 - J'ai écrit un super programme, mais mon prof m'a collé une mauvaise note, je ne comprends pas ?

R : Un code source est plus souvent lu, relu qu'écrit.

Il faut apprendre à rendre votre code source non seulement lisible pour les autres, mais aussi - et surtout - intelligible, de façon à permettre une relecture et une correction sans accroc.

Je veux comprendre :

C'est bien beau d'écrire des programmes qui fonctionnent bien, encore faut-il que les autres puissent les relire et que ce ne soit pas la galère à chaque ligne de code pour comprendre ce que vous avez fait, voulu dire ou essayé d'expliquer.

Voici deux codes sources qui produisent exactement le même résultat :

Code n° 1 :

Code source et fichier à télécharger

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# les coords du point marginal
x,y=(3,30)
import tkinter
e2,b1=e3,e6=(x,y-12)# coord du dehors dedans
from tkinter import *
e1=fen1=b6=x,y,e2
fen3 = Tk()# programe ki ser de l'aff conversion
e1=b2=e6=fen3.title # pour ke l'aff c mieux
x,y,z,n=3.14,-125,1.45**2,32//14
Label(fen3,
text="Veuillez saisir un entier décimal, SVP :"
).pack(pady=5,
padx=10)
af4 = e6=Entry(fen3);e6.pack(# sinon il est nonne
padx=10)
import tkinter
def af3(*y,z=0):
global x,n,af3,af1,af6,n
try:
c6=y+(z**2,z/x-n*x,3.25) # pour centré le point
e3.config(
text=bin(   # c la conv du bin en entier
int(
e6.get(
))))
# on fé l'exception
except:
from tkinter import Label
if '*/+-=axzbf^@' or z-len(y) and af4:
e3.config(text='error')
af6=e2=Label(fen3)
e3 =Label(fen3);e3.pack(
pady=5, padx=10) # input utilizater
af4=Button(fen3,
text="Convertir",
command=af3).pack(side=LEFT,
pady=10,padx=30);af6=Button(fen3, text="Quitter",
command=fen3.destroy).pack(
side=RIGHT, pady=10,
padx=10)
fen3.mainloop()#fontion principal

Résultat visuel :

Code n° 2 :

Code source et fichier à télécharger

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# on utilise la librairie graphique tkinter

from tkinter import *

# zone de déclaration de fonctions

def convertir ():
"""
cette fonction convertit un nombre décimal entier en séquence
binaire de 0 et de 1, puis affiche le résultat;
si les données entrées ne sont pas correctes, le résultat
affichera 'error';
"""
# on récupère les données saisies par l'utilisateur
sequence = entree.get()

# on essaie de faire la conversion en séquence binaire
try:
# conversion d'un entier en séquence binaire
resultat = bin(int(sequence))
# si une erreur survient
except:
# on affiche 'error' à la place
resultat = 'error'
# end try

# affichage du résultat
cvar_resultat.set(resultat)

# end def

# programme principal (GUI - interface graphique utilisateur)

# on crée une fenêtre principale
fenetre = Tk()

# on ajoute des composants graphiques

# étiquette texte à afficher
Label(fenetre, text="Veuillez saisir un entier décimal, SVP :").pack(pady=5, padx=10)

# zone de texte pour la saisie des données entrées par l'utilisateur
entree = Entry(fenetre)
entree.pack(padx=10)

# on va utiliser une variable de contrôle pour afficher un résultat
# variable selon les calculs de conversion que l'on va effectuer
# ultérieurement
cvar_resultat = StringVar()

# on rattache cette variable de contrôle à une étiquette texte afin
# de pouvoir afficher ses données en sortie visuelle
Label(fenetre, textvariable=cvar_resultat).pack(pady=5, padx=10)

# il ne nous reste plus qu'à utiliser un bouton cliquable pour
# déclencher l'opération de conversion du nombre en séquence binaire
# de 0 et de 1
Button(fenetre, text="Convertir", command=convertir).pack(side=LEFT, pady=10, padx=30)

# ainsi qu'un bouton cliquable pour quitter notre application
Button(fenetre, text="Quitter", command=fenetre.destroy).pack(side=RIGHT, pady=10, padx=10)

# pour finir, on lance l'application graphique en entrant dans la
# boucle événementielle principale
fenetre.mainloop()

Résultat visuel :

Lequel des deux comprenez-vous le mieux ?

Pourtant, les deux fonctionnent parfaitement et produisent exactement le même résultat.

Alors ? La lisibilité c'est important, non ?

Aller plus loin avec les conventions de nommage de la PEP 8 de Python (en anglais).

III-A-11 - Mon prof dit qu'il ne sait pas. Au secours ! Quelqu'un pour m'aider ?

R : Tu crois encore au Père Noël à ton âge ?

Votre prof sait parfaitement. Il veut juste que vous appreniez à vous auto-documenter en vous servant de l'internet, des moteurs de recherche, des forums d'entraide et de tous les outils modernes mis à votre disposition pour y arriver.

Je veux comprendre :

Le métier d'informaticien est un long chemin parsemé de perpétuelles nouveautés. Si vous ne prenez pas l'habitude dès le plus jeune âge de vous documenter par vous-même, d'apprendre à aller chercher l'info là où elle se trouve, personne ne le fera à votre place et vous atteindrez très vite vos limites.

L'activité de programmation requiert pour 50 % de son temps de la recherche d'information et de la lecture de documentation.

Même s'il est vrai qu'encore aujourd'hui la plupart des documentations que l'on trouve sur le net est écrite en anglais, il ne faut pas se décourager pour autant : au début, on ne comprend pas bien ce qui s'y dit, et puis, à force de lire et de relire cent fois, mille fois les mêmes termes techniques, on finit par assimiler et par comprendre la doc, même si elle est en anglais.

Alors, quand votre prof fait mine de ne pas savoir, essayez d'apprendre à savoir pour deux.

III-A-12 - Le bac est dans 10 jours et je suis grave à la bourre ! Vous pouvez me refiler les soluces pour mon projet ?

R : Tu nous prends pour des jambons ?

N'essayez même pas de poster des demandes de ce genre, on ne vous répondra pas.

Je veux comprendre :

Vous aviez du temps pour demander sur les forums, puis pour élaborer et mettre au point votre programme. Comment se fait-il que les autres y soient arrivés et pas vous ?

Peut-être que la machine à pomper le code sur internet a un peu trop tourné sur votre clavier d'ordinateur ? Maintenant, si vous ne comprenez pas ce que vous avez copié, ce n'est pas notre problème : le but de l'éducation, c'est d'apprendre, pas d'exploiter sans comprendre.

De plus, il est rare qu'un programmeur ne comprenne pas ce qu'il a lui-même écrit…

III-B - Réponses Python

III-B-1 - J'ai des erreurs incompréhensibles. Pourtant, j'ai vérifié : mon script est bien juste. Je ne comprends pas ?

R : Pensez aux en-têtes standards pour vos scripts Python !

Prenez l'habitude de toujours commencer vos scripts Python avec les mentions suivantes :

Pour Python2 :

#!/usr/bin/env python
# -*- coding: utf-8 -*-

Pour Python3 :

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

Je veux comprendre :

Dans certains cas de figure, il est possible d'appeler un script Python sans préciser formellement dans la console :

python mon_script.py

ou pour Python3 :

python3 mon_script.py

Par exemple, sous Linux, vous pouvez appeler n'importe quel script - rendu exécutable par chmod +x mon_script.py - en invoquant son nom précédé des caractères './', par exemple :

./mon_script.py

Avec une invocation automatique, dite aussi self-launch, le système va regarder la toute première ligne du fichier script en question et va chercher ce qu'on appelle un shebang (contraction de sharp bang et parfois de shell bang - page web en anglais), c'est-à-dire les deux caractères '#!' suivis généralement d'indications sur l'emplacement de l'interpréteur du script. Cette technique permet d'aller chercher n'importe quel interpréteur pour n'importe quel langage de script (pas seulement Python), puis de lui affecter le fichier de script en entrée d'interprétation.

Sous MS-Windows®, vous avez un cas similaire lorsque vous utilisez :

py mon_script.py

En effet, l'exécutable py.exe procède un peu de la même manière : il regarde la toute première ligne du script et tente de déterminer quelle version de Python il doit appeler.

S'il n'y a pas d'en-têtes Python standards, vous risquez d'avoir une erreur au lancement du script.

Pour en savoir plus, jetez un œil à la PEP 0263 de Python (en anglais).

III-B-2 - J'ai des caractères bizarres qui s'affichent dans ma console. Comment ça se fait ?

R : Vous êtes très certainement sous Python2 et ses problèmes récurrents de caractères Unicode.

Mettez ceci en début de script et vous devriez résoudre 99 % de vos problèmes liés à Unicode :

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import unicode_literals

Je veux comprendre :

L'une des différences très marquantes entre Python2 et Python3 est la prise en charge native des caractères Unicode.

Les caractères Unicode permettent de couvrir tous les alphabets et tous les signes existant sur Terre, mais en programmation informatique, il n'y a aucune obligation de les utiliser comme encodage par défaut dans un langage donné, c'est même assez rare quand c'est le cas.

L'équipe Python a décidé de prendre cette option : autant Python2 gère les caractères Unicode comme étant juste un encodage parmi d'autres, autant Python3 a adopté l'idée de pouvoir scripter directement en Unicode UTF-8 - et bien plus encore.

Si vous ne voulez pas galérer avec les problèmes Unicode, je ne puis que vous conseiller d'installer Python3 sur votre machine et de l'utiliser pour écrire vos scripts, vous y gagnerez.

Lien de téléchargement : https://www.python.org/downloads/ (en anglais).

Documentation officielle Python (2 et 3) : https://www.python.org/doc/ (en anglais).

Si vous n'avez pas le choix et que vous êtes obligé(e) de rester sous Python2, jetez un œil attentif au Unicode HOWTO de Python (en anglais).

Pour en savoir plus, jetez un œil à la PEP 0263 de Python (en anglais).

III-B-3 - J'ai une erreur « UnicodeDecodeError: 'ascii' codec can't decode byte 0x... in position ...:ordinal not in range(128) ». C'est quoi ça ?

R : Vous êtes très certainement sous Python2 et ses problèmes récurrents de caractères Unicode.

Mettez ceci en début de script et vous devriez résoudre 99 % de vos problèmes liés à Unicode :

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import unicode_literals

Je veux comprendre :

C'est le même problème que celui expliqué dans la question « J'ai des caractères bizarres qui s'affichent dans ma console. Comment ça se fait ? »

III-B-4 - J'ai un message d'erreur « Traceback... », mais je n'y comprends rien ! Vous pouvez m'aider ?

R : Lisez la dernière ligne du message d'erreur en premier.

Lisez la dernière ligne du message d'erreur puis remontez le message à rebours jusqu'à ce que vous ayez trouvé le ou les indices qui vous permettront de résoudre votre problème.

Je veux comprendre :

Armez-vous aussi de patience. Les messages d'erreur sont (presque) toujours en anglais et presque toujours pas franchement faciles à comprendre.

En général, si vous ne vous laissez pas impressionner par la tonne de textes qui vous inonde lors d'un message d'erreur et que vous prenez bien le temps d'éplucher ce qui s'y dit en partant de la dernière ligne du message et en le remontant à rebours, vous devriez trouver la réponse à votre problème.

Dans Python, un message d'erreur se lit en partant de la dernière ligne et en remontant à rebours jusqu'à trouver le ou les indices qui permettront de résoudre le problème.

Au pire, si vraiment vous n'y comprenez rien, le forum Développez.net (DVP) est là pour vous aider, mais essayez tout de même de faire l'effort d'analyser vos messages d'erreur par vous-mêmes, vous verrez, c'est très instructif et on apprend beaucoup comme ça.

Vous trouverez des explications succinctes sur chaque type d'erreur (appelé aussi « exception ») sur cette page de documentation officielle Python (en anglais).

III-B-5 - J'ai un message « Syntax Error » sur une ligne qui pourtant est juste. Comment ça se fait ?

R : L'erreur de syntaxe se cache en réalité plus haut dans le script.

Il faut remonter le script à rebours à partir du point d'erreur signalé par l'interpréteur Python et bien regarder attentivement : la plupart du temps, on oublie une parenthèse fermante quelque part ou une bêtise d'inattention de ce genre.

Je veux comprendre :

Un interpréteur lit les lignes d'un script séquentiellement, les unes après les autres, selon des schémas qui lui sont propres. Ces schémas s'appellent la syntaxe du langage.

Si on oublie une parenthèse fermante par exemple, ce qui est un cas très fréquent qui arrive à tout le monde, l'interpréteur continue de lire les lignes suivantes croyant qu'elles appartiennent à l'expression comprise entre parenthèses, vu qu'il n'a pas encore rencontré de parenthèse fermante.

Pensez aux parenthèses fermantes oubliées dans les lignes précédentes.

Mais comme les lignes suivantes n'ont syntactiquement rien à voir avec une expression entre parenthèses, l'interpréteur lève une erreur SyntaxError pour se plaindre de ce qu'il prend pour une incohérence, quand bien même les lignes incriminées seraient justes, cela n'a pas d'importance.

Exemple :

#!/usr/bin/env python
# -*- coding: utf-8 -*-

a = 0

x,y = (10, 20   # l'erreur se trouve ici (parenthèse fermante manquante)

z = 2**3 + 10*2.35 - 45//89

Qui provoque l'erreur :

File "./forum.py", line 8
z = 2**3 + 10*2.35 - 45//89
^
SyntaxError: invalid syntax

Alors que la ligne en question est parfaitement juste. C'est le cas typique d'une parenthèse fermante oubliée quelque part dans les lignes qui précèdent.

C'est donc un truc à bien se remémorer : quand vous avez une erreur de syntaxe dans votre script et que la ligne en question semble parfaitement juste, c'est à coup sûr parce que l'erreur se trouve dans les lignes qui précèdent la ligne incriminée par l'interpréteur, vous devez donc remonter l'examen des lignes à rebours jusqu'à trouver ce qui manque - parenthèse fermante ? - ou ce qui ne va pas.

III-B-6 - J'ai cette erreur « NameError: name '...' is not defined ». Pourquoi ?

R : Vous utilisez un nom de variable AVANT de l'avoir explicitement défini.

Pensez à toujours bien initialiser vos variables dès le départ, quitte à les modifier ensuite.

Je veux comprendre :

C'est un peu comme si vous parliez à quelqu'un de choses qu'il ne connaît pas. Automatiquement, il va vous interrompre pour vous demander de lui expliquer ces choses.

Il en va de même pour un interpréteur Python : s'il ne sait pas de quoi vous parlez, il va vous signaler que vous ne lui avez pas défini ce que vous entendez par le nom de variable que vous utilisez.

Comme pour un interlocuteur humain, vous allez donc devoir lui expliquer de quoi il en retourne précisément, sauf que Python utilise un langage mathématique, il faut donc lui définir les choses mathématiquement.

Par exemple, ceci est incorrect et provoque une erreur NameError :

#!/usr/bin/env python
# -*- coding: utf-8 -*-

def f(x):

print a     # d'où sort ce 'a' ? vouliez-vous dire 'x' plutôt ?

# end def

# début du programme

f(0)

Alors que ceci est correct :

#!/usr/bin/env python
# -*- coding: utf-8 -*-

def f(x):

print a     # appel à une variable globale 'a' en lecture seule

# end def

# début du programme

a = 0   # on définit 'a' dès le départ

# on appelle ensuite seulement la fonction
f(0)

Aller plus loin avec les conventions de nommage de la PEP 8 de Python (en anglais).

III-B-7 - J'ai ce message : « UnboundLocalError: local variable '...' referenced before assignment ». Ça veut dire quoi ?

R : Vous faites appel à une variable locale qui n'a pas été définie au bon endroit.

Définissez toujours vos variables locales à une fonction donnée au début de la fonction et en-dehors de toute condition.

Je veux comprendre :

Cette erreur arrive souvent lorsque l'on définit une variable locale à une fonction à l'intérieur d'un bloc conditionnel if …: bloc else: bloc sans l'avoir définie au préalable dans le code de la fonction et que l'on fait appel à cette variable avec la certitude qu'elle sera définie en temps voulu.

Voici un cas d'école typique :

#!/usr/bin/env python
# -*- coding: utf-8 -*-

def f(x):
if x % 4 == 0:
a = 10.584
elif x % 4 == 1:
a = 12.352
elif x % 4 == 2:
a = 24.689
elif x % 4 == 3:
a = 39.547
# end if
# on retourne le résultat de notre traitement
return a
# end def

# test

x = 100.235

print "ajustement de la valeur de %f : %f" % (x, f(x))

Résultat console :

Traceback (most recent call last):
File "./forum.py", line 22, in <module>
print "ajustement de la valeur de %f : %f" % (x, f(x))
File "./forum.py", line 15, in f
return a
UnboundLocalError: local variable 'a' referenced before assignment

Saurez-vous trouver où se cache l'erreur ?

Réponse :

A priori, x % 4 signifie x modulo 4 et fournit bien les valeurs (0, 1, 2, 3) attendues en résultat de ce modulo, donc la variable locale 'a' devrait toujours être initialisée, n'est-ce pas ?

Eh bien, non.

Ouvrez une console Python (ou IDLE) et tapez ce qui suit :

>>> 100 % 4
0
>>> 100 % 4 == 0
True
>>> 100.235 % 4
0.23499999999999943
>>> 100.235 % 4 == 0
False
>>> 100.235 % 4 == 1
False

Note : les symboles '>>> ' représentent l'invite de commande de la console Python et ne doivent pas être saisis au clavier.

N'entrez que ce qui suit ces symboles.

Qu'est-ce qu'on remarque ici ? Eh oui, le calcul du modulo pour un nombre à virgule flottante à décimales non nulles produit en résultat un nombre à virgule flottante et non pas un entier !

Ce qui implique qu'aucun test dans la fonction f(x) ne sera juste et donc que la variable 'a' ne sera jamais initialisée en-dehors de ces tests.

D'où l'erreur au moment de se référer à cette variable dans le return a en fin de fonction.

Comment corrigeriez-vous ce bout de code pour qu'il fonctionne correctement ?

Réponse classique :

#!/usr/bin/env python
# -*- coding: utf-8 -*-

def f(x):
# on force 'x' à n'être qu'un entier
x = int(x)
# et le reste devrait suivre...
if x % 4 == 0:
a = 10.584
elif x % 4 == 1:
a = 12.352
elif x % 4 == 2:
a = 24.689
elif x % 4 == 3:
a = 39.547
# end if
# on retourne le résultat de notre traitement
return a
# end def

# test

x = 100.235

print "ajustement de la valeur de %f : %f" % (x, f(x))

Résultat console :

ajustement de la valeur de 100.235000 : 10.584000

Aller plus loin

Si pour vous coder signifie bien plus qu'une simple option scolaire, découvrez ici ce que l'on peut faire avec la syntaxe Python.

Voici ma réponse personnelle au problème posé ci-dessus :

#!/usr/bin/env python
# -*- coding: utf-8 -*-

def f(x):
# à quoi bon écrire autant de lignes ?
return (10.584, 12.352, 24.689, 39.547)[int(x) % 4]
# end def

# test

x = 100.235

print "ajustement de la valeur de %f : %f" % (x, f(x))

Résultat console :

ajustement de la valeur de 100.235000 : 10.584000

Python est un langage très puissant.

Son exploration est un régal de tous les instants !

III-B-8 - J'ai bien défini ma variable globale et pourtant ça ne marche pas. Comment est-ce possible ?

R : Vous avez oublié de déclarer « global ... » au début de la fonction.

Si pour une variable globale donnée 'ma_variable', vous ne déclarez pas global ma_variable en début de fonction où vous l'utilisez, vous ne pourrez utiliser cette variable qu'en lecture seule et vous ne pourrez pas la modifier.

Je veux comprendre :

Dans une fonction, l'interpréteur Python a besoin du mot-clé réservé global en début de fonction pour bien faire la distinction entre le nom d'une variable locale à cette fonction et le même nom provenant d'une variable globale.

Par exemple, si vous modifiez une variable globale 'ma_variable' dans une fonction et que vous omettez de déclarer global en début de fonction, Python va croire que vous définissez une variable locale nommée elle aussi 'ma_variable' et il lui donnera donc la priorité dans le code de la fonction.

Pour éviter ce genre de confusion, vous devez donc recourir à la mention global :

Code source et fichier à télécharger

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

def f(x):
# ici ON NE PEUT PAS modifier notre variable globale
if x > 5:
ma_variable = 1 # Python SUPPOSE que c'est une variable LOCALE
# end def

def g(x):
global ma_variable  # ici ON PEUT modifier notre variable globale
if x > 5:
ma_variable = 1 # Python SAIT que c'est une variable globale
# end def

# début du programme

ma_variable = 250
f(1)
print("f(1):\t ma_variable =", ma_variable)
f(10)
print("f(10):\t ma_variable =", ma_variable)
g(1)
print("g(1):\t ma_variable =", ma_variable)
g(10)
print("g(10):\t ma_variable =", ma_variable)

Résultat console :

f(1):     ma_variable = 250
f(10):     ma_variable = 250
g(1):     ma_variable = 250
g(10):     ma_variable = 1

Aller plus loin

On peut utiliser le mot-clé global pour déclarer tout une liste de variables en une seule fois :

Code source et fichier à télécharger

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

def f(x):
# init variables GLOBALES
global a, b, c, d, e
# init variables LOCALES
y = a * x + b
z = c * x**2 + d * y**2 - e**2
# on modifie quelques globales
# pour le prochain appel de f(x)
a = b // 2
c = e + d
b = a * c // e
d = c // (b + 1)
# résultat du calcul
return (x, y, z)
# end def

# début du programme

# init variables globales
a = 10
b = 20
c = 12
d = 14
e = 200

# init test
x = 30

# boucle essais
for i in range(10):
print("Pour x = {}, coordonnées (x, y, z) = {}".format(x, f(x)))
# end for

# fin du programme

Résultat console :

Pour x = 30, coordonnées (x, y, z) = (30, 320, 1404400)
Pour x = 30, coordonnées (x, y, z) = (30, 310, 1978500)
Pour x = 30, coordonnées (x, y, z) = (30, 155, 1022000)
Pour x = 30, coordonnées (x, y, z) = (30, 62, 472232)
Pour x = 30, coordonnées (x, y, z) = (30, 31, 343779)
Pour x = 30, coordonnées (x, y, z) = (30, 0, 265100)
Pour x = 30, coordonnées (x, y, z) = (30, 0, 445100)
Pour x = 30, coordonnées (x, y, z) = (30, 0, 625100)
Pour x = 30, coordonnées (x, y, z) = (30, 0, 805100)
Pour x = 30, coordonnées (x, y, z) = (30, 0, 985100)

Remarque : si vous initialisez une variable globale à l'intérieur d'une fonction, vous n'êtes pas obligé(e) de l'initialiser en plus en-dehors de la fonction.

Exemple :

Code source et fichier à télécharger

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from tkinter import *

def debut_programme ():
# init variables GLOBALES
global fenetre, canvas, bouton_jouer, bouton_quitter
# on crée la fenêtre principale
fenetre = Tk()
fenetre.title("Mon premier programme !")
fenetre.resizable(width=False, height=False)
# on lui ajoute des composants graphiques
canvas = Canvas(fenetre, bg="sky blue", width=320, height=240)
canvas.pack(padx=10, pady=10)
bouton_jouer = Button(fenetre, text="Jouer !", command=lancer_jeu)
bouton_jouer.pack(side=LEFT, padx=10, pady=5)
bouton_quitter = Button(fenetre, text="Quitter", command=fenetre.destroy)
bouton_quitter.pack(side=RIGHT, padx=10, pady=5)
# on initialise les données du jeu
initialiser_jeu()
# boucle événementielle principale
fenetre.mainloop()
# end def

def initialiser_jeu ():
# initialisation des données du jeu
# ici, on ne modifie pas la variable canvas
# ON L'UTILISE, nuance...
# on efface le canevas
canvas.delete(ALL)
# nouveau texte...
canvas.create_text(
160, 120,
anchor=CENTER,
text="Mon super jeu !",
font="sans 16 bold",
fill="yellow",
)
# end def

def lancer_jeu ():
# on lance une nouvelle partie ici
# on désactive le bouton 'jouer'
# on le réactivera en fin de partie
bouton_jouer.configure(state=DISABLED)
# on efface le canevas
canvas.delete(ALL)
# on change la couleur du fond
canvas.configure(background="gold")
# nouveau texte...
canvas.create_text(
160, 120,
anchor=CENTER,
text="Début de la partie...",
font="sans 14 bold",
fill="coral",
)
# end def

if __name__ == "__main__":
# début du programme
debut_programme()
# end if

Résultat visuel :

Ici, vous pouvez constater que la totalité du programme a été encapsulée dans des fonctions.

Il n'y a aucune initialisation de variables globales en-dehors des fonctions.

III-B-9 - Python me dit des gros mots : « TypeError: 'int' object is not callable ». Qu'est-ce que je n'ai pas fait correctement ?

R : Collision de noms de variables avec des noms de fonctions.

Utilisez toujours des noms explicites aussi bien pour vos variables que pour vos fonctions.

Je veux comprendre :

À force d'utiliser des abréviations ou des noms de fonctions et de variables trop courts, vous finissez par utiliser des variables et des fonctions qui ont le même nom. Selon l'endroit du code où vous vous trouvez, il est possible que l'interpréteur Python râle parce que vous avez utilisé une variable à la place d'une fonction.

Cas d'école typique :

#!/usr/bin/env python
# -*- coding: utf-8 -*-

def f(x):   # <-- notre fonction s'appelle f !
# init variables GLOBALES
global f, g, h
# init variables LOCALES
y = f * x + g   # on utilise une variable globale f ou une fonction f ?
z = h * x + y
# on modifie quelques globales
# pour le prochain appel de f(x)
f = f + 2   # idem ici: globale f ou fonction f ?
g = g * 3
h = h - 5
# résultat du calcul
return (x, y, z)
# end def

# début du programme

# init variables globales
f = 4   # collision ! on a déjà une fonction qui s'appelle f !
g = 10
h = 12

# init test
x = 30

# boucle essais
for i in range(10):
print "Pour x = %d, coordonnées (x, y, z) = %s" % (x, f(x)) # boum !
# end for

# fin du programme

Résultat console :

Traceback (most recent call last):
File "./forum.py", line 31, in <module>
print "Pour x = %d, coordonnées (x, y, z) = %s" % (x, f(x)) # boum !
TypeError: 'int' object is not callable

La solution consiste ici à prendre l'habitude, dès le départ, de donner des noms explicites à vos variables comme à vos fonctions :

- pour les variables : donnez un nom commun représentatif de ce que contient la variable e.g. ne nommez pas 'choucroute' une variable appelée à contenir le rayon d'un cercle, appelez-la tout simplement 'rayon' (voire 'rayon_cercle'), ce sera forcément plus parlant ;

- pour les fonctions : en maths, on note les fonctions avec une seule lettre ; en informatique, on note les fonctions avec une action, un verbe, qui essaie d'illustrer au mieux ce que fait la fonction e.g. évitez les f(x) et consorts, utilisez par exemple interpoler(x) ou tracer(x) pour expliciter vos intentions.

Un nom commun pour une variable, un verbe pour une fonction.

La PEP 8 (en anglais) de Python énonce quelques règles de nommage (en anglais) que vous n'êtes pas obligés de suivre, mais dont vous devriez néanmoins fortement vous inspirer, notamment :

- les noms de variables et de fonctions s'écrivent toujours en minuscules, avec les lettres a-z de l'alphabet anglais (donc non accentuées), avec le signe souligné ('_') dit underscore et les chiffres 0-9 ;

- un nom débute par une lettre a-z ou le signe souligné ('_'), mais pas par un chiffre e.g. 'x_1' est valide, '_x1' est valide aussi, mais pas '1_x' ;

- dans un nom de variable ou de fonction, on sépare les mots par le signe souligné ('_') : par exemple, on préférera écrire 'tracer_courbe()' que 'tracerCourbe()' ou 'tracercourbe()' ;

- les notations commençant par deux underscores - exemple __rayon_cercle ou __multiplier__ (a, b) - sont à éviter tant que vous ne maîtrisez pas Python un tant soit peu : ces notations sont réservées aux identificateurs dits private ou aux fonctions magiques ;

- les noms de CONSTANTES suivent les mêmes règles que pour les noms de variables sauf qu'on les note en majuscules : par exemple, on écrira plutôt PAGE_NOT_FOUND = 404 que page_not_found = 404, même s'il n'existe pas de type de données spécifique pour les constantes en Python, ce n'est ici qu'une convention de notation pour faire ressortir les constantes dans le code ;

- les noms de classes s'écrivent en notation CamelCase c'est-à-dire en plaçant une majuscule à chaque mot différent dans le nom, le reste étant toujours en minuscules ; quelques exemples : 'Canvas', 'LabelFrame', 'MatrixDisplayHook', '_TkInterfaceManager', '__FactorySubclass', 'Tk2048Game', etc.

Toutes ces règles de nommage visent un objectif pratique : rendre le code lisible, intelligible et facile à cartographier. Si on respecte bien ces règles, on peut retrouver du premier coup d'œil les classes utilisées, les constantes, etc.

III-B-10 - Un coup ça marche, un coup ça ne marche pas. Je n'y comprends plus rien ! Vous pouvez m'aider ?

R : On n'est jamais mieux servi(e) que par soi-même !

C'est l'occasion rêvée pour apprendre à déboguer votre programme.

Je veux comprendre :

Oui, déboguer un programme, c'est une discipline qui s'apprend.

Savoir déboguer est aussi important que savoir coder.

Quelques pistes :

- commencez par décortiquer les messages d'erreur : jetez donc un œil à la question « J'ai un message d'erreur « Traceback... », mais je n'y comprends rien ! Vous pouvez m'aider ? » ;

- faites des tests dans une console Python (même version de Python que votre script, n'ajoutez pas de la difficulté à la difficulté, SVP) : certaines assertions de votre code ne sont peut-être pas aussi justes que vous le pensez ; voir le cas typique cité dans la question « J'ai ce message : « UnboundLocalError: local variable '...' referenced before assignment ». Ça veut dire quoi ? » ;

- faites des tests en fichier séparé : créez un nouveau fichier de test à part e.g. test.py et mettez-y une simulation simplifiée du problème que vous voulez résoudre ; il arrive souvent que l'on rencontre un problème précis dans des projets comportant plusieurs milliers de lignes de code : au lieu de fouiller dans toutes ces lignes, le plus simple reste encore de copier/coller juste la séquence qui vous intéresse dans un fichier à part, puis de tester ce qui ne va pas pour cette séquence précisément ; on appelle cela « faire un test sur un coin de table » ;

- utilisez les outils de débogage inclus dans IDLE, l'éditeur fourni d'office avec Python : jetez un œil à ce tutoriel intéressant ;

- passez par un logiciel de débogage : il existe des logiciels spécialisés dans le traçage et le débogage de scripts, mais il n'est pas dit que vous ne perdrez pas plus de temps à essayer de comprendre comment on utilise de tels logiciels plutôt qu'à déboguer vous-même votre script.

Voyez aussi ce qui se dit à ce sujet dans la documentation officielle Python (en anglais).

III-B-11 - J'ai entendu parler de récursivité. C'est quoi ?

R : C'est quand une fonction s'appelle elle-même.

La récursivité est un concept intéressant pour certaines fonctionnalités, mais elle demeure gourmande en ressources mémoire et en temps machine, surtout si la fonction engendre un grand nombre de récursions pour obtenir un résultat - voir le cas très célèbre du calcul de la fonction factorielle.

Une fonction récursive est une fonction qui s'appelle elle-même.

Il vaut donc mieux recourir à la récursivité une fois que l'on s'est assuré(e) qu'il n'existe pas d'autres alternatives beaucoup plus performantes.

Je veux comprendre :

Prenons justement le cas typique de la fonction factorielle.

En mathématiques, la fonction factorielle se définit par avec pour cas particulier .

Pour calculer cette fonction en programmation, nous avons donc plusieurs approches :

1. On boucle de 1 à n et on multiplie le résultat précédent à chaque étape (itération) ;

2. On remarque que et on se dit qu'on peut tenter le recours à la récursivité.

Voici comment l'on procède pour l'approche n° 1 (itération) :

Code source et fichier à télécharger

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import timeit

def factorielle (n):
# n doit être un entier naturel
n = abs(int(n))
# valeur par défaut
# 0! = 1! = 1
resultat = 1
# on ne traite qu'à partir de n = 2, 3, 4, ...
if n > 1:
for i in range(2, n + 1):
resultat = resultat * i
# end for
# end if
return resultat
# end def

def test ():
for n in range(10):
print("{}! = {}".format(n, factorielle(n)))
# end for
# end def

# test

print(timeit.timeit(test, number=1))

Résultat console (environ 203 µs) :

0! = 1
1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
6! = 720
7! = 5040
8! = 40320
9! = 362880
0.000202894210815

Pour l'approche n° 2 (récursivité) :

Code source et fichier à télécharger

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import timeit

def factorielle (n):
# n doit être un entier naturel
n = abs(int(n))
# si n < 2 on retourne 1 puisque 0! = 1! = 1
if n < 2: return 1
# on utilise la récursivité (la fonction s'appelle elle-même)
return n * factorielle(n - 1)
# end def

def test ():
for n in range(10):
print("{}! = {}".format(n, factorielle(n)))
# end for
# end def

# test

print(timeit.timeit(test, number=1))

Résultat console (environ 239 µs) :

0! = 1
1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
6! = 720
7! = 5040
8! = 40320
9! = 362880
0.000239133834839

Bon, là, l'écart n'est que de 36 µs environ, mais voyons ce qui se passe avec de grands nombres :

Code source et fichier à télécharger

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import timeit

def iterative_factorielle (n):
# n doit être un entier naturel
n = abs(int(n))
# valeur par défaut
# 0! = 1! = 1
resultat = 1
# on ne traite qu'à partir de n = 2, 3, 4, ...
if n > 1:
for i in range(2, n + 1):
resultat = resultat * i
# end for
# end if
return resultat
# end def

def recursive_factorielle (n):
# n doit être un entier naturel
n = abs(int(n))
# si n < 2 on retourne 1 puisque 0! = 1! = 1
if n < 2: return 1
# on utilise la récursivité (la fonction s'appelle elle-même)
return n * recursive_factorielle(n - 1)
# end def

def test_iterative ():
for n in range(debut, fin):
iterative_factorielle(n)
# end for
# end def

def test_recursive ():
for n in range(debut, fin):
recursive_factorielle(n)
# end for
# end def

# tests

debut = 980
fin = debut + 10

boucles = 1000

print("calculs de factorielles de {}! à {}!".format(debut, fin))
print("sur un échantillon statistique de {} boucles :".format(boucles))
print("\ncalculs avec iterative_factorielle(n)...")

test1 = timeit.timeit(test_iterative, number=boucles)

print("total temps iterative : {} s".format(test1))
print("\ncalculs avec recursive_factorielle(n)...")

test2 = timeit.timeit(test_recursive, number=boucles)

print("total temps recursive : {} s".format(test2))
print("\nécart recursive - iterative : {:0.6f} s".format(test2 - test1))

Résultat console :

calculs de factorielles de 980! à 990!
sur un échantillon statistique de 1000 boucles :

calculs avec iterative_factorielle(n)...
total temps iterative : 2.74929011399945 s

calculs avec recursive_factorielle(n)...
total temps recursive : 5.866635123000378 s

écart recursive - iterative : 3.117345 s

Là, y a pas photo !

Sans parler qu'au-delà de 1000!, la fonction récursive fait péter un joli :

File "./forum.py", line 27, in recursive_factorielle
return n * recursive_factorielle(n - 1)
File "./forum.py", line 23, in recursive_factorielle
n = abs(int(n))
RuntimeError: maximum recursion depth exceeded while calling a Python object

De quoi s'intéresser à d'autres alternatives, me semble-t-il. Non ?

III-B-12 - Je voudrais faire un .exe avec mon programme Python. C'est possible ?

R : Oui, mais c'est assez ardu.

Il va falloir plonger dans les arcanes de cx_Freeze ou de Py2exe.

Je veux comprendre :

La philosophie du langage Python est plutôt tournée vers le partage de code source ouvert que vers la notion de code propriétaire, aussi, la démarche de créer un exécutable pour MS-Windows® - qui implique de fermer le code pour l'empaqueter dans un exécutable n'est pas prévue nativement.

En général, cette étape n'est pas vraiment nécessaire. Pour utiliser des logiciels Python, il suffit de récupérer les scripts et de les exécuter tels quels, sans autre forme de procès.

Maintenant, si vous tenez absolument à enfermer votre code dans un exécutable, vous allez devoir faire quelques efforts et vous documenter sur cx_Freeze et Py2exe.

Voir aussi ce qui se dit sur Python et Windows® dans la documentation officielle Python (en anglais).

III-C - Réponses Tkinter

III-C-1 - J'ai fait un programme avec une fenêtre Tkinter, mais elle ne s'affiche pas. Pourquoi ?

R : Avez-vous mis mainloop() en fin de script ?

Il y a de fortes chances pour que vous ayez oublié de lancer la boucle événementielle principale de Tkinter en bout de script.

Exemple :

Code source et fichier à télécharger

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from tkinter import *

fenetre = Tk()

# mettez ici tout le code dont vous avez besoin

Label(fenetre, text="Essai fenêtre").pack(pady=20, padx=10)

Button(fenetre, text="Quitter", command=fenetre.destroy).pack(pady=5)

# /!\ n'oubliez pas de finir avec la boucle principale /!\

fenetre.mainloop()

Résultat visuel :

Je veux comprendre :

Une librairie graphique - appelée aussi GUI pour Graphical User Interface - fonctionne sur le principe des interactions événementielles entre l'utilisateur humain et la machine. Par exemple, si on veut exécuter une action donnée, on va cliquer sur un bouton qui se trouve dans la fenêtre programme. Le bouton va en réalité déclencher un événement qui sera ensuite associé à la fonctionnalité que l'on veut utiliser. De même, quand on redimensionne une fenêtre, celle-ci notifie un événement « changement de taille » à la librairie graphique afin que cette dernière puisse redessiner la fenêtre à sa nouvelle taille. Comme tout fonctionne à coups d'événements dans un environnement graphique, il faut une boucle principale qui capture tous ces événements et qui les traite ensuite.

Pensez à mettre fenetre.mainloop() en fin de script.

Pour Tkinter, cette boucle événementielle principale s'appelle mainloop(). Il faut donc systématiquement l'appeler en fin de script, car une fois cette boucle principale lancée, tout le code qui sera écrit ensuite ne pourra s'exécuter que lorsque la boucle principale prendra fin, c'est-à-dire lorsque vous aurez fermé la fenêtre principale de votre programme.

Par exemple :

Code source et fichier à télécharger

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from tkinter import *

fenetre = Tk()

# mettez ici tout le code dont vous avez besoin

Label(fenetre, text="Essai fenêtre").pack(pady=20, padx=10)

Button(fenetre, text="Quitter", command=fenetre.destroy).pack(pady=5)

# /!\ n'oubliez pas de finir avec la boucle principale /!\

fenetre.mainloop()

# ce code ne s'exécutera que lorsque la fenêtre principale sera fermée

print("fin du programme")

III-C-2 - En voulant afficher plusieurs fenêtres Tk(), mon programme ne fonctionne plus. Comment ça se fait ?

R : Une seule fenêtre Tk() par programme !

Vous ne devez déclarer qu'une seule fenêtre Tk() par programme, puis utiliser Toplevel() pour toutes les autres fenêtres que vous voulez afficher.

Exemple :

Code source et fichier à télécharger

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from tkinter import *

def nouvelle_fenetre ():
"""
cette fonction crée une nouvelle fenêtre fille à chaque fois
qu'on clique sur le bouton correspondant
"""

global fenetre_fille

# ceci est une fenêtre fille (Toplevel)
# elle sera automatiquement détruite lorsque vous quitterez le
# programme en fermant la fenêtre principale

fenetre_fille = Toplevel()

Label(fenetre_fille, text="Hello good people!").pack(pady=20, padx=10)

Button(fenetre_fille, text="Quitter", command=fenetre_fille.destroy).pack(pady=5)

# end def

# ceci est la fenêtre principale du programme (Tk)

fenetre = Tk()

fenetre.title("Fenêtre principale")

# mettez ici tout le code dont vous avez besoin

Label(fenetre, text="Cliquez sur le bouton").pack(pady=20, padx=10)

Button(fenetre, text="Nouvelle fenêtre fille", command=nouvelle_fenetre).pack(pady=5, padx=10)

Button(fenetre, text="Quitter", command=fenetre.destroy).pack(pady=5)

# /!\ n'oubliez pas de finir avec la boucle principale /!\

fenetre.mainloop()

Résultat visuel :

Je veux comprendre :

Pour faire simple, lorsque vous appelez Tk(), vous appelez une fenêtre principale de programme. Or, pour un programme donné, il ne peut y avoir qu'une et une seule fenêtre principale, toutes les autres étant forcément des fenêtres secondaires, c'est-à-dire des Toplevel().

Une seule déclaration de Tk() pour l'ensemble du programme.

On utilise Toplevel() pour les fenêtres secondaires.

C'est pourquoi vous êtes obligé(e) de commencer votre programme en appelant au moins la fenêtre principale Tk(), suivie ensuite - si vous en avez besoin - d'autres fenêtres secondaires Toplevel().

Notez aussi que durant le déroulement de votre programme, vous pouvez fermer autant de fenêtres secondaires Toplevel() que vous voulez, indépendamment les unes des autres, alors que le simple fait de fermer la fenêtre principale Tk() provoque, lui, la fermeture automatique de toutes les fenêtres secondaires de même que l'arrêt du programme graphique. Ce n'est donc pas du tout le même comportement entre une fenêtre principale Tk() et une fenêtre secondaire Toplevel().

III-C-3 - En essayant le script de mon collègue sur mon ordi, j'ai une erreur « ImportError: no module named tkinter ». Qu'est-ce qui ne va pas ?

R : Vous n'avez pas la même version de Python.

Et donc pas la même adaptation de Tkinter non plus.

Prenez l'habitude d'installer et Python2 et Python3 sur votre machine quand vous voulez faire des essais avec des scripts provenant d'autres personnes.

Lien de téléchargement : https://www.python.org/downloads/ (en anglais).

Je veux comprendre :

La librairie graphique Tkinter a été entièrement remaniée dans son organisation entre les versions Python2 et Python3. Ainsi, un script écrit pour Python2 utilise plusieurs modules affiliés à Tkinter, mais néanmoins distincts, alors que pour un script écrit pour Python3, tous les modules ont été regroupés sous un seul package nommé tkinter. Chaque version a ses propres modules et sa propre organisation interne qu'elle ne partage pas avec l'autre version.

Sous Python2, on appelle les modules distincts, affiliés à Tkinter, comme ceci :

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import Tkinter
import ttk
import Tix
import tkFont
import ScrolledText
import tkMessageBox
import tkFileDialog
import tkSimpleDialog
import tkCommonDialog
import tkColorChooser

Alors que sous Python3, on appelle les modules tkinter équivalents comme ceci :

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import tkinter
import tkinter.ttk
import tkinter.tix
import tkinter.font
import tkinter.scrolledtext
import tkinter.messagebox
import tkinter.filedialog
import tkinter.simpledialog
import tkinter.commondialog
import tkinter.colorchooser

Note : dans la mesure du possible, mettez toujours vos mentions import en tout premier dans le script, avant tout autre instruction.

III-C-4 - Mon widget s'affiche, mais je n'arrive pas à le modifier. Une piste ?

R : Vous avez affecté None à votre variable objet.

Séparez la définition de l'objet de la méthode .pack(), .grid() ou .place() que vous utilisez.

Le grand classique du genre :

Code source et fichier à télécharger

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from tkinter import *

fenetre = Tk()

# voici l'erreur LA PLUS RÉPANDUE QUI SOIT chez les débutants tkinter :

canvas = Canvas(fenetre, bg="ivory", height=50).pack() # <-- pack() retourne None donc canvas = None

print("canvas =", repr(canvas))

# il faut d'abord déclarer l'objet

canvas = Canvas(fenetre, bg="sky blue", height=50)

# ensuite seulement appliquer .pack()

canvas.pack()

print("canvas =", repr(canvas))

btn_quitter = Button(fenetre, text="Quitter", command=fenetre.destroy)

btn_quitter.pack()

print("btn_quitter =", repr(btn_quitter))

fenetre.mainloop()

Résultat visuel :

Résultat console :

canvas = None
canvas = <tkinter.Canvas object at 0x7f3550ad8d68>
btn_quitter = <tkinter.Button object at 0x7f3550ad8dd8>

Je veux comprendre :

Avec l'opérateur d'appel de méthode point '.', Python permet de faire plusieurs choses en une seule ligne, comme par exemple de créer un nouvel objet puis d'exploiter immédiatement l'une ou l'autre de ses méthodes dans la foulée.

Lorsque vous écrivez :

canvas = Canvas(fenetre).pack()

Vous effectuez des opérations précises dans l'ordre suivant :

1. Vous déclarez un identificateur (une variable) qui porte le nom de canvas ;

2. Vous demandez à l'interpréteur Python d'assigner à cette variable le résultat de l'expression qui se trouve après le signe égal '=' ;

3. L'interpréteur évalue alors l'expression de gauche à droite ;

4. Tout d'abord, il évalue Canvas(fenetre), c'est-à-dire la création d'un nouvel objet Tkinter.Canvas() qu'il relie immédiatement à l'objet fenetre comme mentionné dans l'expression ;

5. Ensuite, il rencontre l'opérateur point '.' : il sait donc qu'il doit appeler la méthode qui va suivre avec l'objet qu'il vient de créer, ce qu'il fait en allant chercher la méthode pack() dans l'objet Tkinter.Canvas() nouvellement créé ;

6. Comme cette méthode existe dans cet objet, tout va bien : l'expression peut être évaluée jusqu'au bout ;

7. L'interpréteur exécute alors la méthode pack() et - manque de chance pour nous - cette méthode retourne None en fin d'exécution, ce qui signifie que l'évaluation finale de l'expression qui se trouve après le signe égal '=' vaut la dernière valeur évaluée dans l'expression, c'est-à-dire ici, None ;

8. Pour finir, une fois son expression évaluée, l'interpréteur Python place la valeur obtenue - None - dans l'identificateur canvas en cours d'assignation, c'est pourquoi l'expression canvas = Canvas(fenetre).pack() est équivalente à l'expression canvas = None.

Finalement, vous aurez créé un objet Canvas() qui, parce qu'il est relié à fenetre par au moins une référence existera dans fenetre, mais vous aurez aussi assigné la valeur None à votre variable canvas et vous n'aurez donc aucun contrôle sur l'objet réel Canvas().

Si vous avez bien compris ce qui se passe dans la tête de l'interpréteur Python lorsqu'il effectue l'initialisation d'une variable, vous aurez bien compris pourquoi il faut noter finalement :

canvas = Canvas(fenetre)   # on crée l'objet et on l'assigne à canvas
canvas.pack()              # on appelle pack() une fois que canvas est correctement initialisé /!\

III-C-5 - Y a plus rien qui s'affiche ! C'est quoi ce bazar ?

R : Vous avez mélangé des pack() avec des grid() ou des place() pour un même widget conteneur.

N'utilisez qu'une seule méthode : c'est ou pack() ou grid() ou place() - mais surtout pas un mix - au sein d'un même widget conteneur. Évitez les panachages autant que possible. Prenez même l'habitude de n'utiliser qu'une seule méthode tout au long de votre programme.

Je veux comprendre :

Les widgets Tkinter sont tous dotés de trois gestionnaires d'affichage - appelés geometry managers dans la doc officielle - pack(), grid() et place(), chacun ayant sa propre façon d'afficher les widgets, mais aucun n'ayant priorité sur les deux autres.

Ainsi, lorsque vous placez dans un même widget conteneur - appelé aussi widget parent ou master dans certaines documentations - des widgets que vous placez tantôt en appelant pack(), tantôt en appelant grid() ou encore en appelant place(), vous provoquez un conflit d'affichage entre tous ces widgets, chaque gestionnaire voulant placer son widget, mais ne pouvant pas griller la politesse aux autres gestionnaires d'affichage.

Du coup, le système tourne en rond indéfiniment et vous avez l'impression que votre programme s'est planté avec ce qu'on appelle généralement un freeze - écran gelé, plus rien ne se passe.

Depuis la version Tcl/Tk 8.6 sur laquelle s'appuie Tkinter, un système de sécurité a été ajouté et déclenche un message d'erreur en cas de conflit d'affichage, ce qui est beaucoup plus confortable pour le débogage des scripts.

Si vous installez les dernières versions en date de Python2 et de Python3, vous devriez avoir aussi cette petite nouveauté ô combien appréciable.

Lien de téléchargement : https://www.python.org/downloads/ (en anglais).

Exemple de test de conflit d'affichage avec Python3 et Tcl8.6 :

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from tkinter import *

print("Tcl version:", TclVersion)

fenetre = Tk()

Label(fenetre, text="widget.pack()").pack()
Label(fenetre, text="widget.grid()").grid()
Label(fenetre, text="widget.place()").place()

fenetre.mainloop()

Résultat console :

Tcl version: 8.6
Traceback (most recent call last):
File "./forum.py", line 11, in <module>
Label(fenetre, text="widget.grid()").grid()
File "/usr/lib/python3.4/tkinter/__init__.py", line 2023, in grid_configure
+ self._options(cnf, kw))
_tkinter.TclError: cannot use geometry manager grid inside . which already has slaves managed by pack

Si vous avez un freeze, forcez l'arrêt du script en fermant la console qui l'accompagne ou via le gestionnaire de tâches de votre système d'exploitation.

Aller plus loin

Notez en revanche qu'un conflit d'affichage ne survient que pour un mix de gestionnaires au sein d'un même widget conteneur : si votre widget conteneur A contient lui-même un autre widget conteneur B, les widgets qui se placeront dans ce widget conteneur B seront libres de choisir leur gestionnaire d'affichage indépendamment des widgets placés dans le conteneur A.

Exemple :

Code source et fichier à télécharger

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from tkinter import *

# on crée la fenêtre principale
fenetre = Tk()
fenetre.resizable(width=False, height=False)

# on crée un conteneur A
conteneur_A = Frame(fenetre)
conteneur_A.pack()

# on ajoute des widgets à A
Label(conteneur_A, text="Je suis dans A et j'utilise pack()").pack(padx=5, pady=5)
Label(conteneur_A, text="Moi aussi ! dans A et j'utilise pack()").pack(padx=5, pady=5)

# on ajoute aussi un conteneur B dans A
conteneur_B = Frame(conteneur_A)
conteneur_B.pack(padx=5, pady=5)

Button(
conteneur_A,
text="Je suis dans A et j'utilise pack()",
command=fenetre.destroy,
).pack(padx=5, pady=5)

# maintenant, on ajoute des widgets à B
canvas = Canvas(conteneur_B, bg="white", width=300, height=200)
canvas.create_text(150, 100, text="Je suis dans B et j'utilise grid()")
hbar = Scrollbar(conteneur_B, orient=HORIZONTAL)
vbar = Scrollbar(conteneur_B, orient=VERTICAL)

# on décide de les placer avec grid()
canvas.grid(row=0, column=0, sticky=NW+SE)
hbar.grid(row=1, column=0, sticky=EW)
vbar.grid(row=0, column=1, sticky=NS)

# boucle principale programme
fenetre.mainloop()

Résultat visuel :

Comme vous pouvez le constater, en respectant l'ordre d'agencement des widgets conteneurs, on peut utiliser les méthodes pack(), grid() et place(), mais une seule méthode à la fois pour un même widget conteneur donné.

Sur le forum Développez.com, on rencontre aussi assez souvent l'usage intensif - voire abusif - de la méthode place().

Le gestionnaire place() sert à placer un widget avec des coordonnées absolues ou relatives. Ce type de placement est rigide : si l'on redimensionne la fenêtre, les widgets ne suivent pas le redimensionnement et restent fixes à leur place ou donnent des résultats médiocres.

N'utilisez place() qu'en dernier recours.

pack() et grid() répondent généralement à 99 % des cas rencontrés.

En programmation professionnelle, on essaie toujours de faire des interfaces graphiques GUI qui soient souples, adaptables, redimensionnables.

Le secret pour y arriver ?

On utilise pack() pour empiler les widgets quand les contraintes le permettent - c'est-à-dire la plupart du temps - on utilise grid() quand on a réellement besoin de placer les widgets en grille d'affichage et enfin, on ne recourt à place() que dans les cas les plus extrêmes.

En dix ans de programmation Python/Tkinter, je n'ai jamais eu recours à place(), les gestionnaires pack() et grid() ayant toujours pleinement rempli leur fonction - et je me demande encore aujourd'hui comment certain(e)s peuvent faire un usage aussi abusif de place() dans leurs scripts ?

Pour finir, si vous vous sentez l'âme anglophone, vous pouvez vous plonger dans la documentation suivante (en anglais, évidemment) :

- The Tkinter Pack geometry manager ;

- The Tkinter Grid geometry manager ;

- The Tkinter Place geometry manager.

Elle vaut vraiment le détour, vous ne regretterez pas l'effort fourni.

III-C-6 - J'ai bien affecté une fonction à mon bouton et ça ne marche pas. Que se passe-t-il ?

R : Vous avez affecté la valeur retour de la fonction et non pas la fonction elle-même.

Enlevez les parenthèses d'appel de fonction et ne gardez que le nom de la fonction.

Cas typique :

Code source et fichier à télécharger

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from tkinter import *

def convertir ():
resultat.set(bin(int(entree.get())))
# end def

fenetre = Tk()
Label(fenetre, text="Entrez un nombre entier:").pack()
entree = Entry(fenetre)
entree.pack()
Label(fenetre, text="Résultat de la conversion:").pack()
resultat = StringVar()
Label(fenetre, textvariable=resultat).pack()

# command=callback attend un NOM de fonction
# et NON PAS la valeur retour d'une fonction

Button(fenetre, text="Convertir", command=convertir()).pack() # ERREUR!

# remplacez par :

Button(fenetre, text="Convertir", command=convertir).pack() # CORRECT

fenetre.mainloop()

Résultat console :

Traceback (most recent call last):
File "./forum.py", line 21, in <module>
Button(fenetre, text="Convertir", command=convertir()).pack() # ERREUR!
File "./forum.py", line 7, in convertir
resultat.set(bin(int(entree.get())))
ValueError: invalid literal for int() with base 10: ''

Je veux comprendre :

Le paramètre command que l'on trouve dans l'objet Tkinter Button(), comme chez d'autres objets encore, attend le nom d'une fonction - dite callback ou fonction de rappel - et non pas la valeur retour d'une fonction appelée sur place.

En effet, c'est l'objet lui-même qui appellera la fonction callback en temps voulu, vous n'avez donc pas à appeler cette fonction au moment de l'assigner au paramètre command.

Aller plus loin

Si vous vous intéressez au mécanisme général d'une fonction de rappel dite callback, voici comment tout cela fonctionne : une fonction A va effectuer un certain nombre d'opérations, puis, à un moment donné, elle souhaite déléguer l'exécution d'une tâche précise à une autre fonction B, qui se trouve à l'extérieur de cette fonction A. Pour ce faire, la fonction A va accepter en argument de fonction une référence à la fonction B (son nom) de sorte qu'elle puisse faire appel à B le moment venu.

C'est cette référence que l'on appelle une fonction de rappel (callback).

Petit exemple de code générique :

Code source et fichier à télécharger

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

def somme (a, b):
"""
cette fonction retourne la somme de a et b;
"""
return a + b
# end def

def produit (a, b):
"""
cette fonction retourne le produit de a par b;
"""
return a * b
# end def

def formule (a, b):
"""
cette fonction applique une formule à a et b;
vous aurez reconnu ici le calcul de l'hypoténuse;
"""
return (a**2 + b**2)**0.5
# end def

def traiter_liste (callback, liste):
"""
cette fonction parcourt les éléments d'une liste puis
délègue le traitement spécifique à une fonction de rappel
dite 'callback' capable de traiter deux arguments (a, b)
puis de retourner à son tour une valeur unique; la fonction
réduit successivement la liste à une seule valeur finale au
fur et à mesure des résultats obtenus aux itérations
précédentes;
"""
# on crée un itérateur pour parcourir la liste
iterator = iter(liste)
# on essaie de récupérer la première valeur (si existe)
try:
resultat = next(iterator)
# la liste est vide !
except StopIteration:
raise IndexError("impossible de traiter une liste vide")
return None
# end try
# on peut parcourir la liste, à présent:
for item in iterator:
# c'est ici qu'on délègue le traitement à la fonction de rappel
# la fonction doit accepter 2 arguments a et b i.e. callback(a, b)
resultat = callback(resultat, item)
# end for
# on retourne le résultat
return resultat
# end def

# maintenant, testons...
ma_liste = [1, 3, 56, 89, 4, 12, 45, 87, 10, 2, 4, 3, 3, 3, 5]

# annonce
print("pour ma_liste = ", ma_liste)

# faisons la somme de tous les termes de ma_liste:
print("somme de ma_liste = ", traiter_liste(somme, ma_liste))

# calculons le produit de tous les termes de ma_liste:
print("produit de ma_liste = ", traiter_liste(produit, ma_liste))

# appliquons une formule à tous les termes de ma_liste:
print("formule de ma_liste = ", traiter_liste(formule, ma_liste))

# fin du test

Résultat console :

pour ma_liste =  [1, 3, 56, 89, 4, 12, 45, 87, 10, 2, 4, 3, 3, 3, 5]
somme de ma_liste =  327
produit de ma_liste =  30345622272000
formule de ma_liste =  144.88961315429069

Comme vous pouvez le constater, l'intérêt majeur de faire appel à une fonction de rappel callback est de s'offrir la possibilité d'en changer comme bon nous semble, autant de fois que nous le souhaitons sans devoir réécrire le code de la fonction utilisatrice de cette fonction callback.

Ainsi, dans notre exemple, la fonction traiter_liste() peut s'utiliser indifféremment avec les fonctions somme(), produit(), formule() ou n'importe quelle autre fonction que l'on voudra bien se donner la peine de créer, pourvu que cette dernière supporte au moins deux paramètres fixes (a, b) et qu'elle retourne une valeur en résultat.

Les fonctions de rappel représentent un mécanisme particulièrement puissant en programmation.

III-C-7 - Quand je clique sur le bouton, on dirait que mon programme se fige un moment et ensuite tout redevient normal. Pourquoi ?

R : Vous lancez très certainement une boucle qui ne s'inscrit pas dans la boucle événementielle de Tkinter.

Il faut étaler votre boucle dans le temps avec w.after().

Exemple, au lieu d'écrire :

def compter ():
# on efface la sortie console
display.delete("1.0", END)
# et on compte...
for i in range(10):
# petit message censé s'afficher PENDANT le comptage...
display.insert(END, "valeur actuelle : {}\n".format(i))
# on marque une pause exprès...
time.sleep(0.200)
# end for
# end def

Ce qui schématiquement revient à faire :

Écrivez plutôt :

def compter ():
# on désactive le bouton 'compter'
bouton_compter.configure(state=DISABLED)
# on efface la sortie console
display.delete("1.0", END)
# et on compte...
boucle_tkinter(0, 10)
# end def

def boucle_tkinter (debut, fin):
# petit message censé s'afficher PENDANT le comptage...
display.insert(END, "valeur actuelle : {}\n".format(debut))
# màj données
debut += 1
# on étale dans le temps avec w.after()
if debut < fin:
# on marque une pause exprès...
fenetre.after(200, boucle_tkinter, debut, fin)
# c'est fini
else:
# on réactive le bouton 'compter'
bouton_compter.configure(state=NORMAL)
# end if
# end def

Ce qui schématiquement revient à faire :

Je veux comprendre :

Lorsque vous entrez dans la boucle événementielle principale mainloop() de Tkinter, certaines tâches sont différées par la librairie, car elles ne sont pas considérées prioritaires, notamment tout ce qui concerne la mise à jour de l'affichage des widgets.

Si vous lancez une boucle Python for … in … qui prend un peu de temps, vous ne permettrez pas à la boucle mainloop() de faire son travail pendant ce temps-là, notamment pour mettre à jour l'affichage, d'où cette impression de freeze momentané - l'impression que le programme se fige pendant quelques secondes.

Vous avez dès lors deux solutions principales :

1. Vous ne voulez pas vous casser le bonnet et vous insérez un w.update_idletasks() pour forcer la mise à jour de l'affichage des widgets dans votre boucle, tout en la laissant telle quelle, au risque de voir de drôles de choses se passer dans votre programme ;

2. Vous voulez faire les choses bien et vous prenez la peine d'étaler votre boucle dans une boucle Tkinter en utilisant w.after().

La solution forçage avec w.update_idletasks() - peu recommandée :

Code source et fichier à télécharger

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import time
from tkinter import *

# on force la mise à jour de l'affichage des widgets dans le cas ici
# présent avec w.update_idletasks() (à utiliser avec parcimonie)
def compter ():
# on désactive le bouton 'compter'
bouton_compter.configure(state=DISABLED)
# on efface la sortie console
display.delete("1.0", END)
# et on compte...
for i in range(10):
# petit message censé s'afficher PENDANT le comptage...
display.insert(END, "valeur actuelle : {}\n".format(i))
# on force la màj affichage des widgets
fenetre.update_idletasks()
# on marque une pause exprès...
time.sleep(0.2)
# end for
# c'est fini
# on réactive le bouton 'compter'
bouton_compter.configure(state=NORMAL)
# end def

# on crée la fenêtre principale
fenetre = Tk()
fenetre.title("Test de freeze")
fenetre.resizable(width=False, height=False)

# on ajoute des widgets
display = Text(
fenetre,
bg="black",
fg="pale green",
font="monospace 12 bold",
width=40,
height=10,
)
display.pack(padx=5, pady=5)

# on ajoute un bouton 'compter'
bouton_compter = Button(fenetre, text="Compter...", command=compter)
bouton_compter.pack(side=LEFT, padx=10, pady=5)

# on ajoute un bouton 'quitter'
bouton_quitter = Button(fenetre, text="Quitter", command=fenetre.destroy)
bouton_quitter.pack(side=RIGHT, padx=5, pady=5)

# on lance la boucle principale
fenetre.mainloop()

Résultat visuel :

La solution boucle Tkinter avec w.after() - recommandée :

Code source et fichier à télécharger

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from tkinter import *

# la fonction délègue le comptage à une boucle tkinter
def compter ():
# on désactive le bouton 'compter'
bouton_compter.configure(state=DISABLED)
# on efface la sortie console
display.delete("1.0", END)
# et on compte...
boucle_tkinter(0, 10)
# end def

def boucle_tkinter (debut, fin):
# petit message censé s'afficher PENDANT le comptage...
display.insert(END, "valeur actuelle : {}\n".format(debut))
# màj données
debut += 1
# on étale dans le temps avec w.after()
if debut < fin:
# on marque une pause exprès...
fenetre.after(200, boucle_tkinter, debut, fin)
# c'est fini
else:
# on réactive le bouton 'compter'
bouton_compter.configure(state=NORMAL)
# end if
# end def

# on crée la fenêtre principale
fenetre = Tk()
fenetre.title("Test de freeze")
fenetre.resizable(width=False, height=False)

# on ajoute des widgets
display = Text(
fenetre,
bg="black",
fg="pale green",
font="monospace 12 bold",
width=40,
height=10,
)
display.pack(padx=5, pady=5)

# on ajoute un bouton 'compter'
bouton_compter = Button(fenetre, text="Compter...", command=compter)
bouton_compter.pack(side=LEFT, padx=10, pady=5)

# on ajoute un bouton 'quitter'
bouton_quitter = Button(fenetre, text="Quitter", command=fenetre.destroy)
bouton_quitter.pack(side=RIGHT, padx=5, pady=5)

# on lance la boucle principale
fenetre.mainloop()

Résultat visuel :

À vous de voir ce qui vous convient le mieux.

Profitez-en aussi pour jeter un œil à la question « Pour mon jeu, on m'a dit d'utiliser une boucle Tkinter. C'est quoi ? »

III-C-8 - Mes collègues utilisent Pygame avec Tkinter pour leur projet. Je peux faire pareil ?

R : Proposez-leur aussi d'ajouter PyQT et PyGTK, tant qu'à faire !

Peut-être qu'avec quatre environnements graphiques, ils finiront par obtenir en quarante ans ce que vous ferez allègrement avec un seul environnement graphique en un mois ?

Mixer plusieurs librairies graphiques est tout bonnement une très mauvaise idée : concentrez-vous sur Tkinter, apprenez tout d'abord à bien le maîtriser et vous songerez à passer aux vitesses supérieures ensuite seulement.

Connaître Tkinter sur le bout des doigts n'est jamais peine perdue : cette librairie vous dépannera encore bien des fois durant votre carrière professionnelle…

Je veux comprendre :

Chaque librairie, chaque environnement graphique a ses propres règles et son propre fonctionnement. De plus, la plupart des librairies graphiques implémentent une boucle événementielle principale - c'est le cas de Tkinter, de PyGTK et de PyQT, alors que Pygame oblige à écrire sa propre boucle.

Sachant cela, comment allez-vous résoudre le dilemme suivant : si j'utilise au moins deux librairies graphiques en même temps dans mon script, quelle boucle principale dois-je appeler, sachant que si j'en appelle une, je ne peux pas appeler l'autre (ce sont des boucles, hein, pas des passoires) ?

III-C-9 - Pour mon jeu, on m'a dit d'utiliser une boucle Tkinter. C'est quoi ?

R : Tkinter permet de gérer des actions et des animations étalées dans le temps.

Utilisez des fonctions reposant sur w.after() qui s'appellent elles-mêmes en boucle.

Voici comment fonctionne schématiquement une boucle Tkinter :

Documentation Tkinter w.after() : http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/universal.html (en anglais).

Je veux comprendre :

Un jeu est souvent un concept visuel, graphique, qui fait appel à de nombreuses interactions : clics souris, appuis de touches du clavier, entrées joystick et même - de nos jours - événements tactiles ('tap', 'tap-tap', 'glisser', 'écarter', etc.).

Or, pour que ces interactions puissent se répercuter sur le déroulement du jeu, il faut impérativement qu'elles s'inscrivent dans le temps, dans une sorte de boucle dans laquelle la librairie graphique doit toujours avoir le dernier mot - c'est important que la librairie graphique que vous choisirez vous offre cette possibilité.

En effet, la plupart du temps, la librairie graphique gère aussi les événements extérieurs liés aux interactions entre le joueur et le jeu. Il faut donc lui laisser le temps de le faire.

Une bonne illustration valant mille mots, voyons à présent un exemple concret de boucles de gestion des interactions du joueur inscrites dans le temps.

Les boucles suivantes sont extraites du code du jeu Terrain miné que vous retrouverez ci-après.

Boucle temporelle du joueur :

def boucle_joueur ():
"boucle temporelle qui gère les mouvements du joueur"
# init variables globales
global boule_x, boule_y
global tid_joueur
# le joueur a atteint le carré final ?
if carre_x - demi_carre < boule_x < carre_x + demi_carre and \
carre_y - demi_carre < boule_y < carre_y + demi_carre:
# bravo ! c'est gagné !
return bravo()
# end if
# on recherche des collisions
collisions = canvas.find_overlapping(*canvas.bbox(boule_joueur))
# le joueur a heurté autre chose que le carré final ?
if len(collisions) > 1 and carre_final not in collisions:
# aïe ! dommage !
return game_over()
# end if
# tout est OK on déplace le joueur
boule_x = (boule_x + sens_x * pas) % c_width
boule_y = (boule_y + sens_y * pas) % c_height
canvas.coords(
boule_joueur,
boule_x - rayon, boule_y - rayon,
boule_x + rayon, boule_y + rayon,
)
# on boucle à nouveau dans le temps
tid_joueur = fenetre.after(50, boucle_joueur)
# end def

Boucle temporelle des mines :

def boucle_mines ():
"boucle temporelle qui gère la vie des mines"
# init variables globales
global tid_mines
# on pose une mine de temps en temps
poser_mine()
# prochaine mine dans... (millisecondes)
delai = random.randint(1000, 2000)
# on relance la boucle après ce délai
tid_mines = fenetre.after(delai, boucle_mines)
# end def

Le jeu en question : Terrain miné

Utilisons la librairie graphique Tkinter pour notre petit exemple de jeu.

But du jeu : vous devez déplacer une grosse boule vers un carré placé en bas de l'écran. Malheureusement pour vous, le terrain est miné ! Saurez-vous atteindre votre but sans vous faire exploser ?

Version Python2 :

Code source et fichier à télécharger

#!/usr/bin/env python
# -*- coding: utf-8 -*-

# logiciel pour Python2

# (c) 2014 Raphaël SEBAN (tarball69)
# publié sous licence GNU General Public License GPLv3
# cf http://www.gnu.org/licenses/gpl.html

# on demande en premier lieu
# la librairie graphique Tkinter
from Tkinter import *

# on veut aussi utiliser
# les nombres aléatoires
import random

# ------------------- DEBUT DU PROGRAMME -------------------------------

def debut_jeu ():
"le programme commence ici"
# init variables globales
global fenetre, canvas, bouton_jouer
global c_width, c_height, center_x, center_y
# on crée la fenêtre principale
fenetre = Tk()
fenetre.title("Mon super jeu !")
fenetre.resizable(width=False, height=False)
# on ajoute des composants graphiques
canvas = Canvas(fenetre, bg="black", width=400, height=300)
canvas.pack(padx=5, pady=5)
# on met des données courantes en globales
c_width = canvas.winfo_reqwidth()
c_height = canvas.winfo_reqheight()
center_x = c_width // 2
center_y = c_height // 2
# bouton 'jouer'
bouton_jouer = Button(fenetre)
bouton_jouer.pack(side=LEFT, padx=50, pady=5)
# bouton 'quitter'
Button(
fenetre, text="Quitter", command=fenetre.destroy
).pack(side=RIGHT, padx=5, pady=5)
# on affiche une présentation du jeu
presenter_jeu()
# et on lance la
# boucle événementielle principale
fenetre.mainloop()
# end def

def presenter_jeu ():
"on affiche ici une présentation du jeu"
# on commence toujours par effacer le canevas graphique
# qui nous fait office d'écran de jeu
canvas.delete(ALL)
# on affiche un titre
# par rapport au centre de l'écran de jeu
canvas.create_text(
center_x, center_y - 100,
text="Terrain miné",
font="sans 16 bold",
fill="navajowhite2",        # couleur du texte
)
# on affiche des règles du jeu succinctes
canvas.create_text(
center_x, center_y + 30,
text="""\
Règles du jeu:

Vous devez déplacer une grosse boule vers un carré placé en bas de l'écran.
Malheureusement pour vous, le terrain est miné !
Saurez-vous atteindre votre but sans vous faire exploser ?
Utilisez les flèches du clavier pour vous déplacer.
""",
justify=LEFT,
font="sans 10 bold italic",
fill="Pale Goldenrod",
width=0.8 * c_width,        # 80% de la largeur totale
)
# on s'assure que le joueur pourra (re)jouer
bouton_jouer.configure(
text="Jouer !", command=nouvelle_partie, state=NORMAL
)
# end def

def nouvelle_partie ():
"on réinitialise une nouvelle partie ici"
# init variables globales
global tid_joueur, tid_mines
# on autorise le joueur à arrêter la partie quand il le souhaite
bouton_jouer.configure(
text="Stop !", command=arreter_partie, state=NORMAL
)
# on efface le canevas graphique
canvas.delete(ALL)
# on crée l'avatar du joueur (boule)
creer_joueur()
# on crée le carré d'arrivée
creer_carre_final()
# on mine le terrain
miner_terrain()
# on associe les touches fléchées du clavier
# à nos propres fonctions de gestion du mouvement
fenetre.bind_all("<Up>", deplacer_haut)
fenetre.bind_all("<Down>", deplacer_bas)
fenetre.bind_all("<Left>", deplacer_gauche)
fenetre.bind_all("<Right>", deplacer_droite)
# c'est ici qu'on lance les boucles inscrites dans le temps
tid_joueur = fenetre.after(100, boucle_joueur)
# les mines aussi vont avoir leur propre cycle de vie
tid_mines = fenetre.after(1000, boucle_mines)
# end def

def creer_joueur ():
"""
on crée l'avatar du joueur;
ici le joueur est représenté par une grosse boule;
on conserve l'identifiant de l'objet sur le canevas;
ça nous servira plus tard;
on prend des coordonnées au hasard;
"""
# init variables globales
global diametre_boule, rayon
global boule_x, boule_y
global sens_x, sens_y, pas
global boule_joueur
# inits
diametre_boule = 20         # diamètre de la boule
rayon = diametre_boule // 2
boule_x = diametre_boule + random.randint(0, c_width - diametre_boule)
boule_y = diametre_boule + random.randint(0, 10)
sens_x, sens_y = (0, 0)     # sens de déplacement initial
pas = random.randint(2, 5)  # quantité de déplacement (pixels)
# on dessine l'avatar joueur (boule)
boule_joueur = canvas.create_oval(
boule_x - rayon, boule_y - rayon,
boule_x + rayon, boule_y + rayon,
outline="ivory2",       # couleur du contour
width=1,                # épaisseur du trait de contour (pixels)
fill="light sky blue",  # couleur du remplissage
)
# end def

def creer_carre_final ():
"""
on crée le carré final (carré d'arrivée);
on conserve l'identifiant de l'objet sur le canevas;
ça nous servira plus tard;
on prend des coordonnées au hasard;
"""
# init variables globales
global taille_carre, demi_carre
global carre_x, carre_y
global carre_final
# on crée le carré d'arrivée
taille_carre = diametre_boule + 6
demi_carre = taille_carre // 2
carre_x = 10 + random.randint(0, c_width - taille_carre - 10)
carre_y = c_height - taille_carre - random.randint(5, 20)
# on dessine le carré final (arrivée)
carre_final = canvas.create_rectangle(
carre_x - demi_carre, carre_y - demi_carre,
carre_x + demi_carre, carre_y + demi_carre,
outline="ivory",        # couleur du contour
width=1,                # épaisseur du trait de contour (pixels)
fill="palegreen1",      # couleur du remplissage
)
# end def

def miner_terrain ():
"on mine le terrain au début d'une partie"
# on choisit un nombre au hasard
nombre_mines = random.randint(10, 20)
# on boucle pour poser autant de mines
for i in range(nombre_mines):
# on pose une mine au hasard
poser_mine()
# end for
# end def

def poser_mine ():
"""
on pose une mine au hasard, mais jamais dans le voisinage
immédiat du joueur;
"""
# init variables locales
tailles = [8, 10, 12]
couleurs = ["sandy brown", "peru", "sienna1", "coral1"]
couleur_contour = "burlywood1"
# on boucle tant que ce n'est pas bon
# ici, on bouclera cent fois (par précaution)
# et on abandonnera si on n'y arrive pas
for i in range(100):
# on choisit de nouvelles coordonnées au hasard
mine_x = random.randint(0, c_width)
mine_y = random.randint(0, c_height)
# les mines ont une taille variable;
taille_mine = random.choice(tailles)
# on recherche les collisions avec d'autres objets
collisions = canvas.find_overlapping(
*b_rect(mine_x, mine_y, taille_mine)
)
# aucun objet dans le voisinage ?
if not collisions:
# on choisit une couleur parmi une palette
color = random.choice(couleurs)
# on peut poser notre mine !
canvas.create_oval(
b_rect(mine_x, mine_y, taille_mine//2),
outline=couleur_contour,
width=1,        # épaisseur de trait de contour (pixels)
fill=color,     # couleur du remplissage
)
# on remonte la boule joueur
# au premier plan d'affichage
canvas.tag_raise(boule_joueur, ALL)
# c'est bon, on a posé notre mine
# on peut quitter la boucle
break
# end if
# end for
# end def

def b_rect (x, y, size):
"""
retourne les coordonnées (top, left, bottom, right) d'un
rectangle de délimitation autour d'un point P(x, y);
"""
# rectangle d'influence (bounding rectangle)
return (x - size, y - size, x + size, y + size)
# end def

def boucle_joueur ():
"boucle temporelle qui gère les mouvements du joueur"
# init variables globales
global boule_x, boule_y
global tid_joueur
# le joueur a atteint le carré final ?
if carre_x - demi_carre < boule_x < carre_x + demi_carre and \
carre_y - demi_carre < boule_y < carre_y + demi_carre:
# bravo ! c'est gagné !
return bravo()
# end if
# on recherche des collisions
collisions = canvas.find_overlapping(*canvas.bbox(boule_joueur))
# le joueur a heurté autre chose que le carré final ?
if len(collisions) > 1 and carre_final not in collisions:
# aïe ! dommage !
return game_over()
# end if
# tout est OK on déplace le joueur
boule_x = (boule_x + sens_x * pas) % c_width
boule_y = (boule_y + sens_y * pas) % c_height
canvas.coords(
boule_joueur,
boule_x - rayon, boule_y - rayon,
boule_x + rayon, boule_y + rayon,
)
# on boucle à nouveau dans le temps
tid_joueur = fenetre.after(50, boucle_joueur)
# end def

def boucle_mines ():
"boucle temporelle qui gère la vie des mines"
# init variables globales
global tid_mines
# on pose une mine de temps en temps
poser_mine()
# prochaine mine dans... (millisecondes)
delai = random.randint(1000, 2000)
# on relance la boucle après ce délai
tid_mines = fenetre.after(delai, boucle_mines)
# end def

def bravo ():
"super ! le joueur a réussi !"
# on affiche l'écran final
ecran_final(
titre="BRAVO",
sous_titre="C'est gagné !",
couleur_titre="lemon chiffon",
couleur_sous_titre="pale green",
)
# end def

def game_over ():
"c'est ballot ! le joueur a perdu !"
# on affiche l'écran final
ecran_final(
titre="GAME OVER",
sous_titre="C'est perdu !",
couleur_titre="firebrick1",
couleur_sous_titre="floral white",
)
# end def

def arreter_partie ():
"le joueur souhaite arrêter la partie"
# on affiche l'écran final
ecran_final(
titre="ABANDON",
sous_titre="Partie annulée !",
couleur_titre="gold",
couleur_sous_titre="light grey",
)
# end def

def ecran_final (**kw):
"on effectue ici des opérations communes à tous les écrans finaux"
# on bloque les clics intempestifs sur le bouton 'jouer'
bouton_jouer.configure(state=DISABLED)
# on stoppe les boucles temporelles qui pourraient
# éventuellement être encore en cours d'exécution
arreter_boucles()
# on efface le canevas graphique
canvas.delete(ALL)
# on récupère le titre et sa couleur
titre = kw.get("titre") or ""
couleur = kw.get("couleur_titre") or "white"
# on affiche le titre
canvas.create_text(
center_x, center_y - 40,
text=titre,
font="sans 36 bold",
fill=couleur,
)
# on récupère le sous-titre et sa couleur
sous_titre = kw.get("sous_titre") or ""
couleur = kw.get("couleur_sous_titre") or "white"
# on affiche le sous-titre
canvas.create_text(
center_x, center_y,
text=sous_titre,
font="sans 16 bold italic",
fill=couleur,
)
# on relance presenter_jeu() après @delai
# avec toutefois un minimum de 1 seconde
delai = kw.get("delai") or 2000     # valeur par défaut
fenetre.after(max(1000, delai), presenter_jeu)
# end def

def arreter_boucles ():
"on arrête les boucles temporelles susceptibles de tourner encore"
# init variables globales
global tid_joueur, tid_mines
# stooop !
fenetre.after_cancel(tid_joueur)
fenetre.after_cancel(tid_mines)
# RAZ thread ids
tid_joueur = tid_mines = 0
# end def

def deplacer_haut (event=None):
"on déplace l'avatar du joueur vers le haut"
# init variables globales
global sens_x, sens_y
# grâce à la boucle du jeu,
# nous n'avons pas besoin de plus que ça
sens_x, sens_y = (0, -1)
# end def

def deplacer_bas (event=None):
"on déplace l'avatar du joueur vers le bas"
# init variables globales
global sens_x, sens_y
# grâce à la boucle du jeu,
# nous n'avons pas besoin de plus que ça
sens_x, sens_y = (0, 1)
# end def

def deplacer_gauche (event=None):
"on déplace l'avatar du joueur vers la gauche"
# init variables globales
global sens_x, sens_y
# grâce à la boucle du jeu,
# nous n'avons pas besoin de plus que ça
sens_x, sens_y = (-1, 0)
# end def

def deplacer_droite (event=None):
"on déplace l'avatar du joueur vers la droite"
# init variables globales
global sens_x, sens_y
# grâce à la boucle du jeu,
# nous n'avons pas besoin de plus que ça
sens_x, sens_y = (1, 0)
# end def

# ------------------- FIN DU PROGRAMME ---------------------------------

# ce script est automatiquement lancé grâce à ceci :

# /!\ à connaître par coeur /!\
if __name__ == "__main__":
# lancer le programme
debut_jeu()
# end if

Version Python3 :

Code source et fichier à télécharger

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# logiciel pour Python3

# (c) 2014 Raphaël SEBAN (tarball69)
# publié sous licence GNU General Public License GPLv3
# cf http://www.gnu.org/licenses/gpl.html

# on demande en premier lieu
# la librairie graphique Tkinter
from tkinter import *

# on veut aussi utiliser
# les nombres aléatoires
import random

# ------------------- DEBUT DU PROGRAMME -------------------------------

def debut_jeu ():
"le programme commence ici"
# init variables globales
global fenetre, canvas, bouton_jouer
global c_width, c_height, center_x, center_y
# on crée la fenêtre principale
fenetre = Tk()
fenetre.title("Mon super jeu !")
fenetre.resizable(width=False, height=False)
# on ajoute des composants graphiques
canvas = Canvas(fenetre, bg="black", width=400, height=300)
canvas.pack(padx=5, pady=5)
# on met des données courantes en globales
c_width = canvas.winfo_reqwidth()
c_height = canvas.winfo_reqheight()
center_x = c_width // 2
center_y = c_height // 2
# bouton 'jouer'
bouton_jouer = Button(fenetre)
bouton_jouer.pack(side=LEFT, padx=50, pady=5)
# bouton 'quitter'
Button(
fenetre, text="Quitter", command=fenetre.destroy
).pack(side=RIGHT, padx=5, pady=5)
# on affiche une présentation du jeu
presenter_jeu()
# et on lance la
# boucle événementielle principale
fenetre.mainloop()
# end def

def presenter_jeu ():
"on affiche ici une présentation du jeu"
# on commence toujours par effacer le canevas graphique
# qui nous fait office d'écran de jeu
canvas.delete(ALL)
# on affiche un titre
# par rapport au centre de l'écran de jeu
canvas.create_text(
center_x, center_y - 100,
text="Terrain miné",
font="sans 16 bold",
fill="navajowhite2",        # couleur du texte
)
# on affiche des règles du jeu succinctes
canvas.create_text(
center_x, center_y + 30,
text="""\
Règles du jeu:

Vous devez déplacer une grosse boule vers un carré placé en bas de l'écran.
Malheureusement pour vous, le terrain est miné !
Saurez-vous atteindre votre but sans vous faire exploser ?
Utilisez les flèches du clavier pour vous déplacer.
""",
justify=LEFT,
font="sans 10 bold italic",
fill="Pale Goldenrod",
width=0.8 * c_width,        # 80% de la largeur totale
)
# on s'assure que le joueur pourra (re)jouer
bouton_jouer.configure(
text="Jouer !", command=nouvelle_partie, state=NORMAL
)
# end def

def nouvelle_partie ():
"on réinitialise une nouvelle partie ici"
# init variables globales
global tid_joueur, tid_mines
# on autorise le joueur à arrêter la partie quand il le souhaite
bouton_jouer.configure(
text="Stop !", command=arreter_partie, state=NORMAL
)
# on efface le canevas graphique
canvas.delete(ALL)
# on crée l'avatar du joueur (boule)
creer_joueur()
# on crée le carré d'arrivée
creer_carre_final()
# on mine le terrain
miner_terrain()
# on associe les touches fléchées du clavier
# à nos propres fonctions de gestion du mouvement
fenetre.bind_all("<Up>", deplacer_haut)
fenetre.bind_all("<Down>", deplacer_bas)
fenetre.bind_all("<Left>", deplacer_gauche)
fenetre.bind_all("<Right>", deplacer_droite)
# c'est ici qu'on lance les boucles inscrites dans le temps
tid_joueur = fenetre.after(100, boucle_joueur)
# les mines aussi vont avoir leur propre cycle de vie
tid_mines = fenetre.after(1000, boucle_mines)
# end def

def creer_joueur ():
"""
on crée l'avatar du joueur;
ici le joueur est représenté par une grosse boule;
on conserve l'identifiant de l'objet sur le canevas;
ça nous servira plus tard;
on prend des coordonnées au hasard;
"""
# init variables globales
global diametre_boule, rayon
global boule_x, boule_y
global sens_x, sens_y, pas
global boule_joueur
# inits
diametre_boule = 20         # diamètre de la boule
rayon = diametre_boule // 2
boule_x = diametre_boule + random.randint(0, c_width - diametre_boule)
boule_y = diametre_boule + random.randint(0, 10)
sens_x, sens_y = (0, 0)     # sens de déplacement initial
pas = random.randint(2, 5)  # quantité de déplacement (pixels)
# on dessine l'avatar joueur (boule)
boule_joueur = canvas.create_oval(
boule_x - rayon, boule_y - rayon,
boule_x + rayon, boule_y + rayon,
outline="ivory2",       # couleur du contour
width=1,                # épaisseur du trait de contour (pixels)
fill="light sky blue",  # couleur du remplissage
)
# end def

def creer_carre_final ():
"""
on crée le carré final (carré d'arrivée);
on conserve l'identifiant de l'objet sur le canevas;
ça nous servira plus tard;
on prend des coordonnées au hasard;
"""
# init variables globales
global taille_carre, demi_carre
global carre_x, carre_y
global carre_final
# on crée le carré d'arrivée
taille_carre = diametre_boule + 6
demi_carre = taille_carre // 2
carre_x = 10 + random.randint(0, c_width - taille_carre - 10)
carre_y = c_height - taille_carre - random.randint(5, 20)
# on dessine le carré final (arrivée)
carre_final = canvas.create_rectangle(
carre_x - demi_carre, carre_y - demi_carre,
carre_x + demi_carre, carre_y + demi_carre,
outline="ivory",        # couleur du contour
width=1,                # épaisseur du trait de contour (pixels)
fill="palegreen1",      # couleur du remplissage
)
# end def

def miner_terrain ():
"on mine le terrain au début d'une partie"
# on choisit un nombre au hasard
nombre_mines = random.randint(10, 20)
# on boucle pour poser autant de mines
for i in range(nombre_mines):
# on pose une mine au hasard
poser_mine()
# end for
# end def

def poser_mine ():
"""
on pose une mine au hasard, mais jamais dans le voisinage
immédiat du joueur;
"""
# init variables locales
tailles = [8, 10, 12]
couleurs = ["sandy brown", "peru", "sienna1", "coral1"]
couleur_contour = "burlywood1"
# on boucle tant que ce n'est pas bon
# ici, on bouclera cent fois (par précaution)
# et on abandonnera si on n'y arrive pas
for i in range(100):
# on choisit de nouvelles coordonnées au hasard
mine_x = random.randint(0, c_width)
mine_y = random.randint(0, c_height)
# les mines ont une taille variable;
taille_mine = random.choice(tailles)
# on recherche les collisions avec d'autres objets
collisions = canvas.find_overlapping(
*b_rect(mine_x, mine_y, taille_mine)
)
# aucun objet dans le voisinage ?
if not collisions:
# on choisit une couleur parmi une palette
color = random.choice(couleurs)
# on peut poser notre mine !
canvas.create_oval(
b_rect(mine_x, mine_y, taille_mine//2),
outline=couleur_contour,
width=1,        # épaisseur de trait de contour (pixels)
fill=color,     # couleur du remplissage
)
# on remonte la boule joueur
# au premier plan d'affichage
canvas.tag_raise(boule_joueur, ALL)
# c'est bon, on a posé notre mine
# on peut quitter la boucle
break
# end if
# end for
# end def

def b_rect (x, y, size):
"""
retourne les coordonnées (top, left, bottom, right) d'un
rectangle de délimitation autour d'un point P(x, y);
"""
# rectangle d'influence (bounding rectangle)
return (x - size, y - size, x + size, y + size)
# end def

def boucle_joueur ():
"boucle temporelle qui gère les mouvements du joueur"
# init variables globales
global boule_x, boule_y
global tid_joueur
# le joueur a atteint le carré final ?
if carre_x - demi_carre < boule_x < carre_x + demi_carre and \
carre_y - demi_carre < boule_y < carre_y + demi_carre:
# bravo ! c'est gagné !
return bravo()
# end if
# on recherche des collisions
collisions = canvas.find_overlapping(*canvas.bbox(boule_joueur))
# le joueur a heurté autre chose que le carré final ?
if len(collisions) > 1 and carre_final not in collisions:
# aïe ! dommage !
return game_over()
# end if
# tout est OK on déplace le joueur
boule_x = (boule_x + sens_x * pas) % c_width
boule_y = (boule_y + sens_y * pas) % c_height
canvas.coords(
boule_joueur,
boule_x - rayon, boule_y - rayon,
boule_x + rayon, boule_y + rayon,
)
# on boucle à nouveau dans le temps
tid_joueur = fenetre.after(50, boucle_joueur)
# end def

def boucle_mines ():
"boucle temporelle qui gère la vie des mines"
# init variables globales
global tid_mines
# on pose une mine de temps en temps
poser_mine()
# prochaine mine dans... (millisecondes)
delai = random.randint(1000, 2000)
# on relance la boucle après ce délai
tid_mines = fenetre.after(delai, boucle_mines)
# end def

def bravo ():
"super ! le joueur a réussi !"
# on affiche l'écran final
ecran_final(
titre="BRAVO",
sous_titre="C'est gagné !",
couleur_titre="lemon chiffon",
couleur_sous_titre="pale green",
)
# end def

def game_over ():
"c'est ballot ! le joueur a perdu !"
# on affiche l'écran final
ecran_final(
titre="GAME OVER",
sous_titre="C'est perdu !",
couleur_titre="firebrick1",
couleur_sous_titre="floral white",
)
# end def

def arreter_partie ():
"le joueur souhaite arrêter la partie"
# on affiche l'écran final
ecran_final(
titre="ABANDON",
sous_titre="Partie annulée !",
couleur_titre="gold",
couleur_sous_titre="light grey",
)
# end def

def ecran_final (**kw):
"on effectue ici des opérations communes à tous les écrans finaux"
# on bloque les clics intempestifs sur le bouton 'jouer'
bouton_jouer.configure(state=DISABLED)
# on stoppe les boucles temporelles qui pourraient
# éventuellement être encore en cours d'exécution
arreter_boucles()
# on efface le canevas graphique
canvas.delete(ALL)
# on récupère le titre et sa couleur
titre = kw.get("titre") or ""
couleur = kw.get("couleur_titre") or "white"
# on affiche le titre
canvas.create_text(
center_x, center_y - 40,
text=titre,
font="sans 36 bold",
fill=couleur,
)
# on récupère le sous-titre et sa couleur
sous_titre = kw.get("sous_titre") or ""
couleur = kw.get("couleur_sous_titre") or "white"
# on affiche le sous-titre
canvas.create_text(
center_x, center_y,
text=sous_titre,
font="sans 16 bold italic",
fill=couleur,
)
# on relance presenter_jeu() après @delai
# avec toutefois un minimum de 1 seconde
delai = kw.get("delai") or 2000     # valeur par défaut
fenetre.after(max(1000, delai), presenter_jeu)
# end def

def arreter_boucles ():
"on arrête les boucles temporelles susceptibles de tourner encore"
# init variables globales
global tid_joueur, tid_mines
# stooop !
fenetre.after_cancel(tid_joueur)
fenetre.after_cancel(tid_mines)
# RAZ thread ids
tid_joueur = tid_mines = 0
# end def

def deplacer_haut (event=None):
"on déplace l'avatar du joueur vers le haut"
# init variables globales
global sens_x, sens_y
# grâce à la boucle du jeu,
# nous n'avons pas besoin de plus que ça
sens_x, sens_y = (0, -1)
# end def

def deplacer_bas (event=None):
"on déplace l'avatar du joueur vers le bas"
# init variables globales
global sens_x, sens_y
# grâce à la boucle du jeu,
# nous n'avons pas besoin de plus que ça
sens_x, sens_y = (0, 1)
# end def

def deplacer_gauche (event=None):
"on déplace l'avatar du joueur vers la gauche"
# init variables globales
global sens_x, sens_y
# grâce à la boucle du jeu,
# nous n'avons pas besoin de plus que ça
sens_x, sens_y = (-1, 0)
# end def

def deplacer_droite (event=None):
"on déplace l'avatar du joueur vers la droite"
# init variables globales
global sens_x, sens_y
# grâce à la boucle du jeu,
# nous n'avons pas besoin de plus que ça
sens_x, sens_y = (1, 0)
# end def

# ------------------- FIN DU PROGRAMME ---------------------------------

# ce script est automatiquement lancé grâce à ceci :

# /!\ à connaître par coeur /!\
if __name__ == "__main__":
# lancer le programme
debut_jeu()
# end if

Documentation Tkinter : http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/universal.html (en anglais).

III-C-10 - J'ai plusieurs boutons qui doivent faire la même chose à une valeur près. Comment puis-je faire ?

R : Utilisez une fermeture dans le paramètre command de vos boutons avec la valeur souhaitée.

Une fermeture est une fonction qui retourne en résultat une autre fonction.

Un bon exemple valant mille mots, voici un cas d'école : une interface GUI de calculette.

Code source et fichier à télécharger

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from tkinter import *

# on définit une fermeture
def key_pad (key_value):
# on définit ici la vraie fonction qui fait le traitement attendu;
# comme cette fonction sera appelée par le bouton cliquable
# elle ne prend AUCUN ARGUMENT dans sa définition;
# en revanche, elle va se servir des arguments stockés dans
# la fermeture (c'est tout l'intérêt du truc);
def mon_traitement ():
# on exploite l'argument stocké par la fermeture
# touche pavé numérique '0' à '9':
if key_value in range(10):
# on ajoute un chiffre à l'affichage
display.set(display.get() + str(key_value))
# touche 'AC' - 'All Clear' - tout effacer
elif key_value == 'AC':
# on efface tout
display.set("")
# ...etc...
# end if
# end def
# retourne une fonction en résultat
return mon_traitement
# end def

# on crée la fenêtre principale
fenetre = Tk()
fenetre.title("Calculette")
fenetre.resizable(width=False, height=False)

# on ajoute des widgets
display = StringVar()
Label(
fenetre,
textvariable=display,
anchor=E,
bg="pale green",
font="sans 14 bold",
relief=SUNKEN,
width=10,
).pack(padx=5, pady=5)

# on crée un conteneur à boutons
frame = Frame(fenetre)
frame.pack(padx=5, pady=5)

# options de grid()
opts = dict(sticky=NW+SE)

# on ajoute les boutons utilisant la fermeture
Button(
frame,
text="C",
command=key_pad('C')
).grid(row=0, column=0, **opts)

Button(
frame,
text="AC",
command=key_pad('AC')
).grid(row=0, column=1, columnspan=2, **opts)

Button(frame, text="9", command=key_pad(9)).grid(row=1, column=2, **opts)
Button(frame, text="8", command=key_pad(8)).grid(row=1, column=1, **opts)
Button(frame, text="7", command=key_pad(7)).grid(row=1, column=0, **opts)
Button(frame, text="6", command=key_pad(6)).grid(row=2, column=2, **opts)
Button(frame, text="5", command=key_pad(5)).grid(row=2, column=1, **opts)
Button(frame, text="4", command=key_pad(4)).grid(row=2, column=0, **opts)
Button(frame, text="3", command=key_pad(3)).grid(row=3, column=2, **opts)
Button(frame, text="2", command=key_pad(2)).grid(row=3, column=1, **opts)
Button(frame, text="1", command=key_pad(1)).grid(row=3, column=0, **opts)

Button(
frame,
text="0",
command=key_pad(0)
).grid(row=4, column=0, columnspan=2, **opts)

Button(
frame,
text=".",
command=key_pad('.')
).grid(row=4, column=2, **opts)

# on ajoute un bouton quitter
Button(
fenetre,
text="Quitter",
command=fenetre.destroy,
).pack(padx=5, pady=5)

# on lance la boucle principale
fenetre.mainloop()

Résultat visuel :

Je veux comprendre :

Par défaut, le paramètre command d'un bouton cliquable Tkinter attend en argument le nom d'une fonction de rappel - dite callback - à rappeler plus tard, lorsque l'utilisateur cliquera sur le bouton.

Ce que vous aimeriez faire, c'est appeler une même fonction, mais avec différentes valeurs en argument de cette fonction. Or le paramètre command d'un bouton cliquable ne permet pas cela par défaut, vu qu'il attend un nom de fonction et non pas l'appel d'une fonction avec son argument.

Il existe toutefois un contournement classique à ce problème : la fermeture.

Une fermeture, c'est - entre autres - une fonction qui retourne en résultat une autre fonction.

Pourquoi faire, me direz-vous ?

Toute la finesse du concept tient dans le fait qu'une fermeture peut conserver des arguments - et des valeurs - en mémoire et les réutiliser le moment venu.

Pour utiliser plusieurs valeurs avec une même fonction dans le paramètre command d'un bouton cliquable, nous écrirons donc une fermeture comme ceci :

Code source et fichier à télécharger

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from tkinter import *

# on définit une fermeture
def fermeture (valeur):
# on définit ici la vraie fonction qui fait le traitement attendu;
# comme cette fonction sera appelée par le bouton cliquable
# elle ne prend AUCUN ARGUMENT dans sa définition;
# en revanche, elle va se servir des arguments stockés dans
# la fermeture (c'est tout l'intérêt du truc);
def mon_traitement ():
# on exploite l'argument stocké par la fermeture
resultat.set("Vous avez cliqué sur le bouton '{}'".format(valeur))
# end def
# retourne une fonction en résultat
return mon_traitement
# end def

# on crée la fenêtre principale
fenetre = Tk()
fenetre.title("Rigolure")
fenetre.resizable(width=False, height=False)

# on ajoute des widgets
resultat = StringVar()
resultat.set("Cliquez sur un bouton au choix")
Label(fenetre, textvariable=resultat).pack(padx=5, pady=10)
Label(fenetre, text="Adieu...").pack(anchor=W, padx=5)

# on crée un conteneur à boutons (pour la présentation)
frame = Frame(fenetre)
frame.pack(pady=10)

# on ajoute les boutons utilisant la fermeture
Button(
frame,
text="Veaux",
command=fermeture('Veaux'),
).pack(side=LEFT, padx=5)

Button(
frame,
text="Vaches",
command=fermeture('Vaches'),
).pack(side=LEFT, padx=5)

Button(
frame,
text="Cochons",
command=fermeture('Cochons'),
).pack(side=LEFT, padx=5)

# on ajoute un bouton quitter
Button(
fenetre,
text="Quitter",
command=fenetre.destroy,
).pack(side=RIGHT, padx=5, pady=5)

# on lance la boucle principale
fenetre.mainloop()

Résultat visuel :

Aller plus loin

Cette partie peut s'avérer particulièrement ardue pour le niveau Terminale S option ISN ; elle s'adresse en priorité aux passionnés de code qui souhaitent vraiment approfondir le sujet ; passez, si vous sentez qu'elle vous embrouille le cerveau plus qu'autre chose.

Dans le cas de boutons cliquables, il peut parfois être nettement plus simple de recourir aux fonctions anonymes lambda :

Code source et fichier à télécharger

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from tkinter import *

# on définit une fonction qui fait le traitement voulu
def mon_traitement (valeur):
# traitement attendu :
resultat.set("Vous avez cliqué sur le bouton '{}'".format(valeur))
# end def

# on crée la fenêtre principale
fenetre = Tk()
fenetre.title("Rigolure")
fenetre.resizable(width=False, height=False)

# on ajoute des widgets
resultat = StringVar()
resultat.set("Cliquez sur un bouton au choix")
Label(fenetre, textvariable=resultat).pack(padx=5, pady=10)
Label(fenetre, text="Adieu...").pack(anchor=W, padx=5)

# on crée un conteneur à boutons (pour la présentation)
frame = Frame(fenetre)
frame.pack(pady=10)

# on ajoute les boutons utilisant la fonction lambda
Button(
frame,
text="Veaux",
command=lambda: mon_traitement('Veaux'),
).pack(side=LEFT, padx=5)

Button(
frame,
text="Vaches",
command=lambda: mon_traitement('Vaches'),
).pack(side=LEFT, padx=5)

Button(
frame,
text="Cochons",
command=lambda: mon_traitement('Cochons'),
).pack(side=LEFT, padx=5)

# on ajoute un bouton quitter
Button(
fenetre,
text="Quitter",
command=fenetre.destroy,
).pack(side=RIGHT, padx=5, pady=5)

# on lance la boucle principale
fenetre.mainloop()

Résultat visuel :

Une fonction anonyme lambda est une fonction qui n'est pas identifiée formellement par un nom. Elle doit donc être déclarée dans la foulée d'une expression Python, puis être référencée par au moins une variable ou un paramètre de fonction - comme dans le cas du paramètre command d'un bouton cliquable Tkinter, par exemple.

Une fonction lambda est une fonction anonyme ; elle n'a pas de nom, mais elle accepte des paramètres - comme toutes les autres fonctions - et se résume à une seule ligne de code : la valeur retour.

Dans notre exemple ci-dessus, la fonction lambda : mon_traitement(…) remplace en quelque sorte la fermeture fermeture(valeur) en stockant directement dans sa définition le résultat de l'appel de la fonction mon_traitement(valeur) avec son argument valeur immédiatement valorisé i.e. à 'Veaux', 'Vaches' ou 'Cochons'. Comme c'est lambda qui sera appelée lorsque le bouton sera cliqué et non pas mon_traitement(…), le contournement est conforme à la définition du callback attendu par le paramètre command du bouton cliquable.

Notez une particularité de lambda : vous ne pouvez inscrire dans sa définition qu'une expression correspondant à la valeur retour de cette fonction. En effet, une fonction lambda n'accepte qu'une seule ligne de code dans sa définition : la valeur retour.

La syntaxe générique d'une fonction lambda se résume ainsi :

ma_fonction = lambda paramètres: valeur_retour

et correspond à la définition canonique :

def ma_fonction (paramètres):
return valeur_retour

Quelques exemples concrets :

cube = lambda x: x**3 # correspond à def cube(x): return x**3
print(cube(10))

somme = lambda x, y: x + y # correspond à def somme(x, y): return x + y
print(somme(3, 4))

somme_serie = lambda n: n * (n + 1) / 2 # somme de la série 1+2+3+...+n
print(somme_serie(100))

Résultat console :

1000
7
5050.0

III-C-11 - Je voudrais utiliser une seule Scrollbar pour deux canvas en même temps. C'est possible, ça ?

R : Oui, c'est le principe de la fonction dispatch.

Il suffit d'appeler une fonction qui s'occupera de relayer les commandes de l'objet Scrollbar() aux deux canevas.

Exemple :

Code source et fichier à télécharger

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from tkinter import *

# on crée une fonction de dispatch
def dispatch_yview (*args):
"""
cette fonction dispatche les commandes de la scrollbar
VERTICALE entre les deux canevas;
"""
canvas1.yview(*args)
canvas2.yview(*args)
# end def

# on crée la fenêtre principale
fenetre = Tk()

# on ajoute des widgets

# options de texte
txt_options = dict(
text="Hello, world!",
font="sans 16 bold",
fill="indian red",
)

# canvas 1
canvas1 = Canvas(fenetre, bg="pale goldenrod", width=200, height=200)
canvas1.create_text(100, 100, **txt_options)
canvas1.grid(row=0, column=0, sticky=NW+SE)

# canvas 2
canvas2 = Canvas(fenetre, bg="sky blue", width=200, height=200)
canvas2.create_text(100, 100, **txt_options)
canvas2.grid(row=0, column=1, sticky=NW+SE)

# horizontal scrollbar canvas 1
hbar1 = Scrollbar(fenetre, orient=HORIZONTAL)
hbar1.grid(row=1, column=0, sticky=EW)

# horizontal scrollbar canvas 2
hbar2 = Scrollbar(fenetre, orient=HORIZONTAL)
hbar2.grid(row=1, column=1, sticky=EW)

# VERTICAL scrollbar
vbar = Scrollbar(fenetre, orient=VERTICAL)
vbar.grid(row=0, column=2, sticky=NS)

# on rend les canevas redimensionnables
fenetre.rowconfigure(0, weight=1)
fenetre.columnconfigure(0, weight=1)
fenetre.columnconfigure(1, weight=1)

# on connecte les scrollbars aux canevas
canvas1.configure(
xscrollcommand=hbar1.set,
yscrollcommand=vbar.set,
scrollregion=(0, 0, 800, 600),
)
canvas2.configure(
xscrollcommand=hbar2.set,
yscrollcommand=vbar.set,
scrollregion=(0, 0, 800, 600),
)
hbar1.configure(command=canvas1.xview)
hbar2.configure(command=canvas2.xview)

# on relie la fonction de dispatch ici
vbar.configure(command=dispatch_yview)

# on lance la boucle principale
fenetre.mainloop()

Résultat visuel :

Je veux comprendre :

Cette partie peut s'avérer particulièrement ardue pour le niveau Terminale S option ISN ; elle s'adresse en priorité aux passionnés de code qui souhaitent vraiment approfondir le sujet ; passez, si vous sentez qu'elle vous embrouille le cerveau plus qu'autre chose.

En temps normal, on définit la connexion entre un objet Scrollbar() et un Canvas() comme ceci :

Code source et fichier à télécharger

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from tkinter import *

# on crée la fenêtre principale
fenetre = Tk()
fenetre.rowconfigure(0, weight=1)

canvas = Canvas(fenetre, bg="white", width=200, height=100)
canvas.create_text(100, 50, text="Hello good people!")
canvas.grid(row=0, column=0, sticky=NW+SE)

vbar = Scrollbar(fenetre, orient=VERTICAL)
vbar.grid(row=0, column=1, sticky=NS)

# connexion canvas - scrollbar

canvas.configure(
yscrollcommand=vbar.set,
scrollregion=(0, 0, 200, 300),
)

vbar.configure(command=canvas.yview)

# on lance la boucle principale
fenetre.mainloop()

Résultat visuel :

Dans notre exemple, on assigne une fonction de rappel callback vbar.set au paramètre yscrollcommand de l'objet canvas : les changements de taille du canevas ou de son paramètre scrollregion pourront se répercuter dans l'objet vbar en modifiant la taille de l'ascenseur et la gestion du défilement.

Ensuite, avec la ligne de code :

vbar.configure(command=canvas.yview)

on assigne une fonction de rappel callback canvas.yview au paramètre command de l'objet vbar : lorsque l'utilisateur actionne l'ascenseur, l'objet vbar appelle canvas.yview() avec les arguments adéquats pour indiquer au canevas de combien il doit défiler dans un sens ou dans l'autre.

Si l'utilisateur manipule directement l'ascenseur, vbar appelle :

canvas.yview(tkinter.MOVETO, ratio)

avec ratio compris entre 0.0 et 1.0, 0.0 étant le sommet du canvas et 1.0 le bas.

En revanche, si l'utilisateur clique sur les flèches ou sur le creux - c'est-à-dire la zone grisée entre l'ascenseur et les flèches - vbar appelle :

canvas.yview(tkinter.SCROLL, n, what)

avec n la quantité et what l'unité (tkinter.UNITS ou tkinter.PAGES), exemple :

# l'utilisateur clique dans le creux (zone grisée) d'une Scrollbar
canvas.yview(tkinter.SCROLL, 1, tkinter.PAGES)

# l'utilisateur clique sur la flèche d'une Scrollbar
canvas.yview(tkinter.SCROLL, 1, tkinter.UNITS)

Vous pouvez faire une petite expérience amusante pour voir ce qui se passe en vrai lorsque vous manipulez un objet Scrollbar() :

Code source et fichier à télécharger

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from tkinter import *

def my_yview (*args):
"""
cette fonction s'intercale entre l'objet Scrollbar() et
l'objet Canvas() afin d'étudier ce qui se passe lorsque
l'utilisateur manipule l'objet Scrollbar();
"""
# on affiche dans la console les arguments passés
# normalement au callback canvas.yview
print(*args)
# on relaie l'info au canvas
# pour s'assurer du bon fonctionnement
canvas.yview(*args)
# end def

# on crée la fenêtre principale
fenetre = Tk()
fenetre.rowconfigure(0, weight=1)

canvas = Canvas(fenetre, bg="white", width=200, height=100)
canvas.create_text(100, 50, text="Hello good people!")
canvas.grid(row=0, column=0, sticky=NW+SE)

vbar = Scrollbar(fenetre, orient=VERTICAL)
vbar.grid(row=0, column=1, sticky=NS)

# connexion canvas - scrollbar

canvas.configure(
yscrollcommand=vbar.set,
scrollregion=(0, 0, 200, 300),
)

vbar.configure(command=my_yview)

# on lance la boucle principale
fenetre.mainloop()

Résultat visuel :

Pour finir, lorsque vous souhaitez faire défiler plusieurs objets Canvas() en même temps avec un seul objet Scrollbar(), il vous suffit d'écrire une fonction dispatch qui s'intercale entre l'objet Scrollbar() et tous les Canvas() concernés :

# on crée une fonction de dispatch
def dispatch_yview (*args):
"""
cette fonction dispatche les commandes de la scrollbar
VERTICALE entre tous les canevas concernés;
"""
canvas1.yview(*args)
canvas2.yview(*args)
canvas3.yview(*args)
…
canvasN.yview(*args)
# end def

…code…

# on relie la fonction de dispatch ici
vbar.configure(command=dispatch_yview)

Aller plus loin

Si vous souhaitez approfondir la question, jetez donc un œil attentif à la documentation Tkinter officielle (pages en anglais) :

- objet Tkinter Canvas() ;

- méthodes de l'objet Canvas() dont xview() et yview() ;

- objet Tkinter Scrollbar() ;

- paramètre command callback de l'objet Scrollbar().

III-C-12 - Les Scrollbars de Tkinter sont vraiment trop moches ! Y a pas mieux ?

R : Si, mais à condition d'avoir une version récente de Python.

Et donc de Tkinter ! Vous devez avoir au moins Tcl/Tk 8.5 pour en bénéficier.

Vous pouvez utiliser les objets Scrollbar() de la librairie additionnelle ttk fournie avec Tkinter.

Exemple Python2 :

Code source et fichier à télécharger

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from Tkinter import *
import ttk

# on crée la fenêtre principale
fenetre = Tk()

# on crée un conteneur pour bien présenter
frame = Frame(fenetre)
frame.pack(expand=1, fill=BOTH, padx=2, pady=2)

# on ajoute des widgets
canvas = Canvas(frame, bg="white", width=200, height=150)
canvas.create_text(100, 75, text="Hello good people!")
hbar = ttk.Scrollbar(frame, orient=HORIZONTAL)
vbar = ttk.Scrollbar(frame, orient=VERTICAL)

# on ajoute une petite poignée de redimensionnement
# histoire de faire joli
sizegrip = ttk.Sizegrip(frame)

# on connecte les scrollbars au canevas
canvas.configure(
xscrollcommand=hbar.set,
yscrollcommand=vbar.set,
scrollregion=(0, 0, 800, 600),
)
hbar.configure(command=canvas.xview)
vbar.configure(command=canvas.yview)

# on place les widgets avec grid()
canvas.grid(row=0, column=0, sticky=NW+SE)
hbar.grid(row=1, column=0, sticky=EW)
vbar.grid(row=0, column=1, sticky=NS)
sizegrip.grid(row=1, column=1)

# on rend le canevas redimensionnable
frame.rowconfigure(0, weight=1)
frame.columnconfigure(0, weight=1)

# on lance la boucle principale
fenetre.mainloop()

Résultat visuel :

Exemple Python3 :

Code source et fichier à télécharger

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from tkinter import *
from tkinter import ttk

# on crée la fenêtre principale
fenetre = Tk()

# on crée un conteneur pour bien présenter
frame = Frame(fenetre)
frame.pack(expand=1, fill=BOTH, padx=2, pady=2)

# on ajoute des widgets
canvas = Canvas(frame, bg="white", width=200, height=150)
canvas.create_text(100, 75, text="Hello good people!")
hbar = ttk.Scrollbar(frame, orient=HORIZONTAL)
vbar = ttk.Scrollbar(frame, orient=VERTICAL)

# on ajoute une petite poignée de redimensionnement
# histoire de faire joli
sizegrip = ttk.Sizegrip(frame)

# on connecte les scrollbars au canevas
canvas.configure(
xscrollcommand=hbar.set,
yscrollcommand=vbar.set,
scrollregion=(0, 0, 800, 600),
)
hbar.configure(command=canvas.xview)
vbar.configure(command=canvas.yview)

# on place les widgets avec grid()
canvas.grid(row=0, column=0, sticky=NW+SE)
hbar.grid(row=1, column=0, sticky=EW)
vbar.grid(row=0, column=1, sticky=NS)
sizegrip.grid(row=1, column=1)

# on rend le canevas redimensionnable
frame.rowconfigure(0, weight=1)
frame.columnconfigure(0, weight=1)

# on lance la boucle principale
fenetre.mainloop()

Résultat visuel :

Je veux comprendre :

La librairie graphique Tkinter s'appuie sur Tcl/Tk pour gérer ses widgets à l'écran.

À partir de Tcl/Tk 8.5, une librairie additionnelle - ttk, pour Themed Tk widgets - est venue rejoindre Tkinter dans les paquets installés d'office avec le langage Python (modules Python standards).

Pour savoir si vous pouvez bénéficier de la librairie ttk, faites le petit test suivant dans une console Python (ou dans IDLE) :

Python2 :

>>> import ttk

Python3 :

>>> import tkinter.ttk

Note : les symboles '>>> ' représentent l'invite de commande de la console Python et ne doivent pas être saisis au clavier.

N'entrez que ce qui suit ces symboles.

Si vous n'obtenez aucune erreur, c'est bon : la librairie ttk est présente sur votre système. Vous allez pouvoir l'utiliser !

Si vous obtenez une erreur, c'est que très probablement votre version de Python n'intègre pas la version Tcl/Tk 8.5 ou ultérieure, ce qui implique aussi que la version de Python que vous avez est très probablement périmée.

La documentation officielle de Tkinter inclut à présent la documentation officielle de ttk (en anglais)

N'hésitez pas à vérifier de temps en temps s'il n'existe pas une version de Python2 ou de Python3 plus récente que la vôtre - et tant qu'à faire, si vous n'avez que Python2 sur votre machine, profitez-en aussi pour installer le dernier Python3 en date, ça peut servir.

Utilisateurs de distributions Linux® : tenez compte toutefois des contraintes de votre système. Certaines distributions - comme Ubuntu par exemple - s'appuient en interne sur des versions de Python2/Python3 et digèrent plutôt mal le fait que l'on force le système à migrer vers d'autres versions. En général, ces distributions sont très bien gérées par des équipes spécialisées qui vous proposent des mises à jour régulières de votre système - et donc des mises à jour des différentes versions de Python présentes en interne. Dans ce cas précis, mieux vaut suivre les recommandations de ces mises à jour plutôt que de s'amuser à vouloir upgrader le système soi-même, c'est beaucoup moins hasardeux.

Lien de téléchargement : https://www.python.org/downloads/ (en anglais).

IV - Remerciements

Je tiens à remercier chaleureusement toutes les équipes du site Développez.com pour leur relectures attentives, leurs suggestions très pertinentes et leur soutien en général.

Je remercie en particulier Malick SECK (milkoseck) pour la relecture et la correction orthographique, (Winnt) pour la relecture et les suggestions sur la forme, Fabien (f-leb) pour les conseils en gabarisation, la relecture technique et l'expertise pédagogique particulièrement pertinente - qui m'a permis de remanier ce tutoriel en profondeur pour le rendre encore plus accessible aux élèves de Terminale S option ISN - ainsi que Sébastien (-Nikopol-) pour avoir définitivement résolu l'énigme du Chat de Schrödinger. Désormais, nous savons de source sûre que le chat n'est ni mort ni vivant dans sa boîte radioactive, parce qu'il tente de résoudre l'équation de Schrödinger avec un ordinateur quantique, mais que ses moufles trop serrées l'empêchent de sortir de la boîte, ce qui est d'autant mieux puisque Alf l'extraterrestre l'attend à l'extérieur pour le déguster en civet !

Encore un mystère de l'univers résolu au détour d'une discussion de très haut vol, il va sans dire.