Infoforall

Python 12 : LES FONCTIONS - Portée des variables

Nous allons voir un aspect plus technique des fonctions : la portée des variables. Derrière ce terme, se cache la possibilité ou non pour deux parties d'un programme de voir ou de manipuler les variables des autres parties.

Imaginez

  • le corps du programme comme la cour d'un lycée
  • les fonctions comme les salles
  • les variables sont les élèves.

La question qu'on va se poser est alors :

"Une variable située dans une fonction peut-elle être vue depuis le programme principal ?"

"Une (élève) située dans une (salle) peut-elle être vue depuis la (cour du lycée) ?"

Attention : ce que nous allons voir ici est valable pour Python 3. Ce n'est pas forçément le cas pour les autres langages. Les choix effectués sur la portée des variables est un choix propre à chaque langage.

Or, parfois, on veut qu'une fonction puisse modifier une variable du programme. Prenons le cas de notre Simon : on veut que le programme mémorise les cases activées par l'utilisateur. Les cases sont activées via des fonctions. Il faut donc bien mémoriser la séquence hors de la fonction, sinon l'information disparait une fois qu'on sort de la fonction ...

Voici ce qu'on veut obtenir :

Enfin, cette activité est fondamentale au niveau de la création d'un projet d'envergure. Pourquoi ? Nous verrons comment stocker des données en nombre inconnu et agir sur celles-ci. Comment ? En agissant sur des listes depuis des fonctions.

1 - Portée des variables

Là, nous allons rentrer dans un problème bien spécifique à la programmation : la portée du nom d'une variable, ou autrement posé, le problème de la reconnaissance d'une variable par l'ordinateur.

1-1 Espace des noms

Pour éclaircir cette notion, nous allons utiliser le programme suivant :

01° Lire le code et tenter de trouver ce que doit afficher l'ordinateur. Lancer ensuite le code pour le vérifier.

#!/usr/bin/env python

# -*- coding: utf-8 -*-


# - - - - - - - - - - - - - - - - - -

# Déclarations des fonctions

# - - - - - - - - - - - - - - - - - -


def fois_trois(x):

    x = x*3

    print("Dans la fonction fois_trois, \tx vaut \t",x, "\t et son id vaut \t", id(x))

    return x


# - - - - - - - - - - - - - - - - - -

# Corps du programme

# - - - - - - - - - - - - - - - - - -


x = 2

resultat = fois_trois(6)

print("Dans le corps du programme, \tx vaut \t",x, "\t et son id vaut \t", id(x))

input("Appuyer sur ENTREE")

Cela devrait donner :

Dans la fonction trois_fois,    x vaut    18   et son id vaut    1400354400

Dans le corps du programme,     x vaut    2    et son id vaut    1400353888

Appuyer sur ENTREE

Mais quelle est cette magie ? x devrait valoir 18, l'ordinateur fait n'importe quoi !

En réalité, l'ordinateur fait ce qu'on lui dit. D'où vient le problème ? De nous bien entendu.

Nous voyons x dans le corps du programme et x dans la fonction et on en déduit qu'il s'agit de la même entité. Ca nous parait "normal". Attention aux intuitions en informatique : ne pouvez-vous pas connaitre deux personnes qui portent le même nom sans être la même personne ? Et oui, c'est possible.

Il peut y avoir un Guillaume Owsinski à Lille et un Guillaume Owsinski à Paris. Si on parle de Guillaume Owsinski en étant à Paris, on fait référence à celui qui habite à Paris, pas à l'autre. Aucune confusion possible. Et bien, c'est pareil avec les variables.

ESPACE DES NOMS : Lorsque vous voyez une variable x, rajoutez mentalement : "x du programme principal" ou "x de la fonction fois_trois"... Vous allez voir que vous ne les confondrez plus avec ce petit truc. Dans quelques temps, cela vous paraitra naturel de faire la distinction et vous n'aurez plus à faire ceci.

variables locales

Voici le même programme en version animée et en rajoutant une couleur aux x (Jaune-Orange pour l'espace des noms du corps du programme ou Bleu pour espace des noms de la fonction).

#!/usr/bin/env python

# -*- coding: utf-8 -*-


# - - - - - - - - - - - - - - - - - -

# Déclarations des fonctions

# - - - - - - - - - - - - - - - - - -


def fois_trois(x) :

    x = x*3

    print("Dans la fonction fois_trois, \tx vaut \t",x, "\t et son id vaut \t", id(x))

    return x


# - - - - - - - - - - - - - - - - - -

# Corps du programme

# - - - - - - - - - - - - - - - - - -


x = 2

resultat = fois_trois(6)

print("Dans le corps du programme, \tx vaut \t",x, "\t et son id vaut \t", id(x))

input("Appuyer sur ENTREE")

CLIQUEZ ICI POUR VOIR LE CONTENU DES VARIABLES :

x :

resultat :

x :

On voit ainsi que :

1-2 Portée des variables déclarées dans une fonction

Et si on tente d'afficher x dans le programme principal alors qu'on y fait référence uniquement dans la fonction ?

02° Vérifier bien que l'affectation de x n'apparait ci-dessous que dans la fonction mais qu'on tente de l'afficher dans le programme principal. Tester le code ensuite.

#!/usr/bin/env python

# -*- coding: utf-8 -*-


# - - - - - - - - - - - - - - - - - -

# Déclarations des fonctions

# - - - - - - - - - - - - - - - - - -

def fois_trois(x) :

    x = x*3

    print("Dans la fonction fois_trois, x vaut \t",x)

    return x


# - - - - - - - - - - - - - - - - - -

# Corps du programme

# - - - - - - - - - - - - - - - - - -

resultat = fois_trois(6)

print("Dans le corps du programme, x vaut \t",x)

input("Appuyer sur ENTREE")

On voit ainsi que :

Vous devriez avoir une erreur :

    Dans la fonction fois_trois, x vaut 18

Traceback (most recent call last):

File "G:\informatique\python\cours_10_fonctions\lec10_q2.py", line 16, in <module>

print("Dans le corps du programme, x vaut \t",x)

NameError: name 'x' is not defined

Pourquoi ?

Vous tentez dans le corps du programme d'afficher une variable  x  (du programme principal) qui n'existe pas dans l'espace des noms du corps du programme : aucune affectation avec un x n'est apparu dans l'exécution séquentielle du programme principal. La variable  x  de la fonction n'existe que lors de l'exécution de la fonction. D'ailleurs, pour limiter les données stockées inutilement en mémoire, la case ayant servi à stocker  x  est immédiatement liberée une fois qu'on sort de la fonction. C'est le "ramasse-miettes" qui s'occupe de cette destruction. On dit garbage collector en anglais.

variables locales

ESPACE DES NOMS : une variable locale d'une fonction n'a d'existence qu'à l'intérieur de la fonction lors de son exécution. L'espace des noms d'une fonction est reservé à cette fonction. Aucune autre partie du programme n'a accès à cet espace des noms.

portée depuis les fonctions

1-3 Portée des variables déclarées dans le programme principal

Les variables déclarées dans le corps du programme sont-elles accessibles depuis les fonctions ? Vous allez tenter de répondre à cela à l'aide de ce petit code :

#!/usr/bin/env python

# -*- coding: utf-8 -*-


# - - - - - - - - - - - - - - - - - -

# Déclarations des fonctions

# - - - - - - - - - - - - - - - - - -


def une_fonction(x) :

    print("Dans la fonction, z vaut \t",z)

    return x


# - - - - - - - - - - - - - - - - - -

# Corps du programme

# - - - - - - - - - - - - - - - - - -


z = 300

resultat = une_fonction(6)

print("Dans le corps prin., z vaut \t",z)

input("Appuyer sur ENTREE")

03° Utiliser le code précédent pour répondre à la question sur la visibilité du z du programme "principal" depuis une fonction : depuis l'intérieur de la fonction une_fonction peut-on voir la variable z du corps du programme ?

04° Rajouter z = z*2 comme première instruction de la fonction. Relancer. Peut-on modifier la valeur de z définie dans le corps de la fonction depuis l'intérieur de la fonction ?

A RETENIR : une variable locale du corps du programme peut être lue à l'intérieur d'une fonction à laquelle on fait appel. Par contre, on ne pourra pas en modifier la valeur.

variables locales

Si on résume :

  • Chaque partie du programme possède son propre espace des noms : partie principale et les fonctions.
  • Une variable du programme principal peut être lue mais pas modifiée dans les fonctions.
  • Une variable interne à une fonction n'a pas d'existence en dehors de cette fonction.

1-4 Arguments et paramètres

Il reste à voir le cas des arguments et des paramètres. Lorsqu'on envoie un argument à une fonction, peut-elle en changer la valeur ? Et bien, la réponse est NON. Lorsque vous fournissez une variable à une fonction, vous lui fournissez une copie du contenu, pas son adresse réelle.

Si vous voulez vous en convaincre, voilà un programme qui fournit en argument une variable z à la fonction nommée une_fonction. Dans une_fonction, on modifie la valeur du paramètre. Une fois revenu dans le corps du programme, la variable z est restée inchangée :

#!/usr/bin/env python

# -*- coding: utf-8 -*-


# - - - - - - - - - - - - - - - - - -

# Déclarations des fonctions

# - - - - - - - - - - - - - - - - - -


def une_fonction(x) :

    x = x*2

    print("Dans la fonction, x vaut \t",x)

    return x


# - - - - - - - - - - - - - - - - - -

# Corps du programme

# - - - - - - - - - - - - - - - - - -


z = 300

print("Dans le corps prin., z vaut \t",z," avant l'appel de la fonction")


resultat = une_fonction(z)


print("Dans le corps prin., z vaut \t",z," après l'appel de la fonction")

input("Appuyer sur ENTREE")


05° Testez pour vérifier.

Dans le corps princ., z vaut 300 avant l'appel de la fonction

Dans la fonction, x vaut 600

Dans le corps princ., z vaut 300 après l'appel de la fonction

A RETENIR : une variable transmise en argument ne sera pas modifiée même si la fonction modifie le contenu du paramètre correspondant. On dit que la variable est transmise en valeur car le programme n'envoie que la valeur de la variable ( et pas l'adresse ou l'identidfiant de la "case" dans laquelle elle est située).

La fonction n'a ainsi pas les moyens physiques d'atteindre cette case et d'y modifier quoi que ce soit.

argument et parametre

1-5 Deux variables portant le même nom. Laquelle choisir ?

06° Un exercice pour faire si vous avez compris. Que se passe-t-il s'il existe une variable x dans le corps du programme et une autre variable x dans une fonction ? Réaliser un code utilisant des variables portant le même nom et ayant des valeurs différentes (par exemple x = 5 dans le corps du programme et x = 200 dans la fonction). Demander d'affichage de x dans le corps du programme et dans la fonction. Conclusion ?

C'est donc simple : dans une fonction, on commence par chercher une variable qui porte le nom demandé dans l'espace des noms de la fonction. Si on ne trouve pas, on tente de trouver une variable qui porte le bon nom dans le corps du programme. Mais dans ce cas, on ne pourra que lire le contenu, pas en modifier la valeur.

Resumé :

On peut donner le même nom à des variables situées dans des structures différentes :

  • x déclarée dans le corps du programme,
  • x déclarée dans fonction_1 et
  • x déclarée dans fonction_2

désignent trois entités différentes.

variables locales

Une variable d'une fonction n'est pas visible en dehors de cette fonction..

portée depuis les fonctions

Cela revient à dire qu'on ne peut pas voir un élève depuis la cour ou depuis une autre salle.

Une variable du corps du programme est visible des fonctions rattachées. Par contre, elle ne sera pas modifiable depuis les fonctions.

variables locales

Cela revient à dire qu'un élève dans la cour est visible depuis les salles (il y a des fenêtres) mais qu'on ne peut pas le modifier, communiquer avec lui.

Une fonction commence toujours par chercher une variable locale dans son propre espace des noms. Si aucune variable locale ne porte le bon nom, l'interpréteur va voir dans l'espace des noms du corps du programme.

1-6 Communiquer avec le programme principal

Bon, les fonctions ne peuvent pas modifier les variables données dans le programme lui-même. C'est assez limitant. Comment faire pour qu'une fonction puisse néanmoins modifier les variables du corps du programme ?

La façon la plus propre de faire cela est d'affecter la variable qu'on veut modifier à l'aide du return de la fonction elle-même.

07° Tester la fonction mise_au_carre(valeur) qui renvoie avec le return le carré du paramètre valeur. On modifie la valeur de x du programme principal en l'affectant avec ce que renvoie la fonction. Vérifier que cela fonctionne.

#!/usr/bin/env python

# -*- coding: utf-8 -*-


# - - - - - - - - - - - - - - - - - -

# Déclarations des fonctions

# - - - - - - - - - - - - - - - - - -


def mise_au_carre(valeur) :

    return valeur**2


# - - - - - - - - - - - - - - - - - -

# Corps du programme

# - - - - - - - - - - - - - - - - - -


x = 3


for repetition in range(4) :

    print("x a comme valeur ",x," avant l'appel de la fonction")

    x = mise_au_carre(x)

    print("x a comme valeur ",x," après l'appel de la fonction\n")


input("Appuyer sur ENTREE")

Vous devriez obtenir :

x a comme valeur 3 avant l'appel de la fonction

x a comme valeur 9 après l'appel de la fonction


x a comme valeur 9 avant l'appel de la fonction

x a comme valeur 81 après l'appel de la fonction


x a comme valeur 81 avant l'appel de la fonction

x a comme valeur 6561 après l'appel de la fonction


x a comme valeur 6561 avant l'appel de la fonction

x a comme valeur 43046721 après l'appel de la fonction


Appuyer sur ENTREE

Voilà, vous savez comment modifier proprement la valeur d'une variable du programme principal à l'aide d'une fonction. Pourquoi proprement ? Car le programme principal n'a absolument pas besoin de savoir comment la fonction calcule le carré à l'interne. Imaginons qu'on change le nom x du paramètre de la fonction par une variable caChange :

#!/usr/bin/env python

# -*- coding: utf-8 -*-


# - - - - - - - - - - - - - - - - - -

# Déclarations des fonctions

# - - - - - - - - - - - - - - - - - -


def mise_au_carre(caChange) :

    return caChange**2


# - - - - - - - - - - - - - - - - - -

# Corps du programme

# - - - - - - - - - - - - - - - - - -


x = 3


for repetition in range(4) :

    print("x a comme valeur ",x," avant l'appel de la fonction")

    x = mise_au_carre(x)

    print("x a comme valeur ",x," après l'appel de la fonction\n")


input("Appuyer sur ENTREE")

08° Vérifier que cela fonctionne toujours.

C'est ce qu'on nomme l'encapsulation : les deux codes communiquent mais ne sont pas interdépendants des noms de variables de l'autre : on peut modifier le code du corps principal sans changer celui de la fonction, ou modifier celui de la fonction sans avoir à modifier celui du corps principal.

Et ça, lorsqu'on travaille en groupe, c'est carrément bien ! Si votre collègue décide de modifier son code, cela n'affecte pas votre travail !

Nous allons voir maintenant comment faire le Mal. A savoir comment faire pour que la fonction modifie réellement la variable du corps du programme depuis l'intérieur même de la fonction. Sans return sans rien. Pouf. Et nous allons voir pourquoi ce n'est pas une si bonne idée que cela. On utilise parfois cette méthode pour aller vite, mais gardez bien à l'esprit qu'il faudra l'utiliser avec parcimonie.

2 - Variables globales

La compréhension totale des parties suivantes est plutôt optionnelle : c'est en programmant que vous comprendrez réellement. Sur des cas concrets, vous arriverez certainement mieux à en voir l'intêret. Par contre, lisez au moins tout ceci jusqu'au bout. Ca en vaut la peine.

Reprécisons la situation : une fonction peut lire une variable déclarée dans le corps du programme mais ne peut pas la modifier. C'est un avantage conséquent car cela rend le code plus solide.

Néanmoins, on peut vouloir modifier la valeur d'une variable du corps du programme depuis une fonction.

Dans ce cas, il faut rajouter une ligne de code dans la fonction elle-même : si on veut modifier une variable ma_variable située dans le corps du programme, il suffit de rajouter global ma_variable en début de la fonction.

Exemple ci-dessous. Vous noterez que je déclare bien à part ma variable x de faire à bien faire comprendre qu'elle sera destinée à être globale et donc à être reconnue par des fonctions : si quelqu'un veut modifier le code, il sait qu'il ne doit pas changer le nom de cette variable. Rien ne vous oblige à l'initialiser ainsi à part, mais ça évite de faire des bétises lors d'une modification quelques semaines plus tard.

#!/usr/bin/env python

# -*- coding: utf-8 -*-


# - - - - - - - - - - - - - - - - - -

# Déclarations des variables globlales

# - - - - - - - - - - - - - - - - - -


x = 3 # x est destinée à être globale


# - - - - - - - - - - - - - - - - - -

# Déclarations des fonctions

# - - - - - - - - - - - - - - - - - -


def mise_au_carre() :

    global x # x local obtient le même id que x principal

    x = x**2


# - - - - - - - - - - - - - - - - - -

# Corps du programme #

# - - - - - - - - - - - - - - - - - -


for repetition in range(4) :

    print("x a comme valeur ",x," avant l'appel de la fonction")

    mise_au_carre()

    print("x a comme valeur ",x," après l'appel de la fonction\n")


input("Appuyer sur ENTREE")

09° Testez ce code : la fonction parvient-elle à modifier maintenant à modifier la variable x du corps du programme ?

Comme vous le voyez, la fonction ne communique pas avec le corps du programme via un return mais en modifiant directement le contenu de la variable.

variables globales

Pourquoi ne peut pas abuser de cette façon de faire ? Plusieurs raisons à cela :

REMARQUE : cette méthode de la variable globale ne doit donc être utilisée qu'en dernier recours car elle casse l'encapsulation et fragilise votre code en rendant des parties normalement indépendantes dépendantes des noms des variables d'une autre partie. . Pensez-y lors de vos projets : les variables globales c'est l'Agence Tous Risques. Le dernier recours au dernier moment.

agence tous risques

Résumé

  • Chaque partie du programme possède son propre espace des noms : partie principale et les fonctions.
  • Une variable du programme principal peut être lue mais pas modifiée dans les fonctions.
  • Une variable interne à une fonction n'a pas d'existence en dehors de cette fonction.
  • Les paramètres recoivent les données par valeur pas par identifiant. Les variables paramètres ne sont donc que des copies des arguments transmis : modifier les paramètres ne modifira pas la variable transmise en argument.
  • On peut communiquer avec l'extérieur grace au return. Si vous voulez transmettre plus d'un résultat, il faudra transmettre un tuple ou une liste par exemple.
  • On peut permettre à une fonction de contrôler une variable du programme principal à l'aide du mot-clé global.
variables locales

Attention, la notion de programme principal n'a pas beaucoup de sens : nous devrions dire le programme qui a lancé l'appel de la fonction. Et comme une fonction peut lancer une fonction, ca devient vite un jeu des poupées russes.

3 - Le cas des objets

Pour compléter un peu la gestion de code à l'aide des fonctions, il nous reste à voir comment fonctions et variables désignant des objets interagissent en Python.

Pour l'instant nous n'avons travaillé qu'avec des variables de structures contenant un contenu facilement stockable : un nombre, un string...

Nous allons voir comment on peut utiliser une fonction pour agir sur un objet en utilisant une interface Tkinter d'exemple.

L'interface n°1 est simplement un ensemble de trois Labels texte. On va tenter de modifier l'allure des widgets Label depuis la fonction mettre_en_forme. Pour l'instant, la fonction ne tente d'agir que sur widTexte1.

10° Lire le code ci-dessous. A votre avis, le bouton va-t-il parvenir à changer la position, la couleur et les dimensions du premier widget widTexte1 ? Si oui, pourquoi ? Si non, pourquoi ? Lancer ensuite le code pour vérifier votre réponse.

#!/usr/bin/env python

# -*- coding: utf-8 -*-

from tkinter import *


# - - - - - - - - - - - - - - - - - -

# Déclaration des fonctions

# - - - - - - - - - - - - - - - - - -


def mettre_en_forme() :

    # Message informatif sur le widget 1

    print("Dans la fonction, l'id id de widTexte1 est ",id(widTexte1))

    widTexte1.place(x = 50, y = 50)

    widTexte1.config(bg = '#FF9999')

    widTexte1.config(width = 10)

    widTexte1.config(height = 3)


# - - - - - - - - - - - - - - - - - -

# Création de la fenêtre et des objets associés la fenêtre

# - - - - - - - - - - - - - - - - - -


fen_princ = Tk()

fen_princ.geometry("500x500")


# Création et placement des widgets Label


widTexte1 = Label(fen_princ, text = "Zone 1")

widTexte1.place(x = 0, y = 0)


widTexte2 = Label(fen_princ, text = "Zone 2")

widTexte2.place(x = 200, y = 0)


widTexte3 = Label(fen_princ, text = "Zone 3")

widTexte3.place(x = 0, y = 200)


# Création d'un Button lancant la fonction mise_a_jour


widBouton = Button(fen_princ, text = "Mettre en forme !", command=mettre_en_forme)

widBouton.place(x = 0, y = 450)


# Message informatif sur le widget 1

print("Dans le programme principal, l'id de widTexte1 est ",id(widTexte1))


# - - - - - - - - - - - - - - - - - -

# Bouclage de la fenêtre fen_princ

# - - - - - - - - - - - - - - - - - -


fen_princ.mainloop()

...CORRECTION...

Le programme ne provoque pas d'erreur il fonctionne correctement. Pourquoi ?

C'est simple : widTexte1 n'appartient pas à l'espace des noms de la fonction. Lorsqu'on l'utilise, on obtient donc le contenu (id 536) de widTexte1 du programme principal : une fonction a le droit d'aller lire le contenu d'une variable du progamme principal.

widTexte1 contient donc l'adresse 536 de la table de référence de l'objet.

Or, la fonction ne modifie pas cette variable : elle ne fait que la lire pour utiliser l'adresse qu'il contient.

Le programme montre d'ailleurs bien que l'id contenu dans widTexte1 est le même dans la fonction et dans le programme. On désigne la même zone mémoire.

On utilise donc bien les méthodes à partir de la table de référence du premier Label et on parvient donc à modifier l'objet.

Cela va donner une interface très moche. Trop moche. Mais cela fonctionne. Pour rappel en voici la raison :

Dans le programme principal, l'id de widTexte1 est 536

Dans la fonction, l'id id de widTexte1 est 536

Donnons un peu plus d'explication :

Variable de structure et variable de référence

Une variable désigne toujours l'identifiant d'une zone-mémoire. Mais le contenu dans cette zone-mémoire peut être soit un 'véritable' contenu, soit une table permettant d'accéder à d'autres contenus.

Les variables de structure sont donc celles qui contiennent la référence d'une zone-mémoire contenant directement un contenu.

Ici, on tape a et on obtient une adresse. A cette adresse d'identifiant 005, on trouve un contenu qu'on peut afficher.

variable de structure

Je désigne ici parfois les variables de structure par le terme 'variables simples'. Attention, ce terme n'est pas un terme officiel de l'informatique. Mais distinguer les 'variables simples' et les 'variables référence d'objet' permet de facilement comprendre ce que chaque variable contient.

Dans le cas d'une 'variable référence d'objet' ou 'variable de référence', l'adresse mémoire contient l'inventaire des toutes les variables et méthodes attachés à cet objet. On ne peut donc pas directement l'afficher. Lorsqu'on tape le nom de la variable, on aura simplement la référence de l'objet, pas de contenu affichable.

Ici, si on tape widTexte1, on obtient une adresse : 836. A cette adresse d'identifiant 836, on trouve un ensemble d'autres références.

Ainsi, si on tape widTexte1.place(x = 0, y = 0), on doit aller voir à l'id 836 ce que désigne place et on peut aller à l'id 04.

variable de référence

Moralité : les variables de référence se comportent comme les autres variables vis à vis des fonctions. La différence vient de leur utilisation : la variable de référence contient juste la référence de l'objet. On peut donc modifier l'objet avec cette référence et les méthodes adaptées sans avoir à utiliser de nouvelle affectation avec un =. C'est donc plus simple.

Je voudrais maintenant plutôt créer mes widgets directement dans la fonction.

11° Regarder le programme ci-dessous. A votre avis, va-t-on réussir à créer le quatrième widget en cliquant sur le bouton widBouton2 qui active creer_le_widget ? Va-t-il s'afficher ? Si oui, pourquoi ? Si non, pourquoi ?

#!/usr/bin/env python

# -*- coding: utf-8 -*-

from tkinter import *


# - - - - - - - - - - - - - - - - - -

# Déclaration des fonctions

# - - - - - - - - - - - - - - - - - -


def mettre_en_forme() :

    widTexte1.place(x = 50, y = 50)

    widTexte1.config(bg = '#FF9999')

    widTexte1.config(width = 10)

    widTexte1.config(height = 3)


def creer_le_widget() :

    widTexte4 = Label(fen_princ, text = "Zone 4")

    widTexte4.place(x = 200, y = 200)

    print("Dans la fonction, l'id id de widTexte4 est ",id(widTexte4))


# - - - - - - - - - - - - - - - - - -

# Création de la fenêtre et des objets associés la fenêtre

# - - - - - - - - - - - - - - - - - -


fen_princ = Tk()

fen_princ.geometry("500x500")


# Création et placement des widgets Label


widTexte1 = Label(fen_princ, text = "Zone 1")

widTexte1.place(x = 0, y = 0)


widTexte2 = Label(fen_princ, text = "Zone 2")

widTexte2.place(x = 200, y = 0)


widTexte3 = Label(fen_princ, text = "Zone 3")

widTexte3.place(x = 0, y = 200)


# Création des Buttons


widBouton = Button(fen_princ, text = "Mettre en forme !", command=mettre_en_forme)

widBouton.place(x = 0, y = 450)


widBouton2 = Button(fen_princ, text = "Et un nouveau !", command=creer_le_widget)

widBouton2.place(x = 150, y = 450)


# - - - - - - - - - - - - - - - - - -

# Bouclage de la fenêtre fen_princ

# - - - - - - - - - - - - - - - - - -


fen_princ.mainloop()

...CORRECTION...

Cela devrait vous surprendre : normalement, ça ne devrait pas fonctionner !!!

Pourquoi ?

A cause du garbage collector : les variables créées dans une fonction sont supprimées lorsqu'on sort de la fonction. Vous avez déjà rencontré ce problème avec les images qu'on veut créer directement depuis une fonction...

Alors, pourquoi ca fonctionne ici ?

Et bien, simplement parce que la référence de votre widget est en réalité stockée dans la fenêtre globale fen_princ automatiquement ! Ainsi, le garbage collector ne peut pas détruire la zone mémoire qui contient ses données.

Ce petit truc peut donc vous simplifier la vie lorsque vous aller créer vos interfaces. Ca a l'air sympa de pouvoir créer des widgets directement dans des fonctions. Attention, je le répète : c'est un CAS PARTICULIER. Créer n'importe quelle autre variable (pour un integer, une image, un string ou une liste), et zou : le garbage collector vient la détruire !

Tiens : maintenant que le 4e Label est créé, pourquoi ne pas tenter de le modifier avec un autre bouton :

12° Regarder le programme ci-dessous. A votre avis, va-t-on réussir à modifer le quatrième widget en cliquant sur le bouton widBouton3 qui active mettre_en_forme_4 ? N'oubliez pas de d'abord le créer en appuyant sur le bouton 2. Justifier votre avis.

#!/usr/bin/env python

# -*- coding: utf-8 -*-

from tkinter import *


# - - - - - - - - - - - - - - - - - -

# Déclaration des fonctions

# - - - - - - - - - - - - - - - - - -


def mettre_en_forme() :

    widTexte1.place(x = 50, y = 50)

    widTexte1.config(bg = '#FF9999')

    widTexte1.config(width = 10)

    widTexte1.config(height = 3)


def creer_le_widget() :

    widTexte4 = Label(fen_princ, text = "Zone 4")

    widTexte4.place(x = 200, y = 200)

    print("Dans la fonction, l'id id de widTexte4 est ",id(widTexte4))


def mettre_en_forme_4() :

    widTexte4.config(bg = '#FF9999')

    widTexte4.config(width = 10)

    widTexte4.config(height = 3)


# - - - - - - - - - - - - - - - - - -

# Création de la fenêtre et des objets associés la fenêtre

# - - - - - - - - - - - - - - - - - -


fen_princ = Tk()

fen_princ.geometry("500x500")


# Création et placement des widgets Label


widTexte1 = Label(fen_princ, text = "Zone 1")

widTexte1.place(x = 0, y = 0)


widTexte2 = Label(fen_princ, text = "Zone 2")

widTexte2.place(x = 200, y = 0)


widTexte3 = Label(fen_princ, text = "Zone 3")

widTexte3.place(x = 0, y = 200)


# Création des Buttons


widBouton = Button(fen_princ, text = "Mettre en forme !", command=mettre_en_forme)

widBouton.place(x = 0, y = 450)


widBouton2 = Button(fen_princ, text = "Et un nouveau !", command=creer_le_widget)

widBouton2.place(x = 150, y = 450)


widBouton3 = Button(fen_princ, text = "Changer le 4 !", command=mettre_en_forme_4)

widBouton3.place(x = 300, y = 450)


# - - - - - - - - - - - - - - - - - -

# Bouclage de la fenêtre fen_princ

# - - - - - - - - - - - - - - - - - -


fen_princ.mainloop()

...CORRECTION...

Ca ne fonctionne pas et c'est normal : nous sommes parvenu à créer le 4e Label MAIS dans la fonction.

La variable du Label n'apparait donc pas dans l'espace des noms du programme principal : l'autre fonction ne peut donc pas agir sur ce Label puisqu'on n'a pas stocker son id. Il est stocké quelque part dans les données de la fenêtre et si vous saviez y retrouver la référence, vous pourriez agir dessus. Mais pas en renotant simplement le même nom de variable qui n'existe plus nul part !

1er limitation de la création de widget directement depuis une fonction : à moins de renvoyer la référence via un return, on perd l'accès à ce widget. Il existera mais vous ne pourrez plus y toucher.

Bon, c'est pas grave : tentons plutôt de modifier le Label 3 pour y afficher une image et le Label 2 pour y changer le texte.

13° Regarder le programme ci-dessous : on a créé un button widBouton3 qui va lancer la fonction deux_changements. Va-t-on réussir à modifier le texte du Label 2 ? Va-t-on réussir à afficher l'image dans le Label 3 ? Justifier vos avis.

#!/usr/bin/env python

# -*- coding: utf-8 -*-

from tkinter import *

from PIL import Image as Img

from PIL import ImageTk


# - - - - - - - - - - - - - - - - - -

# Déclaration des fonctions

# - - - - - - - - - - - - - - - - - -


def mettre_en_forme() :

    widTexte1.place(x = 50, y = 50)

    widTexte1.config(bg = '#FF9999')

    widTexte1.config(width = 10)

    widTexte1.config(height = 3)


def creer_le_widget() :

    widTexte4 = Label(fen_princ, text = "Zone 4")

    widTexte4.place(x = 200, y = 200)

    print("Dans la fonction, l'id id de widTexte4 est ",id(widTexte4))


def deux_changements() :

    # On tente de modifier le texte du widget 2

    widTexte2.config(text = 'Et voilà le travail !')

    # On tente d'afficher une image dans le widget 3

    presentation = Img.new("RGB", (20,20), (255,255,150))

    presentationTk = ImageTk.PhotoImage(presentation)

    widTexte3.config(image = presentationTk)


# - - - - - - - - - - - - - - - - - -

# Création de la fenêtre et des objets associés la fenêtre

# - - - - - - - - - - - - - - - - - -


fen_princ = Tk()

fen_princ.geometry("500x500")


# Création et placement des widgets Label


widTexte1 = Label(fen_princ, text = "Zone 1")

widTexte1.place(x = 0, y = 0)


widTexte2 = Label(fen_princ, text = "Zone 2")

widTexte2.place(x = 200, y = 0)


widTexte3 = Label(fen_princ, text = "Zone 3")

widTexte3.place(x = 0, y = 200)


# Création des Buttons

widBouton = Button(fen_princ, text = "Mettre en forme !", command=mettre_en_forme)

widBouton.place(x = 0, y = 450)

widBouton2 = Button(fen_princ, text = "Et un nouveau !", command=creer_le_widget)

widBouton2.place(x = 150, y = 450)


widBouton3 = Button(fen_princ, text = "2 Modifs !", command=deux_changements)

widBouton3.place(x = 300, y = 450)


# - - - - - - - - - - - - - - - - - -

# Bouclage de la fenêtre fen_princ

# - - - - - - - - - - - - - - - - - -


fen_princ.mainloop()

...CORRECTION...

Pour le texte du Label 2 aucun problème.

Pour l'image du Label 3 aucun problème non plus tant qu'on est dans la fonction.

La fonction parvient à agir sur le widget puisque sa variable est bien dans l'espace des noms du programme principal. On parvient donc à modifier le Label pour qu'il affiche l'image créée dans la fonction.

D'où vient le problème alors ? Ca vient de l'image : elle a été créée dans la fonction et donc le garbage collector va la supprimer : le Label pointe vers une image qui n'existe plus lorsqu'on revient dans le programme principal.

Comment résoudre le problème ? En stockant l'image dans le Label. Nous verrons le pourquoi du comment lorsqu'on parlera des objets. Pour l'instant, regardez juste la correction à apporter ci-dessous.

2e limitation de la création de widget directement depuis une fonction : c'est que, du coup, on oublie souvent que le cas des variables-widgets stockés dans la fenêtre principale (nommée fen_princ) est un cas particulier. On laisse le garbage collector faire son travail sur des variables qu'on pensait viables et on finit avec un code non fonctionnel. Donc, n'oubliez pas : pour garantir l'existence de variables hors de la fonction, il faut les enregistrer dans un objet qui est créé dans le programme principal.

Comment résoudre notre problème de disparition de presentationTk : en stockant la référence de la nouvelle image directement dans notre Label par exemple :

def deux_changements() :

    widTexte2.config(text = 'Et voilà le travail !')

    presentation = Img.new("RGB", (20,20), (255,255,150))

    presentationTk = ImageTk.PhotoImage(presentation)

    widTexte3.config(image = presentationTk)


    widTexte3.monEspaceDeStockage = presentationTk

Résumons :

Les variables de référence se comportent exactement comme les variables de structures.

Par contre, dans la mesure où on a juste beaucoup de les lire leur contenu pour obtenir l'id de l'objet qu'elles pointent, on peut agir sur les objets depuis une fonction.

Attention néanmoins : si vous créer un objet depuis une fonction, il sera détruit par le garbage collector une fois hors de la fonction. A moins de stocker la référence dans un objet créé dans le programme principal.

Spécificité des widgets Tkinter : lorsqu'on crée un widget, ses données sont stockés à l'intérieur de la fenêtre sur laquelle il devra être affiché. Il ne sera donc pas détruit à la sortie de la fonction, contrairement à tous les autres objets.

4 - Stocker les objets avec les listes

Même si Tkinter n'a pas détruit le beau widget n°4 que vous avez créé, vous ne pouvez plus agir dessus facilement par la suite. C'est un peu bête...

Nous allons donc voir deux manières de garder en mémoire ce que vous voulez garder.

4-1 La méthode du return

Une belle façon de faire qui respecte bien l'encapsulation : on renvoie la référence du widget via le return et on la stocke dans une variable du code principal.

#!/usr/bin/env python

# -*- coding: utf-8 -*-

from tkinter import *


# - - - - - - - - - - - - - - - - - -

# Déclaration des fonctions

# - - - - - - - - - - - - - - - - - -


def creer_le_widget(texte,coordX,coordY) :

    leWidget = Label(fen_princ, text = texte, bg = "#FFFFFF", fg = "#222222", width = 10, height = 3)

    leWidget.place(x = coordX, y = coordY)

    return leWidget


def changer_forme(leWidget) :

    leWidget.config(bg = '#FF9999')

    leWidget.config(fg = '#339933')


def modif_1() :

    changer_forme(widTexte1)


# - - - - - - - - - - - - - - - - - -

# Création de la fenêtre et des objets associés la fenêtre

# - - - - - - - - - - - - - - - - - -


fen_princ = Tk()

fen_princ.geometry("500x500")


# Création et placement des widgets Label


widTexte1 = creer_le_widget("TEXTE 1", 50,50)


# Création des Buttons

widBouton = Button(fen_princ, text = "Modifier le Label 1", command=modif_1)

widBouton.place(x = 0, y = 450)


# - - - - - - - - - - - - - - - - - -

# Bouclage de la fenêtre fen_princ

# - - - - - - - - - - - - - - - - - -


fen_princ.mainloop()

14° Analyser le code puis créer les 4 autres widgets Labels sans chercher à les associer à un bouton.

...CORRECTION...

# Création et placement des widgets Label


widTexte1 = creer_le_widget("TEXTE 1", 50,50)

widTexte2 = creer_le_widget("TEXTE 2", 200,50)

widTexte3 = creer_le_widget("TEXTE 3", 50,200)

widTexte4 = creer_le_widget("TEXTE 4", 200,200)

Comme vous pouvez le voir, cela va nous permettre de simplifier nos déclarations de widgets.

Reste néanmoins un problème : je dois encore créer une variable par widget. Or, si je ne sais pas combien je vais devoir en créer, je fais quoi ? J'en crée 3000 ?

Vous vous en doutez, la réponse est non. La solution ? Nous n'allons pas stocker les références des widgets directement dans des widgets : nous allons les stocker dans des listes.

4-2 Stockage dans les listes

Nous allons réutiliser l'objet liste. C'est un objet un peu particulier car on peut utiliser un print pour connaitre les données qui y sont stockés. Mais il possède bien des méthodes, append par exemple. Rappel :

Rappel de la première rencontre avec les listes ?

Qu'est qu'une liste ? C'est une suite ordonnée et modifiable (mutable) d'objets ou variables quelconques.

Les éléments d'une liste sont séparés par des virgules et la définition de la liste commence avec [ et s'arrête avec ].

 maListe = [1,'a',45.2,"bonjour",'b']  crée une liste de 5 éléments contenant l'integer 1, le char a, le float 45.2, le string bonjour et le char b.

Si on veut afficher une liste, print(maListe) fonctionne.

>>> maListe = [1,'a',45.2,"bonjour",'b']

>>> print(maListe)

[1,'a',45.2,'bonjour','b']

Si on veut connaitre le nombre d'éléments dans une liste, len(maListe) fonctionne.

>>> maListe = [1,'a',45.2,"bonjour",'b']

>>> len(maListe)

5

Si on veut accéder à l'un des éléments d'une liste, on tapera maListe[2], mais attention le premier élément est l'élément 0.

Si on demande print(maListe[2]) avec maListe = [1,'a',45.2,"bonjour",'b'], on obtient :

>>> maListe = [1,'a',45.2,"bonjour",'b']

>>> maListe = [2]

45.2

Si on veut accéder à un ensemble d'éléments d'une liste, on tapera ma_liste[1:4], et on aura les éléments 1,2 et 3 (car cela veut dire qu'on commence à l'élément 1 et qu'on s'arrête avant le 4.).

Si on demande print(ma_liste[1:4]) avec ma_liste = [1, 'a', 45.2,"bonjour", 'b'], on obtient :

>>> maListe = [1,'a',45.2,"bonjour",'b']

>>> print( ma_liste[1:4] )

['a',45.2,'bonjour']

Pour parcourir une liste, il suffit d'utiliser un for.

for element in maListe :

    print(element)

Et on obtient alors

1

a

45.2

bonjour

b

Nous avons également vu qu'on peut utiliser plutôt une boucle for numérique :

for numero in range(len(maListe)) :

    print(maListe[numero])

Enfin, nous avions vu comment rajouter des éléments dans une liste avec la méthode append qui rajoute des éléments en fin de liste.

>>> a = [1,2,3]

>>> a

[1, 2, 3]

>>> a.append('4')

>>> a

[1, 2, 3, '4']

>>> b = [7,8,9]

>>> a.append(b)

>>> a

[1, 2, 3, '4', [7, 8, 9]]

Cette méthode append permet donc de considérer les listes comme des piles de livres : on rajoute les éléments au dessus des derniers éléments.

append() rajoute au dessus de la pile

Alors ? Comment on fait ?

Voici le même programme mais avec le changement via le stockage dans la liste maListeDeLabels. On ne gérera pas la position des éléments pour simplifier le programme. On utilisera simplement pack :

#!/usr/bin/env python

# -*- coding: utf-8 -*-

from tkinter import *


# - - - - - - - - - - - - - - - - - -

# Déclaration des variables du programme

# - - - - - - - - - - - - - - - - - -


maListeDeLabels = [] # liste qui contiendra les références des Labels


# - - - - - - - - - - - - - - - - - -

# Déclaration des fonctions

# - - - - - - - - - - - - - - - - - -


def creer_le_widget(texte) :

    leWidget = Label(fen_princ, text = texte, bg = "#FFFFFF", fg = "#222222", width = 10, height = 3)

    leWidget.pack()

    return leWidget


def changer_forme(leWidget) :

    leWidget.config(bg = '#FF9999')

    leWidget.config(fg = '#339933')


def modif_1() :

    changer_forme(maListeDeLabels[0])


# - - - - - - - - - - - - - - - - - -

# Création de la fenêtre et des objets associés la fenêtre

# - - - - - - - - - - - - - - - - - -


fen_princ = Tk()

fen_princ.geometry("500x500")


# Création des Buttons

widBouton = Button(fen_princ, text = "Modifier le Label 1", command=modif_1)

widBouton.pack()


# Création et placement des widgets de type Label


for numero in range(4) :

    texteDuWidget = "TEXTE " + str(numero)

    leNouveauWidget = creer_le_widget(texteDuWidget)

    maListeDeLabels.append(leNouveauWidget)


# - - - - - - - - - - - - - - - - - -

# Bouclage de la fenêtre fen_princ

# - - - - - - - - - - - - - - - - - -


fen_princ.mainloop()

15° Analyser le code puis modifier le code pour que la fonction modif_1 s'applique à tous les widgets d'un coup.

...CORRECTION...

def modif_1() :

    for element in maListeDeLabels :

        changer_forme(element)

Par contre, on ne peut pas utiliser cette façon de gérer si on veut créer un bouton qui rajoute des Labels. En effet, les fonctions liées au bouton ne permettent pas de récupérer des valeurs. Dans ce cas, on peut très bien se passer du return et utiliser la méthode append directement dans la fonction. Par contre, on perd un peu en encapsulation : la fonction va avoir besoin de connaitre le nom de la liste du programme principal. Si on change ce nom, la fonction ne fonctionnerait plus...

16° Créer un nouveau bouton et sa fonction associée, disons rajout_label. La fonction doit bien évidemment être capable de rajouter un Label à l'écran et dans la liste de stockage des Labels Le Label devra afficher NOUVEAU.

...CORRECTION...

#!/usr/bin/env python

# -*- coding: utf-8 -*-

from tkinter import *


# - - - - - - - - - - - - - - - - - -

# Déclaration des variables du programme

# - - - - - - - - - - - - - - - - - -


maListeDeLabels = [] # liste qui contiendra les références des Labels


# - - - - - - - - - - - - - - - - - -

# Déclaration des fonctions

# - - - - - - - - - - - - - - - - - -


def creer_le_widget(texte) :

    leWidget = Label(fen_princ, text = texte, bg = "#FFFFFF", fg = "#222222", width = 10, height = 3)

    leWidget.pack()

    return leWidget


def changer_forme(leWidget) :

    leWidget.config(bg = '#FF9999')

    leWidget.config(fg = '#339933')


def modif_1() :

    for element in maListeDeLabels :

        changer_forme(element)


def rajout_label() :

    leNouveauWidget = creer_le_widget("NOUVEAU")

    maListeDeLabels.append(leNouveauWidget)


# - - - - - - - - - - - - - - - - - -

# Création de la fenêtre et des objets associés la fenêtre

# - - - - - - - - - - - - - - - - - -


fen_princ = Tk()

fen_princ.geometry("500x500")


# Création des Buttons

widBouton = Button(fen_princ, text = "Modifier les Labels", command=modif_1)

widBouton.pack()


widBouton2 = Button(fen_princ, text = "Rajouter Label", command=rajout_label)

widBouton2.pack()


# Création et placement des widgets de type Label


for numero in range(4) :

    texteDuWidget = "TEXTE " + str(numero)

    leNouveauWidget = creer_le_widget(texteDuWidget)

    maListeDeLabels.append(leNouveauWidget)


# - - - - - - - - - - - - - - - - - -

# Bouclage de la fenêtre fen_princ

# - - - - - - - - - - - - - - - - - -


fen_princ.mainloop()

Resumé global :

Les commentaires ci-dessous s'appliquent aux variables de structures (les variables simples) et les variables de référence (les variables objets).

Espace des noms :

On peut donner le même nom à des variables situées dans des structures différentes :

  • x déclarée dans le corps du programme,
  • x déclarée dans fonction_1 et
  • x déclarée dans fonction_2

désignent trois entités différentes.

Une fonction commence toujours par chercher une variable locale dans son propre espace des noms. Si aucune variable locale ne porte le bon nom, l'interpréteur va voir dans l'espace des noms du corps du programme.

variables locales

Portée des variables des fonctions :

Une variable d'une fonction n'est pas visible en dehors de cette fonction..

portée depuis les fonctions

Portée des variables du programme principal :

Une variable du corps du programme est visible des fonctions rattachées. Par contre, elle ne sera pas modifiable depuis les fonctions.

variables locales

On peut rendre une variable du programme principal modifiable par une fonction en la déclarant comme global dans la fonction.

Particularité des objets : Comme il suffit de pouvoir lire la référence d'un objet pour utiliser les méthodes, une fonction peut agir sans problème sur un objet du programme principal tant qu'on n'utilise pas d'affectation.

D'ailleurs, comme toute autre variable, une variable objet et l'objet qu'elle désigne seront détruit par le ramasse-miette à la sortie de la fonction.

Particularité de tkinter : Les widgets créés depuis les fonctions ne seront pas détruits par le ramasse-miette car on stocke leurs données dans l'objet correspondant à la fenêtre principale. Attention néanmoins : Si vous voulez modifier le widget ultérieurement, pensez à garder la référence via un return ou une liste.

5 - Mini-projet

Pour consolider tout ceci, je vous propose d'analyser notre simili programme de Simon.

ENREGISTREMENT DE LA SEQUENCE :

Vous mémorisez dans la liste listeDesActions les clics-gauche sur les carrés. C'est fonctionnel dans le code ci-dessous. Les codes sont enregistrés dans l'ordre chronologique.

Si vous appuyez sur le carré A, relachez, appuyez sur B, relachez, puis C, on obtient :

listeDesActions = [ 'A ON', 'A OFF', 'B ON', 'B OFF', 'C ON', 'C OFF' ]

ANIMATION DE LA SEQUENCE :

Avec un clic droit, vous lancez la séquence des clics mémorisés : l'ordinateur devra alors nous refaire la séquence que nous avons réalisé.

Nous allons vouloir récuperer le premier élément de listeDesActions (soit 'A ON' ici), simuler l'action et supprimer ce premier élement de la liste.

On devra alors obtenir :

listeDesActions = [ 'A OFF', 'B ON', 'B OFF', 'C ON', 'C OFF' ]

Et on devra recommencer ensuite avec 'A OFF', pour obtenir :

listeDesActions = [ 'B ON', 'B OFF', 'C ON', 'C OFF' ]

Puis avec 'B ON' pour obtenir :

listeDesActions = [ 'B OFF', 'C ON', 'C OFF' ]

ect . . .

Vous allez donc devoir vous approprier un code déjà existant. Une tâche pas facile si le code n'est pas correctement commenté.

Commençons par afficher le code brut. Boum. Vous ne devriez pas tout comprendre, surtout que le code utilise certaines notions que vous n'avez pas encore vu.

#!/usr/bin/env python

# -*- coding: utf-8 -*-


"""

DESCRIPTION :

L'utilisateur peut faire du clic-gauche sur les carrés colorés pour mémoriser une séquence.

Lorsqu'on utilise un clic-droit sur l'un des carrés, on montre la séquence qui a été enregistrée, dans l'ordre chronologique.

"""


from tkinter import *

from PIL import Image as Img

from PIL import ImageTk


# - - - - - - - - - - - - - - - - - -

# Variables du programme

# - - - - - - - - - - - - - - - - - -


listeDesActions = ['TOUTES ON','TOUTES OFF']

"""

La variable listeDesActions est une liste qui contient l'ensemble des actions que l'utilisateur a réalisé sur les carrés colorés.

Valeurs possibles dans cette liste :

'TOUTES ON' permet d'activer les 4 zones Images (les rendre plus claires)

'TOUTES OFF' permet de remettre les 4 carrés d'origine (les rendre plus foncées)

De la même façon :

'A ON' et 'A OFF' permettent respectivement d'avoir le carré bleu en bleu clair et bleu foncé.

'B ON' et 'B OFF' permettent respectivement d'avoir le carré vert en vert clair et vert foncé.

'C ON' et 'C OFF' permettent respectivement d'avoir le carré rouge en rouge clair et rouge foncé.

'D ON' et 'D OFF' permettent respectivement d'avoir le carré jaune en jaune clair et jaune foncé.

Ainsi si listeDesActions = ['D ON','D OFF','A ON','A OFF'], cela veut dire qu'on a mémorisé d'abord un clic sur le carré jaune D puis un clic sur le carré bleu A.

"""


animation = False # Variables GLOBAL pour certaines fonctions

"""

Cette variable sert à savoir si c'est le programme d'animation ou l'utilisateur qui a la main.

Elle doit valoir True pendant la réstitution de la séquence, l'utilisateur n'a pas la main.

Elle doit valoir False pendant la mémorisation de la séquence créée par l'utilisateur via les clics gauche.

"""


# - - - - - - - - - - - - - - - - - -

# Déclaration des fonctions de CREATION

# - - - - - - - - - - - - - - - - - -


def creation_image_tk(rouge,vert,bleu) :

    """

    Permet de créer une image PhotoImage compatible avec le module tkinter.

    Les paramètres rouge, vert et bleu sont des valeurs comprises entre 0 et 255 de la couleur RGB voulue.

    Renvoie la référence de l'image PhotoImage. Devra être stockée dans une variable.

    """

    carrePIL = Img.new("RGB", (100,100), (rouge,vert,bleu))

    carreTk = ImageTk.PhotoImage(carrePIL)

    return carreTk


def creation_widget_case(coordX,coordY,imageDeBase) :

    """

    Permet de créer un widget Label affichant une image.

    Les paramètres ccord_x et coordY sont les coordonnées du widget dans la fenêtre.

    Le paramètres imageDeBase correspond à la référence de l'image qu'on veut afficher initialement.

    Renvoie la référence du widget Label. Devra être stockée dans une variable.

    """

    widLabel = Label(fen_princ, image = imageDeBase)

    widLabel.place(x = coordX, y = coordY)

    return widLabel


# - - - - - - - - - - - - - - - - - -

# Déclaration des fonctions

# - - - - - - - - - - - - - - - - - -


def animer() :

    """

    Cette fonction lance l'animation qui montre la séquence stockée dans listeDesActions.

    Elle prend l'action stockée dans listeDesActions[0].

    Elle change l'affichage en fonction de l'action.

    Elle supprime ensuite le premier élement de listeDesActions.

    Exemple :

    listesDesActions = ['A ON','A OFF','B ON','B OFF'] va gérer l'action "A ON".

    On supprime 'A ON' et on obtient

    listesDesActions = ['A OFF','B ON','B OFF'] va gérer l'action 'A ON'.

    """

    global animation


    # On place dans action l'action à faire, codée à l'aide d'un string : voir modifier_case

    action = listeDesActions[0]


    # PRINT POUR LE MODE DEBUG

    print(len(listeDesActions))

    print(listeDesActions)

    print(action)


    # On lance la modification à faire

    modifier_case(action)

    # On supprime l'action de la liste

    listeDesActions.remove(action)


    # Le test suivant permet de voir s'il reste des actions enregistrées ou si on a fini

    if len(listeDesActions) > 0 :

        fen_princ.after(500,animer)

    else :

        listeDesActions.append('TOUTES ON')

        listeDesActions.append('TOUTES OFF')

        animation = False


def modifier_case(action) :

    """

    Cette fonction permet de modifier les widgets en fonctin du parametre action.

    "A ON" active la 1er case, "A OFF" la désactive

    "B ON" active la 2e case, "B OFF" la désactive

    "C ON" active la 3e case, "C OFF" la désactive

    "D ON" active la 4e case, "D OFF" la désactive

    "TOUTES ON" active les 4 cases, "TOUTES OFF" les désactive

    """

    if action == "A ON" :

        caseA.configure(image = carreBleuA_ON)

    elif action == "B ON" :

        caseB.configure(image = carreVertB_ON)

    elif action == "C ON" :

        caseC.configure(image = carreRougeC_ON)

    elif action == "D ON" :

        caseD.configure(image = carreJauneD_ON)

    elif action == "A OFF" :

        caseA.configure(image = carreBleuA_OFF)

    elif action == "B OFF" :

        caseB.configure(image = carreVertB_OFF)

    elif action == "C OFF" :

        caseC.configure(image = carreRougeC_OFF)

    elif action == "D OFF" :

        caseD.configure(image = carreJauneD_OFF)

    elif action == "TOUTES ON" :

        caseA.configure(image = carreBleuA_ON)

        caseB.configure(image = carreVertB_ON)

        caseC.configure(image = carreRougeC_ON)

        caseD.configure(image = carreJauneD_ON)

    elif action == "TOUTES OFF" :

        caseA.configure(image = carreBleuA_OFF)

        caseB.configure(image = carreVertB_OFF)

        caseC.configure(image = carreRougeC_OFF)

        caseD.configure(image = carreJauneD_OFF)


# - - - - - - - - - - - - - - - - - -

# Déclaration des fonctions EVENEMENTS

# - - - - - - - - - - - - - - - - - -


def lancement(event) :

    """

    Fonction-événement qui lance l'animation de la séquence stockée.

    """

    global animation

    animation = True

    animer()


def changeA_ON(event) :

    """Fonction-événement qui modifie le carré A si l'animation n'est pas active."""

    if animation == False :

        modifier_case("A ON")

        listeDesActions.append('A ON')


def changeA_OFF(event) :

    """Fonction-événement qui modifie le carré A si l'animation n'est pas active."""

    if animation == False :

        modifier_case("A OFF")

        listeDesActions.append('A OFF')


def changeB_ON(event) :

    """Fonction-événement qui modifie le carré B si l'animation n'est pas active."""

    if animation == False :

        modifier_case("B ON")

        listeDesActions.append('B ON')


def changeB_OFF(event) :

    """Fonction-événement qui modifie le carré B si l'animation n'est pas active."""

    if animation == False :

        modifier_case("B OFF")

        listeDesActions.append('B OFF')


def changeC_ON(event) :

    """Fonction-événement qui modifie le carré C si l'animation n'est pas active."""

    if animation == False :

        modifier_case("C ON")

        listeDesActions.append('C ON')


def changeC_OFF(event) :

    """Fonction-événement qui modifie le carré C si l'animation n'est pas active."""

    if animation == False :

        modifier_case("C OFF")

        listeDesActions.append('C OFF')


def changeD_ON(event) :

    """Fonction-événement qui modifie le carré D si l'animation n'est pas active."""

    if animation == False :

        modifier_case("D ON")

        listeDesActions.append('D ON')


def changeD_OFF(event) :

    """Fonction-événement qui modifie le carré D si l'animation n'est pas active."""

    if animation == False :

        modifier_case("D OFF")

        listeDesActions.append('D OFF')


# - - - - - - - - - - - - - - - - - -

# Création de la fenêtre et des objets associés la fenêtre

# - - - - - - - - - - - - - - - - - -


fen_princ = Tk()

fen_princ.geometry("500x500")


# Création des images de base


carreBleuA_OFF = creation_image_tk(0,0,200)

carreBleuA_ON = creation_image_tk(75,75,250)

carreVertB_OFF = creation_image_tk(0,200,0)

carreVertB_ON = creation_image_tk(75,250,75)

carreRougeC_OFF = creation_image_tk(200,0,0)

carreRougeC_ON = creation_image_tk(250,75,75)

carreJauneD_OFF = creation_image_tk(200,200,0)

carreJauneD_ON = creation_image_tk(250,250,0)


# Création et placement des widget Label


caseA = creation_widget_case(100,100,carreBleuA_OFF)

caseB = creation_widget_case(300,100,carreVertB_OFF)

caseC = creation_widget_case(100,300,carreRougeC_OFF)

caseD = creation_widget_case(300,300,carreJauneD_OFF)


# - - - - - - - - - - - - - - - - - -

# Gestion des événements

# - - - - - - - - - - - - - - - - - -


# Associe un clic-gauche sur un carré à une fonction-événement

caseA.bind( '<Button-1>', changeA_ON )

caseB.bind( '<Button-1>', changeB_ON )

caseC.bind( '<Button-1>', changeC_ON )

caseD.bind( '<Button-1>', changeD_ON )


# Associe le relachement du bouton-gauche à une fonction-événement

caseA.bind( '<ButtonRelease-1>', changeA_OFF )

caseB.bind( '<ButtonRelease-1>', changeB_OFF )

caseC.bind( '<ButtonRelease-1>', changeC_OFF )

caseD.bind( '<ButtonRelease-1>', changeD_OFF )


# Associe un clic-gauche sur un carré à une fonction-événement

fen_princ.bind( '<ButtonRelease-3>', lancement )


# - - - - - - - - - - - - - - - - - -

# Bouclage de la fenêtre fen_princ

# - - - - - - - - - - - - - - - - - -


fen_princ.mainloop()

Pour comprendre ce code, il va falloir le lire dans un certain ordre. Ca a l'air compliqué cette histoire ...

Regardons le début du programme.

17° Quels sont les modules importés en totalité ? Parmi ceux-ci quels sont ceux qui sont importées sans avoir besoin de les nommer pour utiliser leur contenu ? Pouvez-vous donner le rôle des deux variables du programme ?

#!/usr/bin/env python

# -*- coding: utf-8 -*-

from tkinter import *

from PIL import Image as Img

from PIL import ImageTk


# - - - - - - - - - - - - - - - - - -

# Variables du programme

# - - - - - - - - - - - - - - - - - -


listeDesActions = ['TOUTES ON','TOUTES OFF']

"""

La variable listeDesActions est une liste qui contient l'ensemble des actions que l'utilisateur a réalisé sur les carrés colorés.

Valeurs possibles dans cette liste :

'TOUTES ON' permet d'activer les 4 zones Images (les rendre plus claires)

'TOUTES OFF' permet de remettre les 4 carrés d'origine (les rendre plus foncées)

De la même façon :

'A ON' et 'A OFF' permettent respectivement d'avoir le carré bleu en bleu clair et bleu foncé.

'B ON' et 'B OFF' permettent respectivement d'avoir le carré vert en vert clair et vert foncé.

'C ON' et 'C OFF' permettent respectivement d'avoir le carré rouge en rouge clair et rouge foncé.

'D ON' et 'D OFF' permettent respectivement d'avoir le carré jaune en jaune clair et jaune foncé.

Ainsi si listeDesActions = ['D ON','D OFF','A ON','A OFF'], cela veut dire qu'on a mémorisé d'abord un clic sur le carré jaune D puis un clic sur le carré bleu A.

"""


animation = False # Variables GLOBAL pour certaines fonctions

"""

Cette variable sert à savoir si c'est le programme d'animation ou l'utilisateur qui a la main.

Elle doit valoir True pendant la réstitution de la séquence, l'utilisateur n'a pas la main.

Elle doit valoir False pendant la mémorisation de la séquence créée par l'utilisateur via les clics gauche.

...CORRECTION MODULES...

On pourra utiliser tous les modules de la bibliothèque tkinter sans avoir besoin de le faire précéder de tkinter.. On importe tout directement.

On pourra utiliser le module Image de la bibliothèque Pillow en utilisant Img.

On pourra utiliser le module ImageTk de la bibliothèque Pillow en utilisant ImageTk.

...CORRECTION VARIABLES...

Visiblement, c'est la valeur de ce booléen (True/False) qui permettra de gérer le fait de lancer l'animation ou de laisser l'utilisateur cliquer sur les carrés.

Pour savoir comment, il va falloir aller lire la suite du code.

Ensuite, on zappe les fonctions et on va le programme principal. Nous irions lire les déclarations d'une fonction lorsqu'on en a besoin. Tentons de voir ce que cela donne au niveau de la création des fenêtres.

Voici le code à étudier ainsi que les fonctions qu'il utilise.

# - - - - - - - - - - - - - - - - - -

# Création de la fenêtre et des objets associés la fenêtre

# - - - - - - - - - - - - - - - - - -


fen_princ = Tk()

fen_princ.geometry("500x500")


# Création des images de base


carreBleuA_OFF = creation_image_tk(0,0,200)

carreBleuA_ON = creation_image_tk(75,75,250)

carreVertB_OFF = creation_image_tk(0,200,0)

carreVertB_ON = creation_image_tk(75,250,75)

carreRougeC_OFF = creation_image_tk(200,0,0)

carreRougeC_ON = creation_image_tk(250,75,75)

carreJauneD_OFF = creation_image_tk(200,200,0)

carreJauneD_ON = creation_image_tk(250,250,0)


# Création et placement des widget Label


caseA = creation_widget_case(100,100,carreBleuA_OFF)

caseB = creation_widget_case(300,100,carreVertB_OFF)

caseC = creation_widget_case(100,300,carreRougeC_OFF)

caseD = creation_widget_case(300,300,carreJauneD_OFF)

...def creation_image_tk...

def creation_image_tk(rouge,vert,bleu) :

    """

    Permet de créer une image PhotoImage compatible avec le module tkinter.

    Les paramètres rouge, vert et bleu sont des valeurs comprises entre 0 et 255 de la couleur RGB voulue.

    Renvoie la référence de l'image PhotoImage. Devra être stockée dans une variable.

    """

    carrePIL = Img.new("RGB", (100,100), (rouge,vert,bleu))

    carreTk = ImageTk.PhotoImage(carrePIL)

    return carreTk

...def creation_widget_case...

def creation_widget_case(coordX,coordY,imageDeBase) :

    """

    Permet de créer un widget Label affichant une image.

    Les paramètres ccord_x et coordY sont les coordonnées du widget dans la fenêtre.

    Le paramètres imageDeBase correspond à la référence de l'image qu'on veut afficher initialement.

    Renvoie la référence du widget Label. Devra être stockée dans une variable.

    """

    widLabel = Label(fen_princ, image = imageDeBase)

    widLabel.place(x = coordX, y = coordY)

    return widLabel

On voit qu'on crée 8 images PhotoImage correspondant à un carré bleu foncé, un bleu clair, un vert foncé, un vert clair, un rouge foncé, un rouge clair, un orange et un jaune.

On crée ensuite nos 4 carrés Label en transmettant

18° Regardez la partie sur la gestion des événements. Vous n'avez pas encore vu les événements, mais vous allez vous en sortir ! Répondre ensuite aux questions suivantes :

  1. Quelle fonction se lance si on clique sur le carré A avec le bouton gauche de la souris (le Button-1) ?
  2. Dans quel cas a-t-on un résultat ? Que provoque cette action si elle a un effet ?
  3. Quelle fonction se lance lorsqu'on relache (release) le bouton gauche de la souris ?
  4. Quelle action se lance avec le relachement d'un clic-droit de la souris (le Button-3) ?

# - - - - - - - - - - - - - - - - - -

# Gestion des événements

# - - - - - - - - - - - - - - - - - -


# Associe un clic-gauche sur un carré à une fonction-événement

caseA.bind( '<Button-1>', changeA_ON )

caseB.bind( '<Button-1>', changeB_ON )

caseC.bind( '<Button-1>', changeC_ON )

caseD.bind( '<Button-1>', changeD_ON )


# Associe le relachement du bouton-gauche à une fonction-événement

caseA.bind( '<ButtonRelease-1>', changeA_OFF )

caseB.bind( '<ButtonRelease-1>', changeB_OFF )

caseC.bind( '<ButtonRelease-1>', changeC_OFF )

caseD.bind( '<ButtonRelease-1>', changeD_OFF )


# Associe un clic-gauche sur un carré à une fonction-événement

fen_princ.bind( '<ButtonRelease-3>', lancement )

...CORRECTION 1...

Le clic-gauche sur sur A est associé à la fonction changeA_ON.

...CORRECTION 2...

Voici cette fonction :

def changeA_ON(event) :

    """Fonction-événement qui modifie le carré A si l'animation n'est pas active."""

    if animation == False :

        modifier_case("A ON")

        listeDesActions.append('A ON')


On voit donc qu'il n'y aura un effet que si l'animation n'est pas en route.

Dans ce cas, on va modifier l'apparence de la case avec la fonction modifier_case et qu'on va ensuite rajouter l'action effectuée à notre liste listeDesActions. Ceci grace à la méthode append.

En allant voir le code, on voit qu'on configure le carré bleu clair.

...CORRECTION 3...

Le relachement sur A est associé à :

def changeA_OFF(event) :

    """Fonction-événement qui modifie le carré A si l'animation n'est pas active."""

    if animation == False :

        modifier_case("A OFF")

        listeDesActions.append('A OFF')


On voit encore qu'il n'y aura un effet que si l'animation n'est pas en route.

Dans ce cas, on va modifier l'apparence de la case avec la fonction modifier_case et qu'on va ensuite rajouter l'action effectuée à notre liste listeDesActions. Ceci grace à la méthode append.

En allant voir le code, on voit qu'on revient au carré bleu foncé.

...CORRECTION 4...

Cette fois, on lance la fonction lancement.


def lancement(event) :

    """

    Fonction-événement qui lance l'animation de la séquence stockée.

    """

    global animation

    animation = True

    animer()


On modifie la variable du programme principal animation à l'aide de GLOBAL.

Avec une affectation à True, on signale qu'on passe en phase d'animation et que les clics de l'utilisateur ne doivent plus avoir d'effet pour l'instant : c'est l'ordinateur qui prend la main.

On lance ensuite la fonction animer : c'est le coeur de l'animation, la fonction qui va contrôler l'ensemble des successions d'actions.


Voici donc le coeur du programme :

def animer() :

    """

    Cette fonction lance l'animation qui montre la séquence stockée dans listeDesActions.

    Elle prend l'action stockée dans listeDesActions[0].

    Elle change l'affichage en fonction de l'action.

    Elle supprime ensuite le premier élement de listeDesActions.

    Exemple :

    listesDesActions = ['A ON','A OFF','B ON','B OFF'] va gérer l'action "A ON".

    On supprime 'A ON' et on obtient

    listesDesActions = ['A OFF','B ON','B OFF'] va gérer l'action 'A ON'.

    """

    global animation


    # On place dans action l'action à faire, codée à l'aide d'un string : voir modifier_case

    action = listeDesActions[0]


    # PRINT POUR LE MODE DEBUG

    print(len(listeDesActions))

    print(listeDesActions)

    print(action)


    # On lance la modification à faire

    modifier_case(action)

    # On supprime l'action de la liste

    listeDesActions.remove(action)


    # Le test suivant permet de voir s'il reste des actions enregistrées ou si on a fini

    if len(listeDesActions) > 0 :

        fen_princ.after(500,animer)

    else :

        listeDesActions.append('TOUTES ON')

        listeDesActions.append('TOUTES OFF')

        animation = False

19° Lancez le programme. Essayez le puis fermez le. Ensuite, dans la console Python, tapez ceci :

>>> help(animer)

Pas mal non ? Cela vous permettra de chercher facilement la documentation sur une fonction, pourvu qu'elle soit bien documentée !

>>> help(animer)

Help on function animer in module __main__:


animer()

    Cette fonction lance l'animation qui montre la séquence stockée dans listeDesActions.

    Elle prend l'action stockée dans listeDesActions[0].

    Elle change l'affichage en fonction de l'action.

    Elle supprime ensuite le premier élement de listeDesActions.

    Exemple :

    listesDesActions = ['A ON','A OFF','B ON','B OFF'] va gérer l'action "A ON".

    On supprime 'A ON' et on obtient

     listesDesActions = ['A OFF','B ON','B OFF'] va gérer l'action 'A ON'.

20° Comment parvient-on à supprimer le premier élément de la liste ? Comment parvient-on à relancer automatiquement la fonction au bout d'un certain temps ?

...SUPPRESSION...

On supprime le premier élément d'une liste à l'aide de la méthode remove qu'on semble appliquer à l'élément qu'on vient de lire : celui stocké dans la variable action.

Exemple :

Avec [ 'chiens','chats','lapins' ].remove('chiens') on obtient [ 'chats','lapins' ].

...RELANCEMENT AUTOMATIQUE...

Tout est présent sur cette simple ligne :

        fen_princ.after(500,animer)

La méthode after permet de lancer l'appel d'une fonction après un temps fourni en ms. Ici avec 500ms, cela veut dire qu'on lance l'appel suivant de la fonction animer après 0,5s d'attente.

La faculté d'une fonction de se relancer elle-même se nomme la récursivité. Elle permet de faire des calculs assez complexe mais également de réaliser des animations comme vous venez de le voir.

Son utilisation pour les animations sera étudiée en détail dans l'activité sur les animations justement.

Voilà. Nous avons fini avec cette activité sur la portée des variables en présentant un cas concret d'utilisation d'une variable de structure animation puis d'un liste listeDesActions modifiée depuis les fonctions.

Comme vous le voyez, le problème des appels de base avec Tk est qu'on ne peut pas facilement transmettre de paramètres. D'où l'utilisation des variables globales pour l'instant.

Nous verrons plus tard des moyens de réduire encore la taille de ce code. Comme vous le voyez, on répète souvent des lignes assez similaires. Vous vous doutez bien qu'on peut faire mieux !

Je vous laisse avec notre Simili-Simon. Testez le, modifiez le. Vous avez maintenant presque assez de connaissances pour créer un véritable jeu animé. C'est le but des chapitres "Interface graphique" 2, 3 et 4.

6 - FAQ : les listes, des trucs en plus

En attendant d'atteindre l'activité sur les listes, voici encore quelques petites astuces qui pourraient vous servir :

Deuxième rencontre avec les listes

Pour rajouter des éléments à une liste, on peut également utiliser un simple + entre deux listes. Attention néanmoins : on crée alors une nouvelle liste qui n'aura pas la même adresse/référence/id que la précédente contrairement à l'utilisation de la méthode append. Comme la liste est mutable, c'est même plutôt une façon de faire à éviter SAUF si vous voulez clairement créer une nouvelle liste à partir d'une autre liste.

Si on utilise maListe = maListe + ['nouveau'] avec maListe = [1,'a',45.2,"bonjour",'b'], on obtient également :

>>> maListe = [1,'a',45.2,"bonjour",'b']

>>> maListe = maListe + ['nouveau']

>>> print(maListe)

['a',45.2,'bonjour','b','nouveau']

Par contre, cette fois, on a recréé une liste qui porte le même nom : ce n'est pas la même liste au niveau de son id.

Si on veut rajouter un élément à une position particulière d'index i dans la liste, on utilise maListe.insert(i,x) où x est l'élément à rajouter et i l'index dans la liste.

>>> maListe = [1,'a',45.2,"bonjour",'b']

>>> maListe.insert(1,'nouveau')

>>> print(maListe)

[1,'nouveau','a',45.2,'bonjour','b']

Souvenez-vous que l'index du premier élément est 0, pas 1.

Si i dépasse l'index maximum, x sera juste ajouté en fin de liste, comme avec un append.

Si on veut supprimer un élément x, on utilise maListe.remove(x) où x est l'élément à supprimer dans la liste.

Si on utilise maListe.remove('nouveau') avec maListe = [1,'nouveau','a',45.2,"bonjour",'b','nouveau'], on obtient :

>>> maListe = [1,'nouveau','a',45.2,"bonjour",'b','nouveau']

>>> maListe.remove('nouveau')

[1,'a',45.2,"bonjour",'b','nouveau']

On remarquera donc qu'elle ne supprime que le premier élément 'nouveau' rencontré. S'il y en a d'autres, il faudra faire d'autres remove.

Attention, si l'élément n'est pas présent dans la liste, la méthode va lever une exception de type valueError. Il faudra donc utiliser un while et un try pour supprimer tous les éléments identiques.

Justement, si on veut connaitre le nombre de fois qu'un élément x apparait dans une liste, on utilise maListe.count(x) où x est l'élément à surveiller dans la liste.

Si on utilise maListe.count('nouveau') avec maListe = [1,'nouveau','a',45.2,"bonjour",'b','nouveau'], on obtient :

>>> maListe = [1,'nouveau','a',45.2,"bonjour",'b','nouveau']

>>> maListe.count('nouveau')

2

Enfin, deux méthodes qui sont pratiques et un peu l'inverse l'une de l'autre :

Si on utilise maListe.sort(), on ordonne les éléments de la liste maListe.

Exemple 1:

>>> maListe = ['un','deux','trois','quatre']

>>> maListe.sort()

>>> maListe

['deux', 'quatre', 'trois', 'un']

Exemple 2:

>>> maListe = [5,1,3,2]

>>> maListe.sort()

>>> maListe

[1, 2, 3, 5]

Exemple 3: mélange d'int et de str

>>> maListe = ['un','deux','trois','quatre',5,1,3,2]

>>> maListe.sort()

TypeError: '<' not supported between instances of 'int' and 'str'

Donc attention : ce n'est pas une méthode miracle : elle doit pouvoir sélectionner et trier les éléments de type différents entre eux. Si elle ne sait pas faire, ça ne fonctionnera pas.

L'inverse, c'est la fonction shuffle : cette fonction du module random va mélanger les éléments de la liste et les répartir au hasard...

>>> import random as random

>>> maListe = ['un','deux','trois','quatre',5,1,3,2]

>>> random.shuffle(maListe)

>>> maListe

['trois', 'un', 'deux', 2, 1, 'quatre', 3, 5]

Pratique non ?