Programmation Python/Version imprimable2b

Ceci est la version imprimable de Programmation Python.
  • Si vous imprimez cette page, choisissez « Aperçu avant impression » dans votre navigateur, ou cliquez sur le lien Version imprimable dans la boîte à outils, vous verrez cette page sans ce message, ni éléments de navigation sur la gauche ou en haut.
  • Cliquez sur Rafraîchir cette page pour obtenir la dernière version du wikilivre.
  • Pour plus d'informations sur les version imprimables, y compris la manière d'obtenir une version PDF, vous pouvez lire l'article Versions imprimables.


Programmation Python

Une version à jour et éditable de ce livre est disponible sur Wikilivres,
une bibliothèque de livres pédagogiques, à l'URL :
https://fr.wikibooks.org/wiki/Programmation_Python

Vous avez la permission de copier, distribuer et/ou modifier ce document selon les termes de la Licence de documentation libre GNU, version 1.2 ou plus récente publiée par la Free Software Foundation ; sans sections inaltérables, sans texte de première page de couverture et sans Texte de dernière page de couverture. Une copie de cette licence est incluse dans l'annexe nommée « Licence de documentation libre GNU ».

Fonctions

Définir une fonction

modifier
Début d’un principe
Fin du principe


Une fonction Python est définie par le spécificateur "def" suivi du nom de la fonction et de ses paramètres :

def nomDeLaFonction(liste de paramètres):
    ... 
    bloc d'instructions
    ...
    return resultat
  • Vous pouvez choisir n'importe quel nom pour la fonction que vous créez, à l'exception des mots réservés du langage, et à la condition de n'utiliser aucun caractère spécial ou accentué (le caractère souligné « _ » est permis). Comme c'est le cas pour les noms de variables, il vous est conseillé d'utiliser surtout des lettres minuscules, notamment au début du nom.
  • Comme les instructions if et while, l'instruction def est une instruction composée. La ligne contenant cette instruction se termine obligatoirement par un double point, lequel introduit un bloc d'instructions que vous ne devez pas oublier d'indenter.
  • La liste de paramètres spécifie quelles informations il faudra fournir en guise d'arguments (avec leurs éventuelles valeurs par défaut) lorsque l'on voudra utiliser cette fonction (les parenthèses peuvent parfaitement rester vides si la fonction ne nécessite pas d'arguments).
  • Une fonction s'utilise pratiquement comme une instruction quelconque. Dans le corps d'un programme, un appel de fonction est constitué du nom de la fonction suivi de parenthèses.
  • Une fonction Python ne renvoie pas obligatoirement de résultat : le mot "return" est facultatif. S'il est absent, en termes de programmation on parlera alors plutôt de procédure que de fonction, et elle renverra "None".
  • Le type d'un paramètre sera le même que celui de l'argument qui aura été transmis à la fonction. Exemple :
>>> def afficher3fois(arg):
...     print arg, arg, arg

>>> afficher3fois(5)
5 5 5

>>> afficher3fois('zut')
zut zut zut

>>> afficher3fois([5, 7])
[5, 7] [5, 7] [5, 7]

>>> afficher3fois(6**2)
36 36 36

Fonctionnement

modifier
def factorielle(n):
    f = 1
    i = 1
    while i <= n:
         f = f * i
         i = i + 1
    return f # la valeur retournée

factorielle(7) # 5040

Récursivité

modifier

Une première fonction peut appeler une deuxième fonction, qui elle-même en appelle une troisième, etc. Mais elle peut aussi s'appeler elle-même :

def factorielle(n):
    if n <= 1:
        return 1
    else:
        return n * factorielle(n-1)

factorielle(7) # 5040

Passage d'argument

modifier

Une fonction accepte entre zéro et 255 d'arguments :

>>> def addition(x, y):
        return x + y

addition(3, 4) # 7

>>> def multiplication(x, y):
        return x * y

multiplication(3, 4) # 12

La signature est ici "x" et "y" en paramètre.

Ces arguments peuvent être des variables, mais aussi des fonctions, appelées alors "fonctions de rappel" ou "callbacks". Exemple :

>>> def operation(x, y, f):
        return f(x, y)

operation(3, 4, addition)         # 7
operation(3, 4, multiplication)   # 12

Arguments facultatifs

modifier

Il suffit de définir une valeur par défaut à un argument pour le rendre facultatif. Naturellement, cette valeur est écrasée si l'argument est précisé :

>>> def f(x = None):
        if x:
            print(x)
print f() # None
print f(1) # 1 None

Arguments nommés

modifier

Pour ne pas être obligé de remplir tous les paramètres facultatifs dans l'ordre, il est possible de n'en n'appeler que quelques-uns s'ils sont nommés :

>>> def f(p1 = 0, p2 = 0, p3):
        ...
f(p3 = 1) # 1

Fonction lambda

modifier

Une fonction lambda est une fonction anonyme : elle n'est pas définie par def.

>>> def f(x):
       return x*2

>>> f(3)
6

>>> g = lambda x: x*2  # 1
>>> g(3)
6

>>> (lambda x: x*2)(3) # 2
6

1 et 2 sont des fonctions lambda.

Récupérer les arguments de la ligne de commande

modifier

La variable sys.argv contient les arguments de la ligne de commande, sous forme d'une liste dont le premier élément est le nom du script invoqué. Exemple :

Si le script truc.py contient

 #!/usr/bin/python
 #-*- coding: utf-8 -*-
 import sys
 print("Arguments : ", sys.argv)

alors l'invocation :

$ python truc.py -a rien -n=nervures

produira la sortie :

Arguments :  ['truc.py', '-a', 'rien', '-n=nervures']

Si on veut récupérer l'argument n° 2 :

 #!/usr/bin/python
 #-*- coding: utf-8 -*-
 import sys
 print("Argument 2 : ", sys.argv[2])

produira la sortie :

Argument 2 : 'rien'

Déclarer les arguments

modifier
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('-argument_nommé', '-a', help="description", type=str, default='valeur par défaut')
parser.add_argument('arguments non nommés', nargs='*')
print parser.parse_args()

Variables locales, variables globales

modifier

Lorsque nous définissons des variables à l'intérieur du corps d'une fonction, ces variables ne sont accessibles qu'à la fonction elle-même. On dit que ces variables sont des variables locales à la fonction.

En effet, chaque fois que la fonction est appelée, Python réserve pour elle (dans la mémoire de l'ordinateur) un nouvel espace de noms. Les contenus des variables locales sont stockés dans cet espace de noms qui est inaccessible depuis l'extérieur de la fonction. De plus, cet espace de noms est automatiquement détruit dès que la fonction a terminé son travail, donc si on l'appelle deux fois de suite elle recommence à zéro.

Les variables définies à l'extérieur d'une fonction sont des variables globales. Leur contenu est « visible » de l'intérieur d'une fonction, mais la fonction ne peut pas le modifier. Exemple :

>>> def mask():
...	  p = 20
...	  print p, q
...
>>> p, q = 15, 38
>>> mask()
20 38
>>> print p, q
15 38
Analysons attentivement cet exemple

Nous commençons par définir une fonction très simple (qui n'utilise d'ailleurs aucun paramètre). À l'intérieur de cette fonction, une variable p est définie, avec 20 comme valeur initiale. Cette variable p qui est définie à l'intérieur d'une fonction sera donc une variable locale.

Une fois terminée la définition de la fonction, nous revenons au niveau principal pour y définir les deux variables p et q auxquelles nous attribuons les contenus 15 et 38. Ces deux variables définies au niveau principal seront donc des variables globales.

Ainsi le même nom de variable p a été utilisé ici à deux reprises, pour définir deux variables différentes : l'une est globale et l'autre est locale. On peut constater dans la suite de l'exercice que ces deux variables sont bel et bien des variables distinctes, indépendantes, obéissant à une règle de priorité qui veut qu'à l'intérieur d'une fonction (où elles pourraient entrer en compétition), ce sont les variables définies localement qui ont la priorité.

On constate en effet que lorsque la fonction mask() est lancée, la variable globale q y est accessible, puisqu'elle est imprimée correctement. Pour p, par contre, c'est la valeur attribuée localement qui est affichée.

On pourrait croire d'abord que la fonction mask() a simplement modifié le contenu de la variable globale p (puisqu'elle est accessible). Les lignes suivantes démontrent qu'il n'en est rien : en dehors de la fonction mask(), la variable globale p conserve sa valeur initiale.

Cet état de choses peut toutefois être modifié si vous le souhaitez. Il peut se faire par exemple que vous ayez à définir une fonction qui soit capable de modifier une variable globale. Pour atteindre ce résultat, il vous suffira d'utiliser l'instruction "global". Cette instruction permet d'indiquer - à l'intérieur de la définition d'une fonction - quelles sont les variables à traiter globalement.

Autre exemple

Dans l'exemple ci-dessous, la variable à utiliser à l'intérieur de la fonction "monter()" est non seulement accessible, mais également modifiable, parce qu'elle est signalée explicitement comme étant une variable qu'il faut traiter globalement. Par comparaison, essayez le même exercice en supprimant l'instruction "global" : la variable "a" n'est plus incrémentée à chaque appel de la fonction.

>>> def monter():
...	global a
...	a = a+1
...	print a
...
>>> a = 15
>>> monter()
16
>>> monter()
17
>>>

Utilisation des fonctions dans un script

modifier

Pour cette première approche des fonctions, nous n'avons utilisé jusqu'ici que le mode interactif de l'interpréteur Python.

Il est bien évident que les fonctions peuvent aussi s'utiliser dans des scripts. Veuillez donc essayer vous-même le petit programme ci-dessous, lequel calcule le volume d'une sphère à l'aide de la formule que vous connaissez certainement :  

def cube(n):
   return n**3

def volumeSphere(r):
   return 4 * 3.1416 * cube(r) / 3

r = input('Entrez la valeur du rayon : ')
print 'Le volume de cette sphère vaut', volumeSphere(r)
Notes

À bien y regarder, ce programme comporte trois parties : les deux fonctions cube() et volumeSphere(), et ensuite le corps principal du programme.

Dans le corps principal du programme, il y a un appel de la fonction volumeSphere().

À l'intérieur de la fonction volumeSphere(), il y a un appel de la fonction cube().

Notez bien que les trois parties du programme ont été disposées dans un certain ordre : d'abord la définition des fonctions, et ensuite le corps principal du programme. Cette disposition est nécessaire, parce que l'interpréteur exécute les lignes d'instructions du programme l'une après l'autre, dans l'ordre où elles apparaissent dans le code source. Dans le script, la définition des fonctions doit donc précéder leur utilisation.

Pour vous en convaincre, intervertissez cet ordre (en plaçant par exemple le corps principal du programme au début), et prenez note du type de message d'erreur qui est affiché lorsque vous essayez d'exécuter le script ainsi modifié.

En fait, le corps principal d'un programme Python constitue lui-même une entité un peu particulière, qui est toujours reconnue dans le fonctionnement interne de l'interpréteur sous le nom réservé __main__ (le mot main signifie « principal », en anglais. Il est encadré par des caractères « souligné » en double, pour éviter toute confusion avec d'autres symboles). L'exécution d'un script commence toujours avec la première instruction de cette entité __main__, où qu'elle puisse se trouver dans le listing. Les instructions qui suivent sont alors exécutées l'une après l'autre, dans l'ordre, jusqu'au premier appel de fonction. Un appel de fonction est comme un détour dans le flux de l'exécution : au lieu de passer à l'instruction suivante, l'interpréteur exécute la fonction appelée, puis revient au programme appelant pour continuer le travail interrompu. Pour que ce mécanisme puisse fonctionner, il faut que l'interpréteur ait pu lire la définition de la fonction avant l'entité __main__, et celle-ci sera donc placée en général à la fin du script.

Dans notre exemple, l'entité __main__ appelle une première fonction qui elle-même en appelle une deuxième. Cette situation est très fréquente en programmation. Si vous voulez comprendre correctement ce qui se passe dans un programme, vous devez donc apprendre à lire un script, non pas de la première à la dernière ligne, mais plutôt en suivant un cheminement analogue à ce qui se passe lors de l'exécution de ce script. Cela signifie concrètement que vous devrez souvent analyser un script en commençant par ses dernières lignes !

Modules de fonctions

modifier

Afin que vous puissiez mieux comprendre encore la distinction entre la définition d'une fonction et son utilisation au sein d'un programme, nous vous suggérons de placer fréquemment vos définitions de fonctions dans un module Python, et le programme qui les utilise dans un autre.

Exemple

On souhaite réaliser la série de dessins ci-dessous, à l'aide du module turtle :

 

Écrivez les lignes de code suivantes, et sauvegardez-les dans un fichier auquel vous donnerez le nom dessins_tortue.py :

from turtle import *
 
def carre(taille, couleur):
    "fonction qui dessine un carré de taille et de couleur déterminées"
    color(couleur)
    c =0
    while c <4:
        forward(taille)
        right(90)
        c = c +1

Vous pouvez remarquer que la définition de la fonction carre() commence par une chaîne de caractères. Cette chaîne ne joue aucun rôle fonctionnel dans le script : elle est traitée par Python comme un simple commentaire, mais qui est mémorisé à part dans un système de documentation interne automatique, lequel pourra ensuite être exploité par certains utilitaires et éditeurs « intelligents ».

Si vous programmez dans l'environnement IDLE, par exemple, vous verrez apparaître cette chaîne documentaire dans une « bulle d'aide », chaque fois que vous ferez appel aux fonctions ainsi documentées.

En fait, Python place cette chaîne dans une variable spéciale dont le nom est __doc__ (le mot « doc » entouré de deux paires de caractères « souligné »), et qui est associée à l'objet fonction comme étant l'un de ses attributs (vous en apprendrez davantage au sujet de ces attributs lorsque nous aborderons les classes d'objets).

Ainsi, vous pouvez vous-même retrouver la chaîne de documentation d'une fonction quelconque en affichant le contenu de cette variable. Exemple :

>>> def essai():
...     "Cette fonction est bien documentée mais ne fait presque rien."
...     print "rien à signaler"

>>> essai()
rien à signaler

>>> print essai.__doc__
Cette fonction est bien documentée mais ne fait presque rien.

Prenez donc la peine d'incorporer une telle chaîne explicative dans toutes vos définitions de fonctions futures : il s'agit là d'une pratique hautement recommandable.

Le fichier que vous aurez créé ainsi est dorénavant un véritable module de fonctions Python, au même titre que les modules turtle ou math que vous connaissez déjà. Vous pouvez donc l'utiliser dans n'importe quel autre script, comme celui-ci, par exemple, qui effectuera le travail demandé :

from dessins_tortue import *

up()                    # relever le crayon
goto(-150, 50)          # reculer en haut à gauche 

# dessiner dix carrés rouges, alignés :
i = 0
while i < 10:
    down()              # abaisser le crayon
    carre(25, 'red')    # tracer un carré
    up()                # relever le crayon
    forward(30)         # avancer + loin
    i = i +1

a = input()             # attendre
 

Vous pouvez à priori nommer vos modules de fonctions comme bon vous semble. Sachez cependant qu'il vous sera impossible d'importer un module si son nom est l'un des 29 mots réservés Python, car le nom du module importé deviendrait une variable dans votre script, et les mots réservés ne peuvent pas être utilisés comme noms de variables. Rappelons aussi qu'il vous faut éviter de donner à vos modules - et à tous vos scripts en général - le même nom que celui d'un module Python préexistant, sinon vous devez vous attendre à des conflits. Par exemple, si vous donnez le nom turtle.py à un exercice dans lequel vous avez placé une instruction d'importation du module "turtle", c'est l'exercice lui-même que vous allez importer !

 

Exercices

  1. 1.2.Définissez une fonction ligneCar(n, ca) qui renvoie une chaîne de n caractères ca.
  2. Définissez une fonction surfCercle(R). Cette fonction doit renvoyer la surface (l'aire) d'un cercle dont on lui a fourni le rayon R en argument. Par exemple, l'exécution de l'instruction :
    print surfCercle(2.5) doit donner le résultat 19.635
  3. Définissez une fonction volBoite(x1,x2,x3) qui renvoie le volume d'une boîte parallélépipédique dont on fournit les trois dimensions x1, x2, x3 en arguments. Par exemple, l'exécution de l'instruction :
    print volBoite(5.2, 7.7, 3.3) doit donner le résultat : 132.13
  4. Définissez une fonction maximum(n1,n2,n3) qui renvoie le plus grand de 3 nombres n1, n2, n3 fournis en arguments. Par exemple, l'exécution de l'instruction :
    print maximum(2,5,4) doit donner le résultat : 5
  5. Complétez le module de fonctions graphiques dessins_tortue.py.
    Commencez par ajouter un paramètre angle à la fonction carre(), de manière à ce que les carrés puissent être tracés dans différentes orientations. Définissez ensuite une fonction triangle(taille, couleur, angle) capable de dessiner un triangle équilatéral d'une taille, d'une couleur et d'une orientation bien déterminées.
    Testez votre module à l'aide d'un programme qui fera appel à ces fonctions à plusieurs reprises, avec des arguments variés pour dessiner une série de carrés et de triangles :
     
  6. Ajoutez au module de l'exercice précédent une fonction etoile5() spécialisée dans le dessin d'étoiles à 5 branches. Dans votre programme principal, insérez une boucle qui dessine une rangée horizontale de 9 petites étoiles de tailles variées :
     
  7. Ajoutez au module de l'exercice précédent une fonction etoile6() capable de dessiner une étoile à 6 branches, elle-même constituée de deux triangles équilatéraux imbriqués. Cette nouvelle fonction devra faire appel à la fonction triangle() définie précédemment.
    Votre programme principal dessinera également une série de ces étoiles :
     
     
  8. Définissez une fonction compteCar(ca,ch) qui renvoie le nombre de fois que l'on rencontre le caractère ca dans la chaîne de caractères ch. Par exemple, l'exécution de l'instruction : print compteCar('e','Cette phrase est un exemple') doit donner le résultat : 7
  9. Définissez une fonction indexMax(liste) qui renvoie l'index de l'élément ayant la valeur la plus élevée dans la liste transmise en argument. Exemple d'utilisation :
    serie = [5, 8, 2, 1, 9, 3, 6, 7]
    print indexMax(serie)
    4
  10. Définissez une fonction nomMois(n) qui renvoie le nom du ne mois de l'année.
    Par exemple, l'exécution de l'instruction :
    print nomMois(4) doit donner le résultat : Avril
  11. Définissez une fonction inverse(ch) qui permette d'inverser les l'ordre des caractères d'une chaîne quelconque. (La chaîne inversée sera renvoyée au programme appelant).
  12. Définissez une fonction compteMots(ph) qui renvoie le nombre de mots contenus dans la phrase ph (On considère comme mots les ensembles de caractères inclus entre des espaces).

Solution

  1. Réfléchissez !
  2. from math import pi
    
    def surfCercle(r):
        "Surface d'un cercle de rayon r"
        return pi * r**2
    
    # test :
    print surfCercle(2.5)
    
  3. def volBoite(x1, x2, x3):
        "Volume d'une boîte parallélipipédique"
        return x1 * x2 * x3
    
    # test :
    print volBoite(5.2, 7.7, 3.3)
    
  4. def maximum(n1, n2, n3):
        "Renvoie le plus grand de trois nombres"
        if n1 >= n2 and n1 >= n3:
            return n1
        elif n2 >= n1 and n2 >= n3:
            return n2
        else:
            return n3
    
    # test :
    print maximum(4.5, 5.7, 3.9)
    
  5. Réfléchissez !
  6. Réfléchissez !
  7. Réfléchissez !
  8. def compteCar(ca, ch):
        "Renvoie le nombre de caractères ca trouvés dans la chaîne ch"
        i, tot = 0, 0
        while i < len(ch):
            if ch[i] == ca:
                tot = tot + 1
            i = i + 1
        return tot    
            
    # test :
    print compteCar("e","Cette chaîne est un exemple")
    
  9. def indexMax(tt):
        "renvoie l'indice du plus grand élément de la liste tt"
        i, max = 0, 0
        while i < len(tt):
            if tt[i] > max :
                max, imax = tt[i], i
            i = i + 1    
        return imax
    
    # test :
    serie = [5, 8, 2, 1, 9, 3, 6, 4]
    print indexMax(serie)
    
  10. def nomMois(n):
        "renvoie le nom du n-ième mois de l'année"
        mois = ['Janvier,', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet',
                'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre']
        return mois[n -1]       # les indices sont numérotés à partir de zéro
    
    # test :
    print nomMois(4)
    
  11. Réfléchissez !
  12. Réfléchissez !


Exercices

  1. Modifiez la fonction volBoite(x1,x2,x3) que vous avez définie dans un exercice précédent, de manière à ce qu'elle puisse être appelée avec trois, deux, un seul, ou même aucun argument. Utilisez pour ceux ci des valeurs par défaut égales à) 10.
    Par exemple :
    print volBoite() doit donner le résultat : 1000
    print volBoite(5.2) doit donner le résultat : 520.0
    print volBoite(5.2, 3) doit donner le résultat : 156.0
  2. Modifiez la fonction volBoite(x1,x2,x3) ci-dessus de manière à ce qu'elle puisse être appelée avec un, deux, ou trois arguments. Si un seul est utilisé, la boîte est considérée comme cubique (l'argument étant l'arête de ce cube). Si deux sont utilisés, la boîte est considérée comme un prisme à base carrée. (Dans ce cas le premier argument est le côté du carré, et le second la hauteur du prisme). Si trois arguments sont utilisés, la boîte est considérée comme un parallélépipède. Par exemple :
    print volBoite() doit donner le résultat : -1 (? indication d'une erreur).
    print volBoite(5.2) doit donner le résultat : 140.608
    print volBoite(5.2, 3) doit donner le résultat : 81.12
    print volBoite(5.2, 3, 7.4) doit donner le résultat : 115.44
  3. Définissez une fonction changeCar(ch,ca1,ca2,debut,fin) qui remplace tous les caractères ca1 par des caractères ca2 dans la chaîne de caractères ch, à partir de l'indice debut et jusqu'à l'indice fin, ces deux derniers arguments pouvant être omis (et dans ce cas la chaîne est traitée d'une extrémité à l'autre). Exemples de la fonctionnalité attendue :
    >>> phrase = 'Ceci est une toute petite phrase.'
    >>> print changeCar(phrase, ' ', '*')
    Ceci*est*une*toute*petite*phrase.
    >>> print changeCar(phrase, ' ', '*', 8, 12)
    Ceci est*une*toute petite phrase.
    >>> print changeCar(phrase, ' ', '*', 12)
    Ceci est une*toute*petite*phrase.
    >>> print changeCar(phrase, ' ', '*', fin = 12)
    Ceci*est*une*toute petite phrase.
    
  4. Définissez une fonction eleMax(liste,debut,fin) qui renvoie l'élément ayant la plus grande valeur dans la liste transmise. Les deux arguments debut et fin indiqueront les indices entre lesquels doit s'exercer la recherche, et chacun d'eux pourra être omis (comme dans l'exercice précédent). Exemples de la fonctionnalité attendue :
    >>> serie = [9, 3, 6, 1, 7, 5, 4, 8, 2]
    >>> print eleMax(serie)
    9
    >>> print eleMax(serie, 2, 5)
    7
    >>> print eleMax(serie, 2)
    8 
    >>> print eleMax(serie, fin =3, debut =1)
    6
    

Solution

  1. def volBoite(x1 =10, x2 =10, x3 =10):
        "Volume d'une boîte parallélipipédique"
        return x1 * x2 * x3
    
    # test :
    print volBoite()
    print volBoite(5.2)
    print volBoite(5.2, 3)
    
  2. def volBoite(x1 =-1, x2 =-1, x3 =-1):
        "Volume d'une boîte parallélépipédique"
        if x1 == -1 :
            return x1           # aucun argument n'a été fourni
        elif x2 == -1 :
            return x1**3        # un seul argument -> boîte cubique
        elif x3 == -1 :
            return x1*x1*x2     # deux arguments -> boîte prismatique
        else :
            return x1*x2*x3
    
    # test :
    print volBoite()
    print volBoite(5.2)
    print volBoite(5.2, 3)
    print volBoite(5.2, 3, 7.4)
    
  3. def changeCar(ch, ca1, ca2, debut =0, fin =-1):
        "Remplace tous les caractères ca1 par des ca2 dans la chaîne ch"
        if fin == -1:
            fin = len(ch)
        nch, i = "", 0            # nch : nouvelle chaîne à construire
        while i < len(ch) :
            if i >= debut and i <= fin and ch[i] == ca1:
                nch = nch + ca2
            else :
                nch = nch + ch[i]
            i = i + 1
        return nch
    
    # test :
    print changeCar("Ceci est une toute petite phrase", " ", "*")
    print changeCar("Ceci est une toute petite phrase", " ", "*", 8, 12)
    print changeCar("Ceci est une toute petite phrase", " ", "*", 12)
    
  4. def eleMax(lst, debut =0, fin =-1):
        "renvoie le plus grand élément de la liste lst"
        if fin == -1:
            fin = len(lst)
        max, i = 0, 0
        while i < len(lst):
            if i >= debut and i <= fin and lst[i] > max:
                max = lst[i]
            i = i + 1
        return max
    
    # test :
    serie = [9, 3, 6, 1, 7, 5, 4, 8, 2]
    print eleMax(serie)
    print eleMax(serie, 2)
    print eleMax(serie, 2, 5)
    


À faire... 

annotations[1]

Références

modifier


Modules

Définition

modifier

Vous avez déjà rencontré des fonctions intégrées au langage lui-même, comme la fonction len(), par exemple, qui permet de connaître la longueur d'une chaîne de caractères. Les fonctions intégrées au langage sont relativement peu nombreuses : ce sont seulement celles qui sont susceptibles d'être utilisées très fréquemment. Les autres sont regroupées dans des fichiers séparés que l'on appelle des modules.

Les modules sont donc des fichiers qui regroupent des ensembles de fonctions. En effet, il peut être commode de découper un programme important en plusieurs fichiers de taille modeste pour en faciliter la maintenance et le travail collectif. Une application Python typique sera alors constituée d'un programme principal accompagné de un ou plusieurs modules contenant chacun les définitions d'un certain nombre de fonctions accessoires.

Il existe un grand nombre de modules pré-programmés qui sont fournis d'office avec Python. Vous pouvez en trouver d'autres chez divers fournisseurs. Souvent on essaie de regrouper dans un même module des ensembles de fonctions apparentées que l'on appelle des bibliothèques.

Un module peut être appelé depuis plusieurs programmes, il s'agit d'un fichier .py commençant par son identité (qui ne contient pas de point).

N'importe quel fichier .py peut donc être appelé depuis un autre comme un module[1]. Il peut contenir :

du script
des fonctions
des classes
...

Importer un module

modifier

Pour utiliser des fonctions de modules dans un programme, il faut au début du fichier importer ceux-ci.

Pour ce faire, utiliser la commande "import" :

import os
import codecs

Sur la même ligne (syntaxe déconseillée[2]) :

import os, codecs

Ou encore en sélectionnant certains ou tous les éléments d'un fichier :

from pywikibot import *

À noter : cette dernière méthode est dangereuse, car des objets du module portant le même nom que des objets du programme peuvent s'écraser l'un l'autre. Mieux vaut donc les nommer explicitement :

from pywikibot import pagegenerators

Le module math, par exemple, est fournit avec Python, et contient les définitions de nombreuses fonctions mathématiques telles que sinus, cosinus, tangente, racine carrée, etc. Pour pouvoir utiliser ces fonctions, il vous suffit d'incorporer la ligne suivante au début de votre script :

from math import *

Dans le corps du script lui-même, vous écrirez par exemple :

racine = sqrt(nombre) pour assigner à la variable "racine" la racine carrée de nombre, sinusx = sin(angle) pour assigner à la variable "sinusx" le sinus de angle (en radians !), etc.

Modules personnalisés

modifier

Pour créer son propre module et l'importer dans un autre fichier, il faut :

  • Soit que le module existe sous la forme d'un fichier .py situé dans le même dossier que le fichier qui l'importe. Exemple :
import Fichier1
Fichier1.fonction1()

Pour changer le préfixe :

import Fichier1 as F1
F1.fonction1()

Pour supprimer le préfixe :

from Fichier1 import *
fonction1()
  • Soit qu'il soit dans un autre dossier du programme, contenant un fichier __init__.py joutant le rôle de relais en important tous fichiers situés à côté de lui, ce qui permettra à toutes les fonctions de ce dossier d'être accessibles par le nom du dossier. Exemple dans un dossier "lib" :
from Fichier1 import *
from Fichier2 import *
from Fichier3 import *

__all__ = ["Fichier1", "Fichier2", "Fichier3"]

Dans le dossier parent :

import lib
fonction1()

Liste des modules standards

modifier

Les modules standards les plus importants sont[3] :

  • cgi
  • math
  • os
  • pickle
  • random
  • re
  • socket
  • sys
  • time
  • urllib

Exemple du module "math"

modifier
# Démo : utilisation des fonctions du module <math>

from math import *

nombre = 121
angle = pi/6	# soit 30° (la bibliothèque math inclut aussi la définition de pi) 
print 'racine carrée de', nombre, '=', sqrt(nombre)
print 'sinus de', angle, 'radians', '=', sin(angle)

L'exécution de ce script provoque l'affichage suivant :

racine carrée de 121 = 11.0
sinus de 0.523598775598 radians = 0.5

Ce court exemple illustre déjà fort bien quelques caractéristiques importantes des fonctions :

  • une fonction apparaît sous la forme d'un nom quelconque associé à des parenthèses. Exemple : sqrt()
  • dans les parenthèses, on transmet à la fonction un ou plusieurs arguments. Exemple : sqrt(121)
  • la fonction fournit une valeur de retour (on dira aussi qu'elle « renvoie » une valeur). Exemple : 11.0

Nous allons développer tout ceci dans les pages suivantes. Veuillez noter au passage que les fonctions mathématiques utilisées ici ne représentent qu'un tout premier exemple. Un simple coup d'œil dans la documentation des bibliothèques Python vous permettra de constater que de très nombreuses fonctions sont d'ores et déjà disponibles pour réaliser une multitude de tâches, y compris des algorithmes mathématiques très complexes (Python est couramment utilisé dans les universités pour la résolution de problèmes scientifiques de haut niveau). Il est donc hors de question de fournir ici une liste détaillée. Une telle liste est aisément accessible dans le système d'aide de Python :

Documentation HTML ® Python documentation ® Modules index ® math

Au chapitre suivant, nous apprendrons comment créer nous-mêmes de nouvelles fonctions.

Exercices

(Note : Dans tous ces exercices, utilisez la fonction raw_input() pour l'entrée des données)
  1. Écrivez un programme qui convertisse en mètres par seconde et en km/h une vitesse fournie par l'utilisateur en miles/heure. (Rappel : 1 mile = 1609 mètres)
  2. Écrivez un programme qui calcule le périmètre et l'aire d'un triangle quelconque dont l'utilisateur fournit les 3 côtés. (Rappel : l'aire d'un triangle quelconque se calcule à l'aide de la formule :
      dans laquelle d désigne la longueur du demi-périmètre, et a, b, c celles des trois côtés).
  3. Écrivez un programme qui calcule la période d'un pendule simple de longueur donnée. La formule qui permet de calculer la période d'un pendule simple est  , l représentant la longueur du pendule et g la valeur de l'accélération de la pesanteur au lieu d'expérience.
  4. Écrivez un programme qui permette d'encoder des valeurs dans une liste. Ce programme devrait fonctionner en boucle, l'utilisateur étant invité à entrer sans cesse de nouvelles valeurs, jusqu'à ce qu'il décide de terminer en frappant <enter> en guise d'entrée. Le programme se terminerait alors par l'affichage de la liste. Exemple de fonctionnement :
    Veuillez entrer une valeur : 25
    Veuillez entrer une valeur : 18
    Veuillez entrer une valeur : 6284
    Veuillez entrer une valeur :
    [25, 18, 6284]
    

Solution

  1. # Conversion de miles/heure en km/h et m/s
    
    print "Veuillez entrer le nombre de miles parcourus en une heure : ",
    ch = raw_input()            # en général préférable à input()
    mph = float(ch)             # conversion de la chaîne entrée en nombre réel
    mps = mph * 1609 / 3600     # conversion en mètres par seconde
    kmph = mph * 1.609          # conversion en km/h
    # affichage :
    print mph, "miles/heure =", kmph, "km/h, ou encore", mps, "m/s"
    
  2. # Périmètre et Aire d'un triangle quelconque
    
    from math import sqrt
    
    print "Veuillez entrer le côté a : "
    a = float(raw_input())
    print "Veuillez entrer le côté b : "
    b = float(raw_input())
    print "Veuillez entrer le côté c : "
    c = float(raw_input())
    d = (a + b + c)/2                # demi-périmètre
    s = sqrt(d*(d-a)*(d-b)*(d-c))    # aire (suivant formule)
    
    print "Longueur des côtés =", a, b, c
    print "Périmètre =", d*2, "Aire =", s
    
  3. Réfléchissez !
  4. # Entrée d'éléments dans une liste
    
    tt = []             # Liste à compléter (vide au départ)
    ch = "start"        # valeur quelconque (mais non nulle) 
    while ch != "":
        print "Veuillez entrer une valeur : "
        ch = raw_input()
        if ch != "":
            tt.append(float(ch))        # variante : tt.append(ch)    
    
    # affichage de la liste :
    print tt
    

Références

modifier


Exercices sur les bases du langage

Révision

modifier

Dans ce qui suit, nous n'allons pas apprendre de nouveaux concepts mais simplement utiliser tout ce que nous connaissons déjà pour réaliser de vrais petits programmes.

Contrôle du flux - Utilisation d'une liste simple

modifier

Commençons par un petit retour sur les branchements conditionnels (il s'agit peut-être là du groupe d'instructions le plus important dans n'importe quel langage !) :

# Utilisation d'une liste et de branchements conditionnels

print ("Ce script recherche le plus grand de trois nombres")
print ('Veuillez entrer trois nombres séparés par des virgules : ')
# Note : la fonction list() convertit en liste la séquence de données qu'on
# lui fournit en argument. L'instruction ci-dessous convertira donc les
# données fournies par l'utilisateur en une liste  nn :
nn = list(input())
max, index = nn[0], 'premier'
if nn[1] > max:			# ne pas omettre le double point !
    max = nn[1]
    index = 'second'
if nn[2] > max:
    max = nn[2]
    index = 'troisième'
print ("Le plus grand de ces nombres est", max)
print ("Ce nombre est le", index, "de votre liste.")
#Bonjour je me permets de rajouter mon code ,car le code proposé ne me permet pas de trouver le max dans une liste

print ("Ce script recherche le plus grand de trois nombres")
print ('Veuillez entrer trois nombres un par un : ')

nn=[]
for i in range(3):
    a=int(input())
    nn.append(a)
    
m = max(nn)

if nn[0] == m:
    m = nn[0]
    index = 'premier'
elif nn[1] == m:			# ne pas omettre le double point !
   m = nn[1]
   index = 'second'
else:
    m = nn[2]
    index = 'troisième'
print ("Le plus grand de ces nombres est", m)
print ("Ce nombre est le", index, "de votre liste.")
 Dans cet exercice, vous retrouvez à nouveau le concept de « bloc d'instructions », déjà abondamment commenté aux chapitres 3 et 4, et que vous devez absolument assimiler. Pour rappel, les blocs d'instructions sont délimités par l'indentation. Après la première instruction if, par exemple, il y a deux lignes indentées définissant un bloc d'instructions. Ces instructions ne seront exécutées que si la condition nn[1] > max est vraie.

La ligne suivante, par contre (celle qui contient la deuxième instruction if) n'est pas indentée. Cette ligne se situe donc au même niveau que celles qui définissent le corps principal du programme. L'instruction contenue dans cette ligne est donc toujours exécutée, alors que les deux suivantes (qui constituent encore un autre bloc) ne sont exécutées que si la condition nn[2] > max est vraie.
En suivant la même logique, on voit que les instructions des deux dernières lignes font partie du bloc principal et sont donc toujours exécutées.

Boucle while - Instructions imbriquées

modifier

Continuons dans cette voie en imbriquant d'autres structures :

# Instructions composées <while> - <if> - <elif> - <else>               

print 'Choisissez un nombre de 1 à 3 (ou zéro pour terminer) ',
a = input()
while a != 0:  # l'opérateur != signifie "différent de"
    if a == 1: 
        print "Vous avez choisi un :" 
        print "le premier, l'unique, l'unité ..." 
    elif a == 2: 
        print "Vous préférez le deux :"
        print "la paire, le couple, le duo ..."
    elif a == 3:
        print "Vous optez pour le plus grand des trois :"
        print "le trio, la trinité, le triplet ..."
    else :
        print "Un nombre entre UN et TROIS, s.v.p."
    print 'Choisissez un nombre de 1 à 3 (ou zéro pour terminer) ',
    a = input()
print "Vous avez entré zéro :"
print "L'exercice est donc terminé."

Nous retrouvons ici une boucle while, associée à un groupe d'instructions if, elif et else.

L'instruction while est utilisée ici pour relancer le questionnement après chaque réponse de l'utilisateur (du moins jusqu'à ce que celui-ci décide de « quitter » en entrant une valeur nulle : rappelons à ce sujet que l'opérateur de comparaison  != signifie « est différent de »). Dans le corps de la boucle, nous trouvons le groupe d'instructions if, elif et else (de la ligne 6 à la ligne 16), qui aiguille le flux du programme vers les différentes réponses, ensuite une instruction print et une instruction input() (lignes 17 & 18) qui seront exécutées dans tous les cas de figure : notez bien leur niveau d'indentation, qui est le même que celui du bloc if, elif et else, Après ces instructions, le programme boucle et l'exécution reprend à l'instruction while (ligne 5). Les deux dernières instructions print (lignes 19 & 20) ne sont exécutées qu'à la sortie de la boucle.


Exercices

  1. Que fait le programme ci-dessous, dans les quatre cas où l'on aurait défini au préalable que la variable a vaut 1, 2, 3 ou 15 ?
    if a !=2:
    	print 'perdu'
    elif a ==3:
    	print 'un instant, s.v.p.'
    else :
    	print 'gagné'
    
  2. Que font ces programmes ?
     a = 5
     b = 2
     if (a==5) & (b<2):
     print '"&" signifie "et"; on peut aussi utiliser le mot "and"'
    
     a, b = 2, 4
     if (a==4) or (b!=4):
     print 'gagné'
     elif (a==4) or (b==4):
     print 'presque gagné'
    
     a = 1
     if not a:
     print 'gagné'
     elif a:
     print 'perdu'
    
  3. Reprendre le programme c) avec a = 0 au lieu de a = 1. Que se passe-t-il ? Conclure !
  4. Écrire un programme qui, étant données deux bornes entières a et b, additionne les nombres multiples de 3 et de 5 compris entre ces bornes.
    Prendre par exemple a = 0, b = 32 ® le résultat devrait être alors 0 + 15 + 30 = 45.
    Modifier légèrement ce programme pour qu'il additionne les nombres multiples de 3 ou de 5 compris entre les bornes a et b. Avec les bornes 0 et 32, le résultat devrait donc être : 0 + 3 + 5 + 6 + 9 + 10 + 12 + 15 + 18 + 20 + 21 + 24 + 25 + 27 + 30 = 225.
  5. Déterminer si une année (dont le millésime est introduit par l'utilisateur) est bissextile ou non. (Une année A est bissextile si A est divisible par 4. Elle ne l'est cependant pas si A est un multiple de 100, à moins que A ne soit multiple de 400).
  6. Demander à l'utilisateur son nom et son sexe (M ou F). En fonction de ces données, afficher « Cher Monsieur » ou « Chère Mademoiselle » suivi du nom de l'élève.
  7. Demander à l'utilisateur d'entrer trois longueurs a, b, c. À l'aide de ces trois longueurs, déterminer s'il est possible de construire un triangle. Déterminer ensuite si ce triangle est rectangle, isocèle, équilatéral ou quelconque. Attention : un triangle rectangle peut être isocèle.
  8. Demander à l'utilisateur qu'il entre un nombre. Afficher ensuite : soit la racine carrée de ce nombre, soit un message indiquant que la racine carrée de ce nombre ne peut être calculée.
  9. Convertir une note scolaire N quelconque, entrée par l'utilisateur sous forme de points (par exemple 27 sur 85), en une note standardisée suivant le code suivant :
    Note Appréciation
    N >= 80 % A
    80 % > N >= 60 % B
    60 % > N >= 50 % C
    50 % > N >= 40 % D
    N < 40 % E
  10. Soit la liste suivante :
    ['Jean-Michel', 'Marc', 'Vanessa', 'Anne', 'Maximilien', 'Alexandre-Benoît', 'Louise']
    Ecrivez un script qui affiche chacun de ces noms avec le nombre de caractères correspondant.
  11. Écrire une boucle de programme qui demande à l'utilisateur d'entrer des notes d'élèves. La boucle se terminera seulement si l'utilisateur entre une valeur négative. Avec les notes ainsi entrées, construire progressivement une liste. Après chaque entrée d'une nouvelle note (et donc à chaque itération de la boucle), afficher le nombre de notes entrées, la note la plus élevée, la note la plus basse, la moyenne de toutes les notes.
  12. Ecrivez un script qui affiche la valeur de la force de gravitation s'exerçant entre deux masses de 10000 kg , pour des distances qui augmentent suivant une progression géométrique de raison 2, à partir de 5 cm (0,05 mètre). La force de gravitation est régie par la formule  
    Exemple d'affichage :
    d = .05 m :  la force vaut  2.668 N
    d = .1 m  :  la force vaut  0.667 N
    d = .2 m  :  la force vaut  0.167 N
    d = .4 m  :  la force vaut  0.0417 N
    etc.
    

Solution

  1. Réfléchissez !
  2. Réfléchissez !
  3. Réfléchissez !
  4. # Traitement de nombres entiers compris entre deux limites
    
    print "Veuillez entrer la limite inférieure :",
    a = input()
    print "Veuillez entrer la limite supérieure :",
    b = input()
    s = 0                   # somme recherchée (nulle au départ)
    # Parcours de la série des nombres compris entre a et b :
    n = a                   # nombre en cours de traitement
    while n <= b:
        if n % 3 ==0 and n % 5 ==0:      # variante : 'or' au lieu de 'and'
            s = s + n
        n = n + 1
    
    print "La somme recherchée vaut", s
    
  5. # Années bissextiles
    
    print "Veuillez entrer l'année à tester :",
    a = input()
    
    if a % 4 != 0:
        # a n'est pas divisible par 4 -> année non bissextile
        bs = 0      
    else:
        if a % 400 ==0:
            # a divisible par 400 -> année bissextile
            bs = 1
        elif a % 100 ==0:
            # a divisible par 100 -> année non bissextile
            bs = 0
        else:
            # autres cas ou a est divisible par 4 -> année bissextile
            bs = 1
    if bs ==1:
        ch = "est"
    else:
        ch = "n'est pas"
    print "L'année", a, ch, "bissextile."
    
    Variante (proposée par Alex Misbah) :
    a=input('entrée une année:')
    
    if (a%4==0) and ((a%100!=0) or (a%400==0)):
        print a,"est une année bissextile"
    else:
        print a,"n'est pas une année bissextile"
    
    Variante (de Mik)
    a=input('année:')
    if (a%4==0 and a%100!=0)or(a%400==0):
        print "bi6"
    else:
        print "nbi6"
    
  6. Réfléchissez !
  7. from sys import exit      # module contenant des fonctions système
    
    print """
    Veuillez entrer les longueurs des 3 côtés
    (en séparant ces valeurs à l'aide de virgules) :"""
    a, b, c = input()
    # Il n'est possible de construire un triangle que si chaque côté
    # a une longueur inférieure à la somme des deux autres :
    if a < (b+c) and b < (a+c) and c < (a+b) :
        print "Ces trois longueurs déterminent bien un triangle."
    else:
        print "Il est impossible de construire un tel triangle !"
        exit()          # ainsi l'on n'ira pas plus loin. 
    
    f = 0
    if a == b and b == c :
        print "Ce triangle est équilatéral."
        f = 1
    elif a == b or b == c or c == a :
        print "Ce triangle est isocèle."
        f = 1
    if a*a + b*b == c*c or b*b + c*c == a*a or c*c + a*a == b*b :
        print "Ce triangle est rectangle."
        f = 1
    if f == 0 :
        print "Ce triangle est quelconque."
    
    Variante (de Mik)
    a,b= input('a:'),input('b:')
    if b>a:
        print '"c":](',b-a,');(',a+b,')['
    else:
        print '"c":](',a-b,');(',a+b,')['
    c=input('c:')
    if a < (b+c) and b < (a+c) and c < (a+b) :
        print 'bien un triangle'
    else:
        print 'impossible constrution triangle'
        exit()
    if a>b and a>c:
        max=a
        x=c*c+b*b
    elif b>a and b>c:
        max=b
        x=c*c+a*a
    elif c>b and c>a:
        max=c
        x=a*a+b*b
    if a==b and b==c :
        print "triangle équilatéral."
    elif a==b or b==c or c==a :
        print "triangle isocèle."
    elif x==max**2 :
        print "triangle rectangle."
    else:
        print "triangle quelconque."
    
  8. Réfléchissez !
  9. Réfléchissez !
  10. Réfléchissez !
  11. # Notes de travaux scolaires
    
    notes = []           # liste à construire  
    n = 2                # valeur positive quelconque pour initier la boucle
    while n >= 0 :
        print "Entrez la note suivante, s.v.p. : ",
        n = float(raw_input())      # conversion de l'entrée en un nombre réel
        if n < 0 :
            print "OK. Terminé."
        else:    
            notes.append(n)         # ajout d'une note à la liste
            # Calculs divers sur les notes déjà entrées :
            # valeurs minimale et maximale + total de toutes les notes. 
            min = 500               # valeur supérieure à toute note
            max, tot, i = 0, 0, 0        
            nn = len(notes)         # nombre de notes déjà entrées
            while i < nn:
                if notes[i] > max:
                    max = notes[i]
                if notes[i] < min:
                    min = notes[i]
                tot = tot + notes[i]
                moy = tot/nn
                i = i + 1
            print nn, "notes entrées. Max =", max, "Min =", min, "Moy =", moy
    
  12. Réfléchissez !

Exercices

modifier

Exercice 1

modifier

Écrire un programme qui affiche "Bonjour le monde".

Exercice 2

modifier

Écrire un programme qui permet de saisir le nom de l'utilisateur et de renvoyer "Bonjour", suivi de ce nom.

(3 solutions possibles)

Exercice 3

modifier

Écrire un programme qui demande à l'utilisateur la saisie de a et b et affiche la somme de a et de b.

Exercice 4

modifier

Écrire un programme qui demande à l'utilisateur son année de naissance et qui affiche son âge. L'année courante sera mise dans une variable.

Exercice 5

modifier

Écrire un programme qui demande à l'utilisateur les coordonnées de deux points dans le plan et qui calcule puis affiche la distance entre ces deux points selon la formule :  



Exercices sur les bases du langage/Palindrome

  def is_palindorme(mot):
     return mot == mot[::-1]

 monMot = input("votre mot: ")

 if is_palindrome(monMot):
     print("est palindrome")
 else:
     print("n'est pas palindrome et donne: ",monMot[::-1])


Regex

En informatique, une expression régulière ou expression rationnelle ou expression normale ou motif, est une chaîne de caractères, qui décrit, selon une syntaxe précise, un ensemble de chaînes de caractères possibles. Les expressions régulières sont également appelées regex (de l'anglais regular expression). Elles sont issues des théories mathématiques des langages formels. Les expressions régulières sont aujourd’hui utilisées pour la lecture, le contrôle, la modification, et l'analyse de textes ainsi que la manipulation des langues formelles que sont les langages informatiques.

L'exemple d'expression régulière suivant permet de valider qu'une chaîne de caractère correspond à la syntaxe d'un nombre entier non signé, c'est à dire une suite non vide de chiffres :

[0-9]+

En détails :

  • Les crochets spécifient l'ensemble des caractères auquel doit appartenir le caractère courant de la chaîne. Dans cet exemple, l'ensemble est celui des chiffres de 0 à 9 inclus.
  • Le caractère plus indique de répéter le motif précédent au moins une fois (suite non vide).


Les expressions régulières en Python nécessitent d'importer le module natif re[1], ou bien l'installation du module externe regex[2] si besoin des regex Unicode tels que \X.

import re
chaine = "12345"
if re.compile('[0-9]+').match(chaine):
	print "Entier positif"

Syntaxe

modifier

Les expressions rationnelles peuvent être analysées et testées via un débogueur en ligne comme https://regex101.com/.

Expressions rationnelles courantes
Caractère Type Explication
. Point N'importe quel caractère
[...] crochets classe de caractères : tous les caractères énumérés dans la classe, avec possibilité de plages dont les bornes sont séparées par "-". Ex : [0-9a-z] pour tout l'alphanumérique en minuscule, ou [0-Z] pour tous les caractères de la table Unicode entre "0" et "Z", c'est-à-dire l'alphanumérique majuscule plus ":;<=>?@"[3].
[^...] crochets et circonflexe classe complémentée : tous les caractères sauf ceux énumérés.
[...[...]] union Union des deux ensembles
[...&&[...]] intersection Intersection des deux ensembles
^ circonflexe Marque le début de la chaîne ou de la ligne.
$ dollar Marque la fin de la chaîne ou de la ligne.
| barre verticale Alternative - ou reconnaît l'un ou l'autre
(...) parenthèses groupe de capture : utilisé pour limiter la portée d'un masque ou de l'alternative, grouper un motif répété ou capturer une séquence
\n référence Même séquence que celle capturée précédemment par le nème groupe de capture
\g{n} référence Même séquence que celle capturée précédemment par le nème groupe de capture
(?P<nom>pattern) Sous-motif nommé Nomme le résultat d'un groupe de capture par un nom.
\g{nom} référence Même séquence que celle capturée précédemment par le groupe de capture nommé nom.
\k<nom> référence Même séquence que celle capturée précédemment par le groupe de capture nommé nom.

Par défaut, les caractères et groupes ne sont pas répétés. Les quantificateurs permettent de spécifier le nombre de répétitions et sont spécifiés immédiatement après le caractère ou groupe concerné.

Quantificateurs
Caractère Type Explication
* astérisque 0, 1 ou plusieurs occurrences
+ plus 1 ou plusieurs occurrences
? interrogation 0 ou 1 occurrence
{...} accolades nombre de répétitions : spécifie le nombre de répétitions du motif précédent (minimum et maximum). Avec la présence de la virgule, quand le minimum est absent la valeur par défaut est zéro, quand le maximum est absent la valeur pas défaut est l'infini. Sans virgule (un seul nombre) il s'agit du nombre exact (minimum et maximum ont la même valeur). Exemples :
  • a{2} deux occurrences de "a",
  • a{1,10} (sans espace) entre une et dix,
  • a{,10} jusqu'à 10 fois (de 0 à 10),
  • a{3,} au moins 3 fois (de 3 à l'infini).

Par défaut les quantificateurs ne recherchent pas forcément la plus longue séquence de répétition possible. Il est possible de les suffixer avec un caractère pour modifier leur comportement.

Modificateurs de quantificateurs
Caractère Type Explication
? réticent Le quantificateur qui précède recherchera la plus petite séquence possible.
+ possessif Le quantificateur qui précède recherchera la plus grande séquence possible.

Remarques :

  • Les caractères de début et fin de chaîne (^ et $) ne fonctionnent pas dans [] où ils ont un autre rôle.
  • Les opérateurs * et + sont toujours avides, pour qu'ils laissent la priorité il faut leur apposer un ? à leur suite[4].
Classes de caractères POSIX[5]
Classe Signification
[[:alpha:]] n'importe quelle lettre
[[:digit:]] n'importe quel chiffre
[[:xdigit:]] caractères hexadécimaux
[[:alnum:]] n'importe quelle lettre ou chiffre
[[:space:]] n'importe quel espace blanc
[[:punct:]] n'importe quel signe de ponctuation
[[:lower:]] n'importe quelle lettre en minuscule
[[:upper:]] n'importe quelle lettre capitale
[[:blank:]] espace ou tabulation
[[:graph:]] caractères affichables et imprimables
[[:cntrl:]] caractères d'échappement
[[:print:]] caractères imprimables exceptés ceux de contrôle
Expressions rationnelles Unicode[6]
Expression Signification
\\ Antislash
\C Caractère spécial C non interprété : [ ] { } ( ) ? * . : \ & - ^ $
\Q...\E Séquence littérale non interprétée
\0xxx Caractère Unicode (1 à 3 chiffres octaux)
\a Alarme (ASCII 07)
\A Début de chaîne
\b Caractère de début ou fin de mot
\B Caractère qui n'est pas début ou fin de mot
\cX Caractère de contrôle ASCII (X étant une lettre)
\d Chiffre
\D Non chiffre
\e Escape (ASCII 1B)
\f Form-feed (ASCII 0C)
\G Fin de la correspondance précédente
\h Espace blanc horizontal [ \t\xA0\u1680\u180e\u2000-\u200a\u202f\u205f\u3000]
\H Non espace blanc horizontal [^\h]
\n Fin de ligne
\pL, \p{L}, \p{Letter} Lettre (dans tout langage)
\r Retour charriot
\R Retour à la ligne, équivaut à \u000D\u000A|[\u000A\u000B\u000C\u000D\u0085\u2028\u2029]
\s Caractères espace [ \t\n\x0B\f\r]
\S Non caractères espace [^\s]
\t Tabulation
\uxxxx Caractère Unicode (4 chiffres hexadécimaux)
\v Espace blanc vertical [\n\x0B\f\r\x85\u2028\u2029]
\V Non espace blanc vertical [^\v]
\w Caractère alphanumérique : lettre, chiffre ou underscore
\W Caractère qui n'est pas lettre, chiffre ou underscore
\xxx Caractère Unicode (2 chiffres hexadécimaux)
\x{xx...x} Caractère Unicode (chiffres hexadécimaux)
\X Caractère Unicode du groupe de graphèmes étendu
\z Fin de chaîne

Constructeurs spéciaux : Ces fonctions précèdent l'expression à laquelle elles s'appliquent, et le tout doit être placé entre parenthèses.

  • ?: : groupe non capturant. Ignorer le groupe de capture lors de la numérotation des backreferences. Exemple : ((?:sous-chaine_non_renvoyée|autre).*).
    La présence d'un groupe capturant peut engendrer une allocation mémoire supplémentaire. Si une expression régulière particulièrement complexe provoque une erreur de mémoire, essayez de remplacer les groupes capturant non référencés et inutilisés par des groupes non-capturant en ajoutant ?: juste après la parenthèse ouvrante, et en décalant les numéros des groupes référencés.
  • ?> : groupe non capturant indépendant.
  • ?<= : positive lookbehind, vérifier (sans consommer) que ce qui précède correspond au motif spécifié. Exemple :
    Chercher une lettre u précédée d'une lettre q : (?<=q)u
  • ?<! : negative lookbehind, vérifier (sans consommer) que ce qui précède ne correspond pas au motif spécifié.
  • ?= : positive lookahead, vérifier (sans consommer) que ce qui suit correspond au motif spécifié.
  • ?! : negative lookahead, vérifier (sans consommer) que ce qui suit ne correspond pas au motif spécifié. Exemples :
    Chercher une lettre q non suivie d'une lettre u : q(?!u)
    ((?!sous-chaine_exclue).)
    <(?!body).*> : pour avoir toutes les balises HTML sauf "body".
    début((?!mot_exclu).)*fin[7] : pour rechercher tout ce qui ne contient pas un mot entre deux autres.
    (?!000|666) : pour exclure 000 et 666[8].

Options :

Les options d'interprétation sont en général spécifiées à part. Mais certaines API ne permettent pas de les spécifier. Il est possible d'insérer ces options dans l'expression régulière[9].

(?optionsactivées-optionsdésactivées)

Exemples :

  • Chercher un mot composé de voyelles sans tenir compte de la casse :
    (?i)[AEIOUY]+
  • Chercher un mot composé de voyelles en tenant compte de la casse, ici en majuscules :
    (?-i)[AEIOUY]+

Les options s'appliquent à toute l'expression quelle que soit leur position dans l'expression.


  • (?:ma_chaine)* : groupe optionnel.
  • \1 : résultat du premier groupe de capture dans les remplacements (\2 correspond au deuxième, etc.).

Recherche

modifier
  • compile() renvoie None si l'expression rationnelle n'est pas trouvée dans la chaîne.
  • search() renvoie la position des chaînes recherchées.
#!/usr/bin/env python
import re
chaine = "Test regex Python pour Wikibooks francophone."
if re.compile('Wikibooks').search(chaine):
	print "Position du mot Wikibooks : "
	print re.search(u'Wikibooks', chaine).start()
        # Affiche "23"
        print re.search(u'Wikibooks', chaine).end()
        # Affiche "32"

Pour voir le motif compilé : re.compile('Wikibooks').pattern

  • findall() trouve toutes les correspondances dans un tableau.
  • finditer() trouve toutes les correspondances dans un itérateur.
#!/usr/bin/env python
# Affiche tous les mots qui commencent par "Wiki"
import re
chaine = "Wikilivre regex Python pour Wikibooks francophone."
print (re.findall(r"Wiki\w+", chaine))
# Affiche ['Wikilivre', 'Wikibooks']


Les parenthèses imbriquées permettent d'indiquer des mots facultatifs au sein d'un groupe de capture. Ex :

#!/usr/bin/env python
# Trouve à un mot prêt
import re
chaine = "Wikilivre regex Python pour Wikibooks francophone."

regex = r'(Python pour Wikibooks)'
print re.search(regex, chaine).start() # 16

regex = r'(Python (pour )*Wikibooks)'
print re.search(regex, chaine).start() # 16

regex = r'(Python pour (les )*Wikibooks)'
print re.search(regex, chaine).start() # 16

group()

modifier

Pour accéder aux résultats des groupes de capture, utiliser group() en partant de 1 (0 étant le match du motif entier) :

#!/usr/bin/env python
import re
chaine = "Wikilivre regex Python pour Wikibooks francophone."
s = re.search(r'(Wiki[a-z]*).*(Wiki[a-z]*)', chaine)
if s:
    print s.group(0)
    # Affiche 'Wikilivre regex Python pour Wikibooks'
    print s.group(1)
    # Affiche 'Wikilivre'
    print s.group(2)
    # Affiche 'Wikibooks'

Le comportement de certaines expressions peut être reconfiguré en ajoutant un "flag" en paramètre des méthodes[10].

re.IGNORECASE

modifier

Ignore la casse. Ainsi dans l'exemple précédent nous pouvions aussi faire :

 s = re.search(r'(wiki[a-z]*).*(wiki[a-z]*)', chaine, re.IGNORECASE)

re.MULTILINE

modifier

Par défaut, les caractères "^" et "$" désignent le début et la fin de tout le texte. Or, en mode multiligne, un "^" en début de re.search() considérera le début de chaque ligne, et "$" leurs fins.

Pour partir uniquement du début de la chaîne globale, il faut alors ne plus utiliser "re.search()" mais "re.match()"[11].

re.DOTALL

modifier

Par défaut, .* et .+ s'arrêtent aux retours chariot (\n). Pour qu'ils englobent ces retours à la ligne, il faut appeler re.DOTALL. Exemple :

 if re.search(regex, text, re.MULTILINE| re.DOTALL):

Remplacement

modifier
#!/usr/bin/env python
# Remplace tous les espaces par des underscores
import re
chaine = "Test regex Python pour Wikibooks francophone."
chaineTriee = re.sub(r' ', "_", chaine)
print chaineTriee
# Affiche "Test_regex_Python_pour_Wikibooks_francophone."

Pour remplacer certains éléments en conservant ceux placés entre parenthèses, il faut les désigner par \1, \2, \3...

#!/usr/bin/env python
# Ajoute des guillemets à tous les mots suivent "livre"
import re
chaine = "Test regex Python pour le livre Python de Wikibooks francophone."
chaineTriee = re.sub(r'(.*)livre (\w+)(.*)', r'\1livre "\2"\3', chaine)
print chaineTriee
# Affiche "Test regex Python pour le livre "Python" de Wikibooks francophone."

Remarque : si les paramètres (\1, \2...) sont remplacés par le symbole �, vérifier que la chaine regex est bien encodée avec r.

 

Les différents contenus d'un même groupe de capture sont remplacés par le premier \1. Pour éviter cela, il faut les traiter un par un avec "finditer()".

 

Dans un contexte multi-ligne, re.sub() ne recherche pas tout comme re.search() (qui a un global flag).

Exemple : remplacement de la balise "font color=" par "span style=font-size:".

    text = r'<font color=green>Vert</font> / <font color=red>rouge</font>'
    regex = r'<font color=([^>]*)>'
    pattern = re.compile(regex, re.UNICODE)
    for match in pattern.finditer(text):
        print u'Remplacement de ' + match.group(0) + u' par <span style="font-color:' + match.group(1) + u'">'
        text = text.replace(match.group(0), u'<span style="font-color:' + match.group(1) + u'">')
        text = text.replace('</font>', u'</span>')
    input(text)

Exemples de formules

modifier
  • Récupérer le premier modèle 1 wiki non imbriquée dans un autre modèle :
page = u'{{Modèle2|Paramètre2, {{Modèle1|Paramètre3}} }}, {{Modèle1|Paramètre4}}'
regex = r'({{(.*?)}}|.)*[^}]*'
input(re.sub(regex, r'\2', page))

Pour indiquer un nombre précis d'occurrences, utiliser "{nombre}". Ex :

#!/usr/bin/env python
import re
chaine = 'w.1, ww.2, www.3, wwww.4'
print re.sub(r' w{3}\.', ' http://www.', chaine)
w.1, ww.2, http://www.3, wwww.4

Idem pour une plage de nombres : {min,max}.

 

Quand on injecte une variable dans un motif, il faut échapper ses caractères interprétables avec re.escape().

Références

modifier


Programmation orientée objet pour les non-programmeurs

Le concept objet

modifier

Dans les environnements de développement informatique, il a fallu attendre assez longtemps pour voir émerger le concept de l'objet. Son apparition a permis la création de systèmes beaucoup plus complexes mais aussi très empreints de mimétisme. En effet, dans notre monde réel, nous sommes tous entourés d'objets qui ont très souvent deux critères d'appréciation.

Le critère descriptif

modifier

Ce premier est universel, il contient toutes les caractéristiques qui décrivent l'objet. Nous prendrons comme exemple un dé, si nous avions à le décrire, nous dirions qu'il possède 6 faces avec un chiffre allant de 1 à 6 sur chacune d'elles, que la somme de deux valeurs étant sur des faces opposées vaut 7, que chaque chiffre entre un et six y est repris une et une seule fois, qu'il est (souvent) de couleur rouge et de petite taille. Il serait possible de le décrire plus précisément, mais en réalité, indiquer qu'il est fait de bois, que les nombres sont représentés par une quantité de point qui leur est égal, qu'il dispose de coin arrondi... n'aurait pas été plus éloquent.

Le critère d'interaction

modifier

Le deuxième critère est celui d'interaction, il indique l'utilité de l'objet, les possibilités qu'il vous offre. Pour le dé nous pourrions indiquer que celui-ci peut rouler, mais ce n'est pas son rôle. De même, dans certaines circonstances, celui-ci peut vous servir de cale, mais ici encore, nous nous éloignons du sujet. Objectivement, le dé a pour rôle de donner un nombre compris entre son minimum et son maximum (inclus) au hasard. D'ailleurs, on peut ajouter que cela arrive après l'avoir lancé.

L'héritage et l'implémentation

modifier

Ici, nous avons décrit un objet, et il nous suffit de faire de même en informatique. Mais nous pourrions approfondir la description en indiquant aussi que le dé est en fait dérivé d'un objet de base : le cube. Ainsi nous pourrions dire que le dé :

  • est un cube.
  • est de couleur rouge.
  • peut être lancé pour renvoyer un nombre compris entre 1 et 6 (le nombre de face qui le compose).

puis expliquer que le cube :

  • est un volume géométrique à trois dimensions.
  • est constitué de 6 carrés.

puis bien sûr qu'un carré :

  • est une figure géométrique à deux dimensions.


Et nous pourrions continuer en précisant le terme dimension mais dans notre cas ce n'est pas utile. Nous pouvons ainsi établir le schémas suivant : le dé hérite des caractéristiques du cube (c'est un cube). Mais on ne peut pas dire que le cube hérite des caractéristiques du carré. En effet, on indique bien qu'il est constitué de mais pas qu'il est et c'est la toute la différence, vous êtes constitué de deux bras musclés mais vous n'êtes pas deux bras musclés (sauf si vous êtes déménageur... c'est une blague bien entendu, les déménageurs sont suffisamment allègre pour ne pas lancer un avis de recherche sur ma tête ) ! Nous dirons donc que :

  • l'objet cube implémente l'objet surface carré
  • l'objet hérite de l'objet cube

Un objet est une entité que l'on construit par instanciation à partir d'une classe (c'est-à-dire en quelque sorte une « catégorie » ou un « type » d'objet). Par exemple, on peut trouver dans la bibliothèque Tkinter, une classe Button() à partir de laquelle on peut créer dans une fenêtre un nombre quelconque de boutons.

Nous allons à présent examiner comment vous pouvez vous-mêmes définir de nouvelles classes d'objets. Il s'agit là d'un sujet relativement ardu, mais vous l'aborderez de manière très progressive, en commençant par définir des classes d'objets très simples, que vous perfectionnerez ensuite. Attendez-vous cependant à rencontrer des objets de plus en plus complexes par après.

Comme les objets de la vie courante, les objets informatiques peuvent être très simples ou très compliqués. Ils peuvent être composés de différentes parties, qui soient elles-mêmes des objets, ceux-ci étant faits à leur tour d'autres objets plus simples, etc.

Utilité des classes

modifier

Les classes sont les principaux outils de la programmation orientée objet ou POO (Object Oriented Programming ou OOP en anglais). Ce type de programmation permet de structurer les logiciels complexes en les organisant comme des ensembles d'objets qui interagissent, entre eux et avec le monde extérieur.

Le premier bénéfice de cette approche de la programmation consiste dans le fait que les différents objets utilisés peuvent être construits indépendamment les uns des autres (par exemple par des programmeurs différents) sans qu'il n'y ait de risque d'interférence. Ce résultat est obtenu grâce au concept d'encapsulation : la fonctionnalité interne de l'objet et les variables qu'il utilise pour effectuer son travail, sont en quelque sorte « enfermés » dans l'objet. Les autres objets et le monde extérieur ne peuvent y avoir accès qu'à travers des procédures bien définies.

En particulier, l'utilisation de classes dans vos programmes vous permettra - entre autres choses - d'éviter au maximum l'emploi de variables globales. Vous devez savoir en effet que l'utilisation de variables globales comporte des risques, surtout dans les programmes volumineux, parce qu'il est toujours possible que de telles variables soient modifiées ou même redéfinies n'importe où dans le corps du programme (et ce risque s'aggrave particulièrement si plusieurs programmeurs différents travaillent sur un même logiciel).

Un second bénéfice résultant de l'utilisation des classes est la possibilité qu'elles offrent de construire de nouveaux objets à partir d'objets préexistants, et donc de réutiliser des pans entiers d'une programmation déjà écrite (sans toucher à celle-ci !), pour en tirer une fonctionnalité nouvelle. Cela est rendu possible grâce aux concepts de dérivation et de polymorphisme.

  • La dérivation est le mécanisme qui permet de construire une classe « enfant » au départ d'une classe « parente ». L'enfant ainsi obtenu hérite toutes les propriétés et toute la fonctionnalité de son ancêtre, auxquelles on peut ajouter ce que l'on veut.
  • Le polymorphisme permet d'attribuer des comportements différents à des objets dérivant les uns des autres, ou au même objet ou en fonction d'un certain contexte.

La programmation orientée objet est optionnelle sous Python. Vous pouvez donc mener à bien de nombreux projets sans l'utiliser, avec des outils plus simples tels que les fonctions. Sachez cependant que les classes constituent des outils pratiques et puissants. Une bonne compréhension des classes vous aidera notamment à maîtriser le domaine des interfaces graphiques (Tkinter, wxPython), et vous préparera efficacement à aborder d'autres langages modernes tels que C++ ou Java.

Objet = [ attributs + méthodes ]

Cette façon d'associer dans une même « capsule » les propriétés d'un objet et les fonctions qui permettent d'agir sur elles, correspond chez les concepteurs de programmes à une volonté de construire des entités informatiques dont le comportement se rapproche du comportement des objets du monde réel qui nous entoure.

Considérons par exemple un widget « bouton ». Il nous paraît raisonnable de souhaiter que l'objet informatique que nous appelons ainsi ait un comportement qui ressemble à celui d'un bouton d'appareil quelconque dans le monde réel. Or la fonctionnalité d'un bouton réel (sa capacité de fermer ou d'ouvrir un circuit électrique) est bien intégrée dans l'objet lui-même (au même titre que d'autres propriétés telles que sa taille, sa couleur, etc.) De la même manière, nous souhaiterons que les différentes caractéristiques de notre bouton logiciel (sa taille, son emplacement, sa couleur, le texte qu'il supporte), mais aussi la définition de ce qui se passe lorsque l'on effectue différentes actions de la souris sur ce bouton, soient regroupés dans une entité bien précise à l'intérieur du programme, de manière telle qu'il n'y ait pas de confusion avec un autre bouton ou d'autres entités.



Classes

Définition d'une classe élémentaire

modifier

Pour créer une nouvelle classe d'objets Python, donc un nouveau type de donnée, on utilise l'instruction "class". Les définitions de classes peuvent être situées n'importe où dans un programme, mais on les placera en général au début (ou bien dans un module à importer).

Par exemple, nous allons maintenant créer un nouveau type composite : le type "Point". Ce type correspondra au concept de point en mathématiques. Dans un espace à deux dimensions, un point est caractérisé par deux nombres (ses coordonnées suivant x et y). En notation mathématique, on représente donc un point par ses deux coordonnées x et y enfermées dans une paire de parenthèses. On parlera par exemple du point (25, 17). Une manière naturelle de représenter un point sous Python serait d'utiliser pour les coordonnées deux valeurs de type float. Nous voudrions cependant combiner ces deux valeurs dans une seule entité, ou un seul objet. Pour y arriver, nous allons définir une classe Point() :

>>> class Point:
        "Définition d'un point mathématique"

Remarquons d'emblée que :

  • L'instruction class est un nouvel exemple d’instruction composée. Ce bloc doit contenir au moins une ligne. Dans notre exemple, cette ligne n'est rien d'autre qu'un simple commentaire. Par convention, si la première ligne suivant l'instruction class est une chaîne de caractères, celle-ci sera considérée comme un commentaire et incorporée automatiquement dans un dispositif de documentation des classes qui fait partie intégrante de Python. Prenez donc l'habitude de toujours placer une chaîne décrivant la classe à cet endroit.
  • Rappelez-vous aussi la convention qui consiste à toujours donner aux classes des noms qui commencent par une majuscule. Dans la suite de ce texte, nous respecterons encore une autre convention qui consiste à associer à chaque nom de classe une paire de parenthèses, comme nous le faisons déjà pour les noms de fonctions.

Nous pouvons dès à présent nous servir de cette classe pour créer des objets de ce type, par instanciation. Créons par exemple un nouvel objet p9.

 Sous Python, on peut donc instancier un objet à l'aide d'une simple instruction d'affectation. D'autres langages imposent l'emploi d'une instruction spéciale, souvent appelée "new" pour bien montrer que l'on crée un nouvel objet à partir d'un moule. Exemple : p9 = new Point().
>>> p9 = Point()

Après cette instruction, la variable p9 contient la référence d'un nouvel objet Point(). Nous pouvons dire également que p9 est une nouvelle instance de la classe Point().

 Comme les fonctions, les classes auxquelles on fait appel dans une instruction doivent toujours être accompagnées de parenthèses (même si aucun argument n'est transmis).

Remarquez bien cependant que la définition d'une classe ne nécessite pas de parenthèses (contrairement à ce qui est de règle lors de la définition des fonctions), sauf si nous souhaitons que la classe en cours de définition dérive d'une autre classe préexistante.

Attributs (ou variables) d'instance

modifier

L'objet que nous venons de créer est une coquille vide. Nous pouvons ajouter des composants à cet objet par simple assignation, en utilisant le système de qualification des noms par points.

 Ce système de notation est similaire à celui que nous utilisons pour désigner les variables d'un module, comme par exemple "math.pi" ou "string.uppercase". Les modules peuvent en effet contenir des fonctions, mais aussi des classes et des variables. Essayez par exemple :
import  string
print string.uppercase # ABCDEFGHIJKLMNOPQRSTUVWXYZ
print string.lowercase # abcdefghijklmnopqrstuvwxyz
print string.hexdigits # 0123456789abcdefABCDEF


Complétons la classe précédente avec les coordonnées d'un point :

class Point:
    x = 0
    y = 0

p9 = Point()
p9.x = 3.0
p9.y = 4.0
print (p9.x, p9.y)
(3.0, 4.0)
 
Schéma de variables d'instance

Les variables ainsi définies sont des attributs de l'objet p9, ou encore des variables d'instance. Elles sont incorporées, ou plutôt encapsulées dans l'objet. Le diagramme d'état ci-contre montre le résultat de ces affectations : la variable p9 contient la référence indiquant l'emplacement mémoire du nouvel objet, qui contient lui-même les deux attributs x et y.

On peut utiliser les attributs d'un objet dans n'importe quelle expression, comme toutes les variables ordinaires :

>>> print p9.x
3.0
>>> print p9.x**2 + p9.y**2	
25.0

Du fait de leur encapsulation dans l'objet, les attributs sont des variables distinctes d'autres variables qui pourraient porter le même nom. Par exemple, l'instruction x = p9.x signifie : « extraire de l'objet référencé par p9 la valeur de son attribut x, et assigner cette valeur à la variable x ».

Il n'y a pas de conflit entre la variable x et l'attribut x de l'objet p9. L'objet p9 contient en effet son propre espace de noms, indépendant de l'espace de nom principal où se trouve la variable x.

 Nous venons de voir qu'il est très aisé d'ajouter un attribut à un objet en utilisant une simple instruction d'assignation telle que p9.x = 3.0. On peut se permettre cela sous Python (c'est une conséquence de l'assignation dynamique des variables), mais cela n'est pas vraiment recommandable. En effet, nous n'utiliserons cette façon de faire uniquement dans le but de simplifier nos explications concernant les attributs d'instances.

Passage d'objets comme arguments lors de l'appel d'une fonction

modifier

Les fonctions peuvent utiliser des objets comme paramètres (elles peuvent également fournir un objet comme valeur de retour). Par exemple, vous pouvez définir une fonction telle que celle-ci :

>>> def affiche_point(p):
        print "coord. horizontale =", p.x, "coord. verticale =", p.y

Le paramètre p utilisé par cette fonction doit être un objet de type Point(), puisque l'instruction qui suit utilise les variables d'instance p.x et p.y. Lorsqu'on appelle cette fonction, il faut donc lui fournir un objet de type Point() comme argument. Essayons avec l'objet p9 :

>>> affiche_point(p9)
coord. horizontale = 3.0 coord. verticale = 4.0

Exercices

  1. Écrivez une fonction distance() qui permette de calculer la distance entre deux points. Cette fonction attendra évidemment deux objets Point() comme arguments.

Solution

  1. Réfléchissez !


Similitude et unicité

modifier

Dans la langue parlée, les mêmes mots peuvent avoir des significations fort différentes suivant le contexte dans lequel on les utilise. La conséquence en est que certaines expressions utilisant ces mots peuvent être comprises de plusieurs manières différentes (expressions ambiguës).

Le mot « même », par exemple, a des significations différentes dans les phrases : « Charles et moi avons la même voiture » et « Charles et moi avons la même mère ». Dans la première, ce que je veux dire est que la voiture de Charles et la mienne sont du même modèle. Il s'agit pourtant de deux voitures distinctes. Dans la seconde, j'indique que la mère de Charles et la mienne constituent en fait une seule et unique personne.

Lorsque nous traitons d'objets logiciels, nous pouvons rencontrer la même ambiguïté. Par exemple, si nous parlons de l'égalité de deux objets Point(), cela signifie-t-il que ces deux objets contiennent les mêmes données (leurs attributs), ou bien cela signifie-t-il que nous parlons de deux références à un même et unique objet ? Considérez par exemple les instructions suivantes :

>>> p1 = Point()
>>> p1.x = 3
>>> p1.y = 4
>>> p2 = Point()
>>> p2.x = 3
>>> p2.y = 4
>>> print (p1 == p2)
0

Ces instructions créent deux objets p1 et p2 qui restent distincts, même s'ils ont des contenus similaires. La dernière instruction teste l'égalité de ces deux objets (double signe égale), et le résultat est zéro (ce qui signifie que l'expression entre parenthèses est fausse : il n'y a donc pas égalité).

On peut confirmer cela d'une autre manière encore :

>>> print p1
<__main__.Point instance at 00C2CBEC>
>>> print p2
<__main__.Point instance at 00C50F9C>

L'information est claire : les deux variables p1 et p2 référencent bien des objets différents.


Essayons autre chose, à présent :

>>> p2 = p1
>>> print (p1 == p2)
1

Par l'instruction p2 = p1, nous assignons le contenu de p1 à p2. Cela signifie que désormais ces deux variables référencent le même objet. Les variables p1 et p2 sont des alias l'une de l'autre.

Le test d'égalité dans l'instruction suivante renvoie cette fois la valeur 1, ce qui signifie que l'expression entre parenthèses est vraie : p1 et p2 désignent bien toutes deux un seul et unique objet, comme on peut s'en convaincre en essayant encore :

>>> p1.x = 7
>>> print p2.x
7
>>> print p1
<__main__.Point instance at 00C2CBEC>
>>> print p2
<__main__.Point instance at 00C2CBEC>

Objets composés d'objets

modifier

Supposons maintenant que nous voulions définir une classe pour représenter des rectangles. Pour simplifier, nous allons considérer que ces rectangles seront toujours orientés horizontalement ou verticalement, et jamais en oblique.

De quelles informations avons-nous besoin pour définir de tels rectangles ? Il existe plusieurs possibilités. Nous pourrions par exemple spécifier la position du centre du rectangle (deux coordonnées) et préciser sa taille (largeur et hauteur). Nous pourrions aussi spécifier les positions du coin supérieur gauche et du coin inférieur droit. Ou encore la position du coin supérieur gauche et la taille. Admettons ce soit cette dernière méthode qui soit retenue.

Définissons donc notre nouvelle classe :

>>> class Rectangle:
        "définition d'une classe de rectangles"

... et servons nous-en tout de suite pour créer une instance :

>>> boite = Rectangle()
>>> boite.largeur = 50.0
>>> boite.hauteur = 35.0

Nous créons ainsi un nouvel objet Rectangle() et deux attributs. Pour spécifier le coin supérieur gauche, nous allons utiliser une instance de la classe Point() que nous avons définie précédemment. Ainsi nous allons créer un objet à l'intérieur d'un autre objet !

>>> boite.coin = Point()
>>> boite.coin.x = 12.0
>>> boite.coin.y = 27.0

Pour accéder à un objet qui se trouve à l'intérieur d'un autre objet, on utilise la qualification des noms hiérarchisée (à l'aide de points) que nous avons déjà rencontrée à plusieurs reprises. Ainsi l'expression boite.coin.y signifie « Aller à l'objet référencé dans la variable boite. Dans cet objet, repérer l'attribut coin, puis aller à l'objet référencé dans cet attribut. Une fois cet autre objet trouvé, sélectionner son attribut y. »

Vous pourrez peut-être mieux vous représenter à l'avenir les objets composites, à l'aide de diagrammes similaires à celui que nous reproduisons ci-dessous :

 
schéma de variables dans un contexte objet

Le nom « boîte » se trouve dans l'espace de noms principal. Il référence un autre espace de noms réservé à l'objet correspondant, dans lequel sont mémorisés les noms « largeur », « hauteur » et « coin ». Ceux-ci référencent à leur tour, soit d'autres espaces de noms (cas du nom « coin »), soit des valeurs bien déterminées. Python réserve des espaces de noms différents pour chaque module, chaque classe, chaque instance, chaque fonction. Vous pouvez tirer parti de tous ces espaces bien compartimentés afin de réaliser des programmes robustes, c'est-à-dire des programmes dont les différents composants ne peuvent pas facilement interférer.


Objets comme valeurs de retour d'une fonction

modifier

Nous avons vu plus haut que les fonctions peuvent utiliser des objets comme paramètres. Elles peuvent également transmettre une instance comme valeur de retour. Par exemple, la fonction trouveCentre() ci-dessous doit être appelée avec un argument de type Rectangle() et elle renvoie un objet Point(), lequel contiendra les coordonnées du centre du rectangle.

>>> def trouveCentre(box):
        p = Point()
        p.x = box.coin.x + box.largeur/2.0
        p.y = box.coin.y + box.hauteur/2.0
        return p

Pour appeler cette fonction, vous pouvez utiliser l'objet boite comme argument :

>>> centre = trouveCentre(boite)
>>> print centre.x, centre.y
37.0  44.5

Les objets sont modifiables

modifier

Nous pouvons changer les propriétés d'un objet en assignant de nouvelles valeurs à ses attributs. Par exemple, nous pouvons modifier la taille d'un rectangle (sans modifier sa position), en réassignant ses attributs hauteur et largeur :

>>> boite.hauteur = boite.hauteur + 20
>>> boite.largeur = boite.largeur – 5

Nous pouvons faire cela sous Python, parce que dans ce langage les propriétés des objets sont toujours publiques (du moins dans la version actuelle 2.0). D'autres langages établissent une distinction nette entre attributs publics (accessibles de l'extérieur de l'objet) et attributs privés (qui sont accessibles seulement aux algorithmes inclus dans l'objet lui-même).

Comme nous l'avons déjà signalé plus haut (à propos de la définition des attributs par assignation simple, depuis l'extérieur de l'objet), modifier de cette façon les attributs d'une instance n'est pas une pratique recommandable, parce qu'elle contredit l'un des objectifs fondamentaux de la programmation orientée objet, qui vise à établir une séparation stricte entre la fonctionnalité d'un objet (telle qu'elle a été déclarée au monde extérieur) et la manière dont cette fonctionnalité est réellement implémentée dans l'objet (et que le monde extérieur n'a pas à connaître).

Plus concrètement, nous devrons veiller désormais à ce que les objets que nous créons ne soient modifiables en principe que par l'intermédiaire de méthodes mises en place spécifiquement dans ce but, comme nous allons l'expliquer dans le chapitre suivant.


Définition d'une méthode

modifier

Pour illustrer notre propos, nous allons définir une nouvelle classe Time, qui nous permettra d'effectuer toute une série d'opérations sur des instants, des durées, etc. :

>>> class Time:
        "Définition d'une classe temporelle"

Créons à présent un objet de ce type, et ajoutons-lui des variables d'instance pour mémoriser les heures, minutes et secondes :

>>> instant = Time()
>>> instant.heure = 11
>>> instant.minute = 34
>>> instant.seconde = 25

À titre d'exercice, écrivez maintenant vous-même une fonction affiche_heure(), qui serve à visualiser le contenu d'un objet de classe Time() sous la forme conventionnelle « heure:minute:seconde ».

Appliquée à l'objet instant créé ci-dessus, cette fonction devrait donc afficher 11:34:25 :

>>> print affiche_heure(instant)
11:34:25

Votre fonction ressemblera probablement à ceci :

>>> def affiche_heure(t):
        print str(t.heure) + ":" + str(t.minute) + ":" + str(t.seconde)

(Notez au passage l'utilisation de la fonction str() pour convertir les données numériques en chaînes de caractères). Si par la suite vous utilisez fréquemment des objets de la classe Time(), il y a gros à parier que cette fonction d'affichage vous sera fréquemment utile.

Il serait donc probablement fort judicieux d'encapsuler cette fonction affiche_heure() dans la classe Time() elle-même, de manière à s'assurer qu'elle soit toujours automatiquement disponible chaque fois que l'on doit manipuler des objets de la classe Time().

Une fonction qui est ainsi encapsulée dans une classe s'appelle une méthode.


Définition concrète d'une méthode

On définit une méthode comme on définit une fonction, avec cependant deux différences :

  • La définition d'une méthode est toujours placée à l'intérieur de la définition d'une classe, de manière à ce que la relation qui lie la méthode à la classe soit clairement établie.
  • Le premier paramètre utilisé par une méthode doit toujours être une référence d'instance. Vous pourriez en principe utiliser un nom de variable quelconque pour ce paramètre, mais il est vivement conseillé de respecter la convention qui consiste à toujours lui donner le nom self. Le paramètre self désigne donc l'instance à laquelle la méthode sera associée, dans les instructions faisant partie de la définition. (De ce fait, la définition d'une méthode comporte toujours au moins un paramètre, alors que la définition d'une fonction peut n'en comporter aucun).

Voyons comment cela se passe en pratique :

Pour ré-écrire la fonction affiche_heure() comme une méthode de la classe Time(), il nous suffit de déplacer sa définition à l'intérieur de celle de la classe, et de changer le nom de son paramètre :

>>> class Time:
        "Nouvelle classe temporelle"
        def affiche_heure(self):
            print str(self.heure) + ":" + str(self.minute) \
                  + ":" + str(self.seconde)

La définition de la méthode fait maintenant partie du bloc d'instructions indentées après l'instruction class. Notez bien l'utilisation du mot réservé self, qui se réfère donc à toute instance susceptible d'être créée à partir de cette classe.


Essai de la méthode dans une instance

Nous pouvons dès à présent instancier un objet de notre nouvelle classe Time() :

>>> maintenant = Time()

Si nous essayons d'utiliser un peu trop vite notre nouvelle méthode, ça ne marche pas :

>>> maintenant.affiche_heure()
AttributeError: 'Time' instance has no attribute 'heure'

C'est normal : nous n'avons pas encore créé les attributs d'instance. Il faudrait faire par exemple :

>>> maintenant.heure = 13
>>> maintenant.minute = 34
>>> maintenant.seconde = 21
>>> maintenant.affiche_heure()
13:34:21

Nous avons cependant déjà signalé à plusieurs reprises qu'il n'est pas recommandable de créer ainsi les attributs d'instance en dehors de l'objet lui-même, ce qui conduit (entre autres désagréments) à des erreurs comme celle que nous venons de rencontrer, par exemple.

Voyons donc à présent comment nous pouvons mieux faire.

Méthodes prédéfinies

modifier

Certaines méthodes de classe Python existent automatiquement dans toutes les classes sans être déclarées, et certaines se lancent automatiquement lors de certains événements. Ces méthodes spéciales sont nommées entre deux underscores (__)[1].


__doc__

modifier

Comme pour les fonctions, la chaîne de documentation est définie dans cette méthode.

>>> print p9.__doc__
Définition d'un point mathématique

Dans le cas d'un module, la doc correspond au premier bloc de commentaire :

"""Définition d'un point mathématique"""

__contains__

modifier

Cette méthode permet de lancer des recherches dans une classes comme dans un objet composite comme la liste, avec "in". En effet, il suffit d'y placer les getters sur les attributs :

class MyClass:
    attribute1 = 'ok'

    def __contains__(self, attribute):
        if self.attribute1: return True

MaClasse1 = MyClass()
print attribute1 in MaClasse1 # True
print attribute2 in MaClasse1 # False

__del__

modifier

Destructeur : se lance quand l'objet est détruit.


__enter__ et __exit__

modifier

Respectivement constructeur et destructeur des classes instanciées avec with, exécutés respectivement après et avant __init__ et __del__. Exemple :

class Test:        
    def __enter__(self):
        print 'enter'
     
    def __exit__(self, exc_type, exc_value, traceback):
        print 'exit'

with Test():
    pass
enter
exit


__ init __

modifier

Cette méthode se lance lors du premier accès à la classe.

Exemple :

class Complexe:
	def __init__(self, r, i):
		self.reel = r
		self.imaginaire = i

Complexe1 = Complexe(1, 2)
print Complexe1.reel    # Affiche 1


L'erreur que nous avons rencontrée au paragraphe précédent est-elle évitable ? Elle ne se produirait effectivement pas, si nous nous étions arrangés pour que la méthode affiche_heure() puisse toujours afficher quelque chose, sans qu'il ne soit nécessaire d'effectuer au préalable aucune manipulation sur l'objet nouvellement créé. En d'autres termes, il serait judicieux que les variables d'instance soient prédéfinies elles aussi à l'intérieur de la classe, avec pour chacune d'elles une valeur « par défaut ».

Pour obtenir cela, nous allons faire appel à une méthode particulière, que l'on appelle un constructeur. Une méthode constructeur est une méthode qui est exécutée automatiquement lorsque l'on instancie un nouvel objet à partir de la classe. On peut y placer tout ce qui semble nécessaire pour initialiser automatiquement l'objet que l'on crée.

Exemple :

>>> class Time:
        "Encore une nouvelle classe temporelle"
        def __init__(self):
            self.heure =0
            self.minute =0
            self.seconde =0

        def affiche_heure(self):
            print str(self.heure) + ":" + str(self.minute) \
                 + ":" + str(self.seconde)

>>> tstart = Time()
>>> tstart.affiche_heure()
0:0:0

L'intérêt de cette technique apparaîtra plus clairement si nous ajoutons encore quelque chose. Comme toute méthode qui se respecte, la méthode __init__() peut être dotée de paramètres. Ceux-ci vont jouer un rôle important, parce qu'ils vont permettre d'instancier un objet et d'initialiser certaines de ses variables d'instance, en une seule opération. Dans l'exemple ci-dessus, veuillez donc modifier la définition de la méthode __init__() comme suit :

        def __init__(self, hh =0, mm =0, ss =0):
            self.heure = hh
            self.minute = mm
            self.seconde = ss

La méthode __init__() comporte à présent 3 paramètres, avec pour chacun une valeur par défaut. Pour lui transmettre les arguments correspondants, il suffit de placer ceux-ci dans les parenthèses qui accompagnent le nom de la classe, lorsque l'on écrit l'instruction d'instanciation du nouvel objet.

Voici par exemple la création et l'initialisation simultanées d'un nouvel objet Time() :

>>> recreation = Time(10, 15, 18)
>>> recreation.affiche_heure()
10:15:18

Puisque les variables d'instance possèdent maintenant des valeurs par défaut, nous pouvons aussi bien créer de tels objets Time() en omettant un ou plusieurs arguments :

>>> rentree = Time(10, 30)
>>> rentree.affiche_heure()
10:30:0

__main__

modifier
>>> print p9
<__main__.Point instance at 0x403e1a8c>

Le message renvoyé par Python indique, comme vous l'aurez certainement bien compris tout de suite, que "p9" est une instance de la classe "Point()", qui est définie elle-même au niveau principal du programme. Elle est située dans un emplacement bien déterminé de la mémoire vive, dont l'adresse apparaît ici en notation hexadécimale.


__new__

modifier

Constructeur de métaclasse.

La méthode __new__ est statique. Elle est appelée pour créer l'objet instancié avec l'opérateur new, juste avant l'appel à la méthode __init__ initialisant les membres de l'objet créé.

__new__(class, *args, **kwargs)

Les arguments de la méthode __new__ sont décris ci-après :

class
Le premier argument est la classe de l'objet à créer.
args
Liste itérable des arguments non nommés à transmettre au constructeur.
kwargs
Dictionnaire des arguments nommés à transmettre au constructeur.

La méthode retourne l'objet créé. Comme cette méthode est héritée de la classe object, une classe peut la surcharger en appelant celle de la classe parente (super().__new__()) avec des arguments modifiés, ou pour exécuter des actions avant ou après la création d'un objet.

Techniquement, la méthode object.__new__() peut être appelée pour créer un objet, mais il faut ensuite appeler manuellement la méthode __init__() sur l'objet créé. Cet enchaînement des deux appels n'est fait que par l'opérateur new.

Exemple : Une classe héritant de celle des entiers pour créer des cubes d'entier.

class CubeEntier(int):
    def __new__(cls, value):
        return super().__new__(cls, value ** 3)

x = CubeEntier(3)
print(x)  # 27

__repr__

modifier

Cette méthode renvoie une représentation de l'objet quand on l'appelle directement (sans chercher à accéder à ses attributs ou méthodes). Exemple :

class Bar:
    def __init__ (self, iamthis):
        self.iamthis = iamthis

    def __repr__(self):
        return "Bar('%s')" % self.iamthis

bar = Bar('apple')
print bar
Bar('apple')

Dans cette méthode, cet objet renverrait :

<__main__.Bar instance at 0x7f282bbf2a28>


__str__

modifier

Renvoie une chaine de caractères quand on traite l'objet comme tel. Exemple :

class Bar:
    def __init__ (self, iam_this):
        self.iam_this = iam_this

    def __str__ (self):
        return self.iam_this

bar = Bar('apple')
print bar
apple


__unicode__

modifier

Réservé à Python 2.x.


Opérateurs binaires

modifier
Fonction Opérateur
__add__ A + B
__sub__ A - B
__mul__ A * B
__truediv__ A / B
__floordiv__ A // B
__mod__ A % B
__pow__ A ** B
__and__ A & B
__or__ A | B
__xor__ A ^ B
__eq__ A == B
__ne__ A != B
__gt__ A > B
__lt__ A < B
__ge__ A >= B
__le__ A <= B
__lshift__ A << B
__rshift__ A >> B
__contains__ A in B
A not in B


Opérateurs unaires

modifier
Fonction Opérateur
__pos__ +A
__neg__ -A
__inv__ ~A
__abs__ abs(A)
__len__ len(A)


Gestion des attributs

modifier

Getters et setters.

Fonction Forme indirecte Forme directe
__getattr__ getattr(A, B) A.B
__setattr__ setattr(A, B, C) A.B = C
__delattr__ delattr(A, B) del A.B

Gestion des indices

modifier

Se déclenchent lorsque l'on manipule un objet comme un dictionnaire[2].

Fonction Opérateur
__getitem__ C[i]
__setitem__ C[i] = v
__delitem__ del C[i]
__getslice__ C[s:e]
__setslice__ C[s:e] = v
__delslice__ del C[s:e]


Fonction Opérateur
__cmp__ cmp(x, y)
__hash__ hash(x)
__nonzero__ bool(x)
__call__ f(x)
__iter__ iter(x)
__reversed__ reversed(x) (2.6+)
__divmod__ divmod(x, y)
__int__ int(x)
__long__ long(x)
__float__ float(x)
__complex__ complex(x)
__hex__ hex(x)
__oct__ oct(x)
__index__
__copy__ copy.copy(x)
__deepcopy__ copy.deepcopy(x)
__sizeof__ sys.getsizeof(x) (2.6+)
__trunc__ math.trunc(x) (2.6+)
__format__ format(x, ...) (2.6+)


Espaces de noms des classes et instances

modifier

Les variables définies à l'intérieur d'une fonction sont des variables locales, inaccessibles aux instructions qui se trouvent à l'extérieur de la fonction. Cela permet d'utiliser les mêmes noms de variables dans différentes parties d'un programme, sans risque d'interférence.

Pour décrire la même chose en d'autres termes, nous pouvons dire que chaque fonction possède son propre espace de noms, indépendant de l'espace de noms principal.

Les instructions se trouvant à l'intérieur d'une fonction peuvent accéder aux variables définies au niveau principal, mais en lecture seulement : elles peuvent utiliser les valeurs de ces variables, mais pas les modifier (à moins de faire appel à l'instruction global).

Il existe donc une sorte de hiérarchie entre les espaces de noms. Nous allons constater la même chose à propos des classes et des objets. En effet :

  • Chaque classe possède son propre espace de noms. Les variables qui en font partie sont appelées les attributs de la classe.
  • Chaque objet instance (créé à partir d'une classe) obtient son propre espace de noms. Les variables qui en font partie sont appelées variables d'instance ou attributs d'instance.
  • Les classes peuvent utiliser (mais pas modifier) les variables définies au niveau principal.
  • Les instances peuvent utiliser (mais pas modifier) les variables définies au niveau de la classe et les variables définies au niveau principal.

Considérons par exemple la classe Time() définie précédemment. À la page précédente, nous avons instancié deux objets de cette classe : recreation et rentree. Chacun a été initialisé avec des valeurs différentes, indépendantes. Nous pouvons modifier et réafficher ces valeurs à volonté dans chacun de ces deux objets, sans que l'autre n'en soit affecté :

>>> recreation.heure = 12
>>> rentree.affiche_heure()
10:30:0
>>> recreation.affiche_heure()
12:15:18

Veuillez à présent encoder et tester l'exemple ci-dessous :

>>> class Espaces:                          # 1
        aa = 33                             # 2
        def affiche(self):                  # 3
            print aa, Espaces.aa, self.aa   # 4

>>> aa = 12                                 # 5
>>> essai = Espaces()                       # 6
>>> essai.aa = 67                           # 7
>>> essai.affiche()                         # 8
12 33 67
>>> print aa, Espaces.aa, essai.aa          # 9
12 33 67

Dans cet exemple, le même nom aa est utilisé pour définir trois variables différentes : une dans l'espace de noms de la classe (à la ligne 2), une autre dans l'espace de noms principal (à la ligne 5), et enfin une dernière dans l'espace de nom de l'instance (à la ligne 7).

La ligne 4 et la ligne 9 montrent comment vous pouvez accéder à ces trois espaces de noms (de l'intérieur d'une classe, ou au niveau principal), en utilisant la qualification par points. Notez encore une fois l'utilisation de self pour désigner l'instance.

Héritage

modifier

Les classes constituent le principal outil de la programmation orientée objet ou POO (Object Oriented Programming ou OOP en anglais), qui est considérée de nos jours comme la technique de programmation la plus performante. L'un des principaux atouts de ce type de programmation réside dans le fait que l'on peut toujours se servir d'une classe préexistante pour en créer une nouvelle qui possédera quelques fonctionnalités différentes ou supplémentaires. Le procédé s'appelle dérivation. Il permet de créer toute une hiérarchie de classes allant du général au particulier.

Nous pouvons par exemple définir une classe Mammifere(), qui contiendra un ensemble de caractéristiques propres à ce type d'animal. À partir de cette classe, nous pourrons alors dériver une classe Primate(), une classe Rongeur(), une classe Carnivore(), etc., qui hériteront toutes des caractéristiques de la classe Mammifere(), en y ajoutant leurs spécificités.

Au départ de la classe Carnivore(), nous pourrons ensuite dériver une classe Belette(), une classe Loup(), une classe Chien(), etc., qui hériteront encore une fois toutes les caractéristiques de la classe Mammifere avant d'y ajouter les leurs. Exemple :

>>> class Mammifere:
        caract1 = "il allaite ses petits ;"

>>> class Carnivore(Mammifere):
        caract2 = "il se nourrit de la chair de ses proies ;"

>>> class Chien(Carnivore):
        caract3 = "son cri s'appelle aboiement ;"

>>> mirza = Chien()
>>> print mirza.caract1, mirza.caract2, mirza.caract3
il allaite ses petits ; il se nourrit de la chair de ses proies ;
son cri s'appelle aboiement ;

Dans cet exemple, nous voyons que l'objet mirza, qui est une instance de la classe Chien(), hérite non seulement l'attribut défini pour cette classe, mais également des attributs définis pour les classes parentes.

Vous voyez également dans cet exemple comment il faut procéder pour dériver une classe à partir d'une classe parente : On utilise l'instruction class, suivie comme d'habitude du nom que l'on veut attribuer à la nouvelle classe, et on place entre parenthèses le nom de la classe parente.

Notez bien que les attributs utilisés dans cet exemple sont des attributs des classes (et non des attributs d'instances). L'instance mirza peut accéder à ces attributs, mais pas les modifier :

>>> mirza.caract2 = "son corps est couvert de poils"
>>> print mirza.caract2
son corps est couvert de poils
>>> fido = Chien()
>>> print fido.caract2
il se nourrit de la chair de ses proies ;

Dans ce nouvel exemple, la ligne 1 ne modifie pas l'attribut caract2 de la classe Carnivore(), contrairement à ce que l'on pourrait penser au vu de la ligne 3. Nous pouvons le vérifier en créant une nouvelle instance fido (lignes 4 à 6).

Si vous avez bien assimilé les paragraphes précédents, vous aurez compris que l'instruction de la ligne 1 crée une nouvelle variable d'instance associée seulement à l'objet mirza. Il existe donc dès ce moment deux variables avec le même nom caract2 : l'une dans l'espace de noms de l'objet mirza, et l'autre dans l'espace de noms de la classe Carnivore().

Comment faut-il alors interpréter ce qui s'est passé aux lignes 2 et 3 ? Comme nous l'avons vu plus haut, l'instance mirza peut accéder aux variables situées dans son propre espace de noms, mais aussi à celles qui sont situées dans les espaces de noms de toutes les classes parentes. S'il existe des variables aux noms identiques dans plusieurs de ces espaces, laquelle sera-t-elle sélectionnée lors de l'exécution d'une instruction comme celle de la ligne 2 ?

Pour résoudre ce conflit, Python respecte une règle de priorité fort simple. Lorsqu'on lui demande d'utiliser la valeur d'une variable nommée alpha, par exemple, il commence par rechercher ce nom dans l'espace local (le plus « interne », en quelque sorte). Si une variable alpha est trouvée dans l'espace local, c'est celle-là qui est utilisée, et la recherche s'arrête. Sinon, Python examine l'espace de noms de la structure parente, puis celui de la structure grand-parente, et ainsi de suite jusqu'au niveau principal du programme.

À la ligne 2 de notre exemple, c'est donc la variable d'instance qui sera utilisée. À la ligne 5, par contre, c'est seulement au niveau de la classe grand-parente qu'une variable répondant au nom caract2 peut être trouvée. C'est donc celle-là qui est affichée.

Héritage et polymorphisme

modifier

Pour bien comprendre ce script, il faut cependant d'abord vous rappeler quelques notions élémentaires de chimie. Dans votre cours de chimie, vous avez certainement dû apprendre que les atomes sont des entités constituées d'un certain nombre de protons (particules chargées d'électricité positive), d'électrons (chargés négativement) et de neutrons (neutres).

Le type d'atome (ou élément) est déterminé par le nombre de protons, que l'on appelle également numéro atomique. Dans son état fondamental, un atome contient autant d'électrons que de protons, et par conséquent il est électriquement neutre. Il possède également un nombre variable de neutrons, mais ceux-ci n'influencent en aucune manière la charge électrique globale.

Dans certaines circonstances, un atome peut gagner ou perdre des électrons. Il acquiert de ce fait une charge électrique globale, et devient alors un ion (il s'agit d'un ion négatif si l'atome a gagné un ou plusieurs électrons, et d'un ion positif s'il en a perdu). La charge électrique d'un ion est égale à la différence entre le nombre de protons et le nombre d'électrons qu'il contient.

Le script suivant génère des objets atome et des objets ion. Nous avons rappelé ci-dessus qu'un ion est simplement un atome modifié. Dans notre programmation, la classe qui définit les objets « ion » sera donc une classe dérivée de la classe atome : elle héritera d'elle tous ses attributs et toutes ses méthodes, en y ajoutant les siennes propres. On pourra dire également que la méthode affiche() a été surchargée.

L'une de ces méthodes ajoutées (la méthode affiche()) remplace une méthode de même nom héritée de la classe atome. Les classes « atome » et « ion » possèdent donc chacune une méthode de même nom, mais qui effectuent un travail différent. On parle dans ce cas de polymorphisme. On pourra dire également que la méthode affiche() a été surchargée.

Il sera évidemment possible d'instancier un nombre quelconque d'atomes et d'ions à partir de ces deux classes. Or l'une d'entre elles (la classe atome) doit contenir une version simplifiée du tableau périodique des éléments (tableau de Mendeleïev), de façon à pouvoir attribuer un nom d'élément chimique, ainsi qu'un nombre de neutrons, à chaque objet généré. Comme il n'est pas souhaitable de recopier tout ce tableau dans chacune des instances, nous le placerons dans un attribut de classe. Ainsi ce tableau n'existera qu'en un seul endroit en mémoire, tout en restant accessible à tous les objets qui seront produits à partir de cette classe.

Voyons concrètement comment toutes ces idées s'articulent :

class Atome:
    """atomes simplifiés, choisis parmi les 10 premiers éléments du TP""" 
    table = [None, ('hydrogène',0), ('hélium',2), ('lithium',4),
            ('béryllium',5), ('bore',6), ('carbone',6), ('azote',7),
            ('oxygène',8), ('fluor',10), ('néon',10)]
            
    def __init__(self, nat):
        "le n° atomique détermine le n. de protons, d'électrons et de neutrons" 
        self.np, self.ne = nat, nat       # nat = numéro atomique
        self.nn = Atome.table[nat][1]     # nb. de neutrons trouvés dans table
        
    def affiche(self):
        print
        print "Nom de l'élément :", Atome.table[self.np][0]
        print "%s protons, %s électrons, %s neutrons" % \
                  (self.np, self.ne, self.nn)
               
class Ion(Atome):
    """les ions sont des atomes qui ont gagné ou perdu des électrons"""
     
    def __init__(self, nat, charge):
        "le n° atomique et la charge électrique déterminent l'ion"
        Atome.__init__(self, nat)
        self.ne = self.ne - charge
        self.charge = charge
    
    def affiche(self):
        "cette méthode remplace celle héritée de la classe parente" 
        Atome.affiche(self)			# ... tout en l'utilisant elle-même ! 
        print "Particule électrisée. Charge =", self.charge        
        
### Programme principal : ###     

a1 = Atome(5)
a2 = Ion(3, 1)
a3 = Ion(8, -2)
a1.affiche()
a2.affiche()
a3.affiche()

L'exécution de ce script provoque l'affichage suivant :

Nom de l'élément : bore
5 protons, 5 électrons, 6 neutrons

Nom de l'élément : lithium
3 protons, 2 électrons, 4 neutrons
Particule électrisée. Charge = 1

Nom de l'élément : oxygène
8 protons, 10 électrons, 8 neutrons
Particule électrisée. Charge = -2

Au niveau du programme principal, vous pouvez constater que l'on instancie les objets Atome() en fournissant leur numéro atomique (lequel doit être compris entre 1 et 10). Pour instancier des objets Ion(), par contre, on doit fournir un numéro atomique et une charge électrique globale (positive ou négative). La même méthode affiche() fait apparaître les propriétés de ces objets, qu'il s'agisse d'atomes ou d'ions, avec dans le cas de l'ion une ligne supplémentaire (polymorphisme).

Commentaires

La définition de la classe Atome() commence par l'assignation de la variable table. Une variable définie à cet endroit fait partie de l'espace de noms de la classe. C'est donc un attribut de classe, dans lequel nous plaçons une liste d'informations concernant les 10 premiers éléments du tableau périodique de Mendeleïev. Pour chacun de ces éléments, la liste contient un tuple : (nom de l'élément, nombre de neutrons), à l'indice qui correspond au numéro atomique. Comme il n'existe pas d'élément de numéro atomique zéro, nous avons placé à l'indice zéro dans la liste, l'objet spécial None (a priori, nous aurions pu placer à cet endroit n'importe quelle autre valeur, puisque cet indice ne sera pas utilisé. L'objet None de Python nous semble cependant particulièrement explicite).

Viennent ensuite les définitions de deux méthodes :

  • Le constructeur __init__() sert essentiellement ici à générer trois attributs d'instance, destinés à mémoriser respectivement les nombres de protons, d'électrons et de neutrons pour chaque objet atome construit à partir de cette classe (Les attributs d'instance sont des variables liées à self).

Notez bien la technique utilisée pour obtenir le nombre de neutrons à partir de l'attribut de classe, en mentionnant le nom de la classe elle-même dans une qualification par points.

  • La méthode affiche() utilise à la fois les attributs d'instance, pour retrouver les nombres de protons, d'électrons et de neutrons de l'objet courant, et l'attribut de classe (lequel est commun à tous les objets) pour en extraire le nom d'élément correspondant. Veuillez aussi remarquer au passage l'utilisation de la technique de formatage des chaînes.

La définition de la classe Ion() comporte des parenthèses. Il s'agit donc d'une classe dérivée, sa classe parente étant bien entendu la classe Atome() qui précède.

Les méthodes de cette classe sont des variantes de celles de la classe Atome(). Elles devront donc vraisemblablement faire appel à celles-ci. Cette remarque est importante :

 Comment peut-on, à l'intérieur de la définition d'une classe, faire appel à une méthode définie dans une autre classe ?

Il ne faut pas perdre de vue, en effet, qu'une méthode se rattache toujours à l'instance qui sera générée à partir de la classe (instance représentée par self dans la définition). Si une méthode doit faire appel à une autre méthode définie dans une autre classe, il faut pouvoir lui transmettre la référence de l'instance à laquelle elle doit s'associer. Comment faire ? C'est très simple :

 Lorsque dans la définition d'une classe, on souhaite faire appel à une méthode définie dans une autre classe, on doit lui transmettre la référence de l'instance comme premier argument.

C'est ainsi que dans notre script, par exemple, la méthode affiche() de la classe Ion() peut faire appel à la méthode affiche() de la classe Atome() : les informations affichées seront bien celles de l'objet-ion courant, puisque sa référence a été transmise dans l'instruction d'appel :

Atome.affiche(self)

(dans cette instruction, self est bien entendu la référence de l'instance courante).

De la même manière, la méthode constructeur de la classe Ion() fait appel à la méthode constructeur de sa classe parente, dans :

Atome.__init__(self, nat)
 
Schéma résumant le fonctionnement d'une classe


Exercices

modifier

Exercices

  1. Définissez une classe Domino() qui permette d'instancier des objets simulant les pièces d'un jeu de dominos. Le constructeur de cette classe initialisera les valeurs des points présents sur les deux faces A et B du domino (valeurs par défaut = 0). Deux autres méthodes seront définies :
    • une méthode affiche_points() qui affiche les points présents sur les deux faces ;
    • une méthode valeur() qui renvoie la somme des points présents sur les deux faces.
    Exemples d'utilisation de cette classe :
    >>> d1 = Domino(2,6)
    >>> d2 = Domino(4,3)
    >>> d1.affiche_points()
    face A : 2  face B : 6
    >>> d2.affiche_points()
    face A : 4  face B : 3
    >>> print "total des points :", d1.valeur() + d2.valeur()
    total des points : 15
    >>> liste_dominos = []
    >>> for i in range(7):
        liste_dominos.append(Domino(6, i))
    
    >>> liste_dominos[1].affiche_points()
    face A: 6  face B: 1
    
    etc., etc.
    
  2. Définissez une classe CompteBancaire(), qui permette d'instancier des objets tels que compte1, compte2, etc. Le constructeur de cette classe initialisera deux attributs d'instance "nom" et "solde", avec les valeurs par défaut 'Dupont' et 1000. Trois autres méthodes seront définies :
    • depot(somme) permettra d'ajouter une certaine somme au solde
    • retrait(somme) permettra de retirer une certaine somme du solde
    • affiche() permettra d'afficher le nom du titulaire et le solde de son compte.
    Exemples d'utilisation de cette classe :
    >>> compte1 = CompteBancaire('Duchmol', 800)
    >>> compte1.depot(350)
    >>> compte1.retrait(200)
    >>> compte1.affiche()
    Le solde du compte bancaire de Duchmol est de 950 euros.
    >>> compte2 = CompteBancaire()
    >>> compte2.depot(25)
    >>> compte2.affiche()
    Le solde du compte bancaire de Dupont est de 1025 euros.
    
  3. Définissez une classe Voiture() qui permette d'instancier des objets reproduisant le comportement de voitures automobiles. Le constructeur de cette classe initialisera les attributs d'instance suivants, avec les valeurs par défaut indiquées : marque = 'Ford', couleur = 'rouge', pilote = 'personne', vitesse = 0. Lorsque l'on instanciera un nouvel objet Voiture(), on pourra choisir sa marque et sa couleur, mais pas sa vitesse, ni le nom de son conducteur. Les méthodes suivantes seront définies : - choix_conducteur(nom) permettra de désigner (ou changer) le nom du conducteur - accelerer(taux, duree) permettra de faire varier la vitesse de la voiture. La variation de vitesse obtenue sera égale au produit : taux x duree. Par exemple, si la voiture accélère au taux de 1,3 m/s2 pendant 20 secondes, son gain de vitesse doit être égal à 26 m/s. Des taux négatifs seront acceptés (ce qui permettra de décélérer). La variation de vitesse ne sera pas autorisée si le conducteur est 'personne'. - affiche_tout() permettra de faire apparaître les propriétés présentes de la voiture, c'est-à-dire sa marque, sa couleur, le nom de son conducteur, sa vitesse. Exemples d'utilisation de cette classe :
    >>> a1 = Voiture('Peugeot', 'bleue')
    >>> a2 = Voiture(couleur = 'verte')
    >>> a3 = Voiture('Mercedes')
    >>> a1.choix_conducteur('Roméo')
    >>> a2.choix_conducteur('Juliette')
    >>> a2.accelerer(1.8, 12)
    >>> a3.accelerer(1.9, 11)
    Cette voiture n'a pas de conducteur !
    >>> a2.affiche_tout()
    Ford verte pilotée par Juliette, vitesse = 21.6 m/s.
    >>> a3.affiche_tout()
    Mercedes rouge pilotée par personne, vitesse = 0 m/s.
    
  4. Définissez une classe Satellite() qui permette d'instancier des objets simulant des satellites artificiels lancés dans l'espace, autour de la terre. Le constructeur de cette classe initialisera les attributs d'instance suivants, avec les valeurs par défaut indiquées : masse = 100, vitesse = 0. Lorsque l'on instanciera un nouvel objet Satellite(), on pourra choisir son nom, sa masse et sa vitesse. Les méthodes suivantes seront définies : - impulsion(force, duree) permettra de faire varier la vitesse du satellite. Pour savoir comment, rappelez-vous votre cours de physique : la variation de vitesse   subie par un objet de masse m soumis à l'action d'une force F pendant un temps t vaut  . Par exemple : un satellite de 300 kg qui subit une force de 600 Newtons pendant 10 secondes voit sa vitesse augmenter (ou diminuer) de 20 m/s. - affiche_vitesse() affichera le nom du satellite et sa vitesse courante. - energie() renverra au programme appelant la valeur de l'énergie cinétique du satellite. Rappel : l'énergie cinétique   se calcule à l'aide de la formule   Exemples d'utilisation de cette classe :
    >>> s1 = Satellite('Zoé', masse =250, vitesse =10)
    >>> s1.impulsion(500, 15)
    >>> s1.affiche_vitesse()
    vitesse du satellite Zoé = 40 m/s.
    >>> print s1.energie()
    200000
    >>> s1.impulsion(500, 15)
    >>> s1.affiche_vitesse()
    vitesse du satellite Zoé = 70 m/s.
    >>> print s1.energie()
    612500
    

Solution

  1. class Domino:
        def __init__(self, pa, pb):
            self.pa, self.pb = pa, pb
             
        def affiche_points(self):
            print "face A :", self.pa,
            print "face B :", self.pb
            
        def valeur(self):
            return self.pa + self.pb
    
    # Programme de test :
    
    d1 = Domino(2,6)
    d2 = Domino(4,3)
    
    d1.affiche_points()
    d2.affiche_points()
    
    print "total des points :", d1.valeur() + d2.valeur() 
    
    liste_dominos = []
    for i in range(7):
        liste_dominos.append(Domino(6, i))
    
    vt =0
    for i in range(7):
        liste_dominos[i].affiche_points()
        vt = vt + liste_dominos[i].valeur()
        
    print "valeur totale des points", vt
    
  2. Réfléchissez !
  3. class Voiture:
        def __init__(self, marque = 'Ford', couleur = 'rouge'):
            self.couleur = couleur
            self.marque = marque
            self.pilote = 'personne'
            self.vitesse = 0
            
        def accelerer(self, taux, duree):
            if self.pilote =='personne':
                print "Cette voiture n'a pas de conducteur !"
            else:    
                self.vitesse = self.vitesse + taux * duree
            
        def choix_conducteur(self, nom):
            self.pilote = nom    
            
        def affiche_tout(self):
                print "%s %s pilotée par %s, vitesse = %s m/s" % \
                (self.marque, self.couleur, self.pilote, self.vitesse)     
        
    a1 = Voiture('Peugeot', 'bleue')
    a2 = Voiture(couleur = 'verte')
    a3 = Voiture('Mercedes')
    a1.choix_conducteur('Roméo')
    a2.choix_conducteur('Juliette')
    a2.accelerer(1.8, 12)
    a3.accelerer(1.9, 11)
    a2.affiche_tout()
    a3.affiche_tout()
    
  4. class Satellite:
        def __init__(self, nom, masse =100, vitesse =0):
            self.nom, self.masse, self.vitesse = nom, masse, vitesse
             
        def impulsion(self, force, duree):
            self.vitesse = self.vitesse + force * duree / self.masse
            
        def energie(self):
            return self.masse * self.vitesse**2 / 2    
                    
        def affiche_vitesse(self):
            print "Vitesse du satellite %s = %s m/s" \
                              % (self.nom, self.vitesse)
    
    # Programme de test :
    
    s1 = Satellite('Zoé', masse =250, vitesse =10)
    
    s1.impulsion(500, 15)
    s1.affiche_vitesse()
    print s1.energie()
    s1.impulsion(500, 15)
    s1.affiche_vitesse()
    print s1.energie()
    

Exercices

  1. Définissez une classe Cercle(). Les objets construits à partir de cette classe seront des cercles de tailles variées. En plus de la méthode constructeur (qui utilisera donc un paramètre rayon), vous définirez une méthode surface(), qui devra renvoyer la surface du cercle. Définissez ensuite une classe Cylindre() dérivée de la précédente. Le constructeur de cette nouvelle classe comportera les deux paramètres rayon et hauteur. Vous y ajouterez une méthode volume() qui devra renvoyer le volume du cylindre. (Rappel : Volume d'un cylindre = surface de section x hauteur). Exemple d'utilisation de cette classe :
    >>> cyl = Cylindre(5, 7)
    >>> print cyl.surface()
    78.54
    >>> print cyl.volume()
    549.78
    
  2. Complétez l'exercice précédent en lui ajoutant encore une classe Cone(), qui devra dériver cette fois de la classe Cylindre(), et dont le constructeur comportera lui aussi les deux paramètres rayon et hauteur. Cette nouvelle classe possédera sa propre méthode volume(), laquelle devra renvoyer le volume du cône. (Rappel : Volume d'un cône = volume du cylindre correspondant divisé par 3). Exemple d'utilisation de cette classe :
    >>> co = Cone(5,7)
    >>> print co.volume()
    
    183.26
  3. Définissez une classe JeuDeCartes() permettant d'instancier des objets « jeu de cartes » dont le comportement soit similaire à celui d'un vrai jeu de cartes. La classe devra comporter au moins les trois méthodes suivantes : - méthode constructeur : création et remplissage d'une liste de 52 éléments, qui sont eux-mêmes des tuples de 2 éléments contenant les caractéristiques de chacune des 52 cartes. Pour chacune d'elles, il faut en effet mémoriser séparément un nombre entier indiquant la valeur (2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, les 4 dernières valeurs étant celles des valet, dame, roi et as), et un autre nombre entier indiquant la couleur de la carte (c'est-à-dire 0,1,2,3 pour Cœur, Carreau, Trèfle & Pique). Dans une telle liste, l'élément (11,2) désigne donc le valet de Trèfle, et la liste terminée doit être du type : [(2, 0), (3,0), (3,0), (4,0), ... ... (12,3), (13,3), (14,3)] - méthode nom_carte() : cette méthode renvoie sous la forme d'une chaîne l'identité d'une carte quelconque, dont on lui a fourni le tuple descripteur en argument. Par exemple, l'instruction : print jeu.nom_carte((14, 3)) doit provoquer l'affichage de : As de pique - méthode battre() : comme chacun sait, battre les cartes consiste à les mélanger. Cette méthode sert donc à mélanger les éléments de la liste contenant les cartes, quel qu'en soit le nombre. - méthode tirer() : lorsque cette méthode est invoquée, une carte est retirée du jeu. Le tuple contenant sa valeur et sa couleur est renvoyé au programme appelant. On retire toujours la première carte de la liste. Si cette méthode est invoquée alors qu'il ne reste plus aucune carte dans la liste, il faut alors renvoyer l'objet spécial None au programme appelant. Exemple d'utilisation de la classe JeuDeCartes() :
    jeu = JeuDeCartes()            # instanciation d'un objet
    jeu.battre()                   # mélange des cartes
    for n in range(53):            # tirage des 52 cartes : 
       c = jeu.tirer() 
       if c == None:               # il ne reste plus aucune carte
          print 'Terminé !'        # dans la liste
       else:
          print jeu.nom_carte(c)   # valeur et couleur de la carte
    
  4. Complément de l'exercice précédent : Définir deux joueurs A et B. Instancier deux jeux de cartes (un pour chaque joueur) et les mélanger. Ensuite, à l'aide d'une boucle, tirer 52 fois une carte de chacun des deux jeux et comparer leurs valeurs. Si c'est la première des 2 qui a la valeur la plus élevée, on ajoute un point au joueur A. Si la situation contraire se présente, on ajoute un point au joueur B. Si les deux valeurs sont égales, on passe au tirage suivant. Au terme de la boucle, comparer les comptes de A et B pour déterminer le gagnant.

Solution

  1. Voir ci-dessous.
  2. #(classes de cylindres et de cônes) :
    # Classes dérivées - polymorphisme
    
    class Cercle:
        def __init__(self, rayon):
            self.rayon = rayon
    
        def surface(self):
            return 3.1416 * self.rayon**2
            
    class Cylindre(Cercle):
        def __init__(self, rayon, hauteur):
            Cercle.__init__(self, rayon)
            self.hauteur = hauteur
            
        def volume(self):
            return self.surface()*self.hauteur
            
            # la méthode surface() est héritée de la classe parente
            
    class Cone(Cylindre):
        def __init__(self, rayon, hauteur):
            Cylindre.__init__(self, rayon, hauteur)
                    
        def volume(self):
            return Cylindre.volume(self)/3
            
            # cette nouvelle méthode volume() remplace celle que
            # l'on a héritée de la classe parente (exemple de polymorphisme)
                                
    cyl = Cylindre(5, 7)
    print cyl.surface()
    print cyl.volume()
    
    co = Cone(5,7)
    print co.surface()
    print co.volume()
    
  3. # Tirage de cartes
    
    from random import randrange
    
    class JeuDeCartes:
        """Jeu de cartes"""
        # attributs de classe (communs à toutes les instances) :
        couleur = ('Pique', 'Trèfle', 'Carreau', 'Cœur')
        valeur = (0, 0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 'valet', 'dame', 'roi', 'as')
    
        def __init__(self):
            "Construction de la liste des 52 cartes"
            self.carte =[]          
            for coul in range(4):
                for val in range(13):
                    self.carte.append((val +2, coul))   # la valeur commence à 2
    
        def nom_carte(self, c):
            "Renvoi du nom de la carte c, en clair"
            return "%s de %s" % (self.valeur[c[0]], self.couleur[c[1]])
            
        def battre(self):
            "Mélange des cartes"
            t = len(self.carte)             # nombre de cartes restantes
            # pour mélanger, on procède à un nombre d'échanges équivalent :
            for i in range(t):
                # tirage au hasard de 2 emplacements dans la liste :         
                h1, h2 = randrange(t), randrange(t)     
                # échange des cartes situées à ces emplacements :
                self.carte[h1], self.carte[h2] = self.carte[h2], self.carte[h1]
            
        def tirer(self):
            "Tirage de la première carte de la pile"
            t = len(self.carte)             # vérifier qu'il reste des cartes 
            if t >0:                        
                carte = self.carte[0]       # choisir la première carte du jeu
                del(self.carte[0])          # la retirer du jeu
                return carte                # en renvoyer copie au prog. appelant
            else:
                return None                 # facultatif
    
    
    ### Programme test :
    
    if __name__ == '__main__':
        jeu = JeuDeCartes()                 # instanciation d'un objet
        jeu.battre()                        # mélange des cartes
        for n in range(53):                 # tirage des 52 cartes : 
            c = jeu.tirer()  		 
            if c == None:                   # il ne reste aucune carte
                print 'Terminé !'           # dans la liste
            else:
                print jeu.nom_carte(c)      # valeur et couleur de la carte
    
  4. #(On supposera que l'exercice précédent a été sauvegardé sous le nom cartes.py)
    # Bataille de cartes
    
    from cartes import JeuDeCartes
    
    jeuA = JeuDeCartes()        # instanciation du premier jeu      
    jeuB = JeuDeCartes()        # instanciation du second jeu      
    jeuA.battre()               # mélange de chacun
    jeuB.battre()
    pA, pB = 0, 0               # compteurs de points des joueurs A et B
    
    # tirer 52 fois une carte de chaque jeu :
    for n in range(52):         
        cA, cB = jeuA.tirer(), jeuB.tirer()
        vA, vB = cA[0], cB[0]   # valeurs de ces cartes
        if vA > vB:
            pA += 1
        elif vB > vA:
            pB += 1             # (rien ne se passe si vA = vB)
        # affichage des points successifs et des cartes tirées :
        print "%s * %s ==> %s * %s" % (jeuA.nom_carte(cA), jeuB.nom_carte(cB), pA, pB) 
    
    print "le joueur A obtient %s points, le joueur B en obtient %s." % (pA, pB)
    


Exercices

Créer vous-même un nouveau module de classes, en encodant les lignes d'instruction ci-dessous dans un fichier que vous nommerez formes.py :

class Rectangle:
    "Classe de rectangles"
    def __init__(self, longueur = 30, largeur = 15):
        self.L = longueur
        self.l = largeur
        self.nom ="rectangle"

    def perimetre(self):
        return "(%s + %s) * 2 = %s" % (self.L, self.l, 
                                            (self.L + self.l)*2)
    def surface(self):
        return "%s * %s = %s" % (self.L, self.l, self.L*self.l)

    def mesures(self):
        print "Un %s de %s sur %s" % (self.nom, self.L, self.l)
        print "a une surface de %s" % (self.surface(),)
        print "et un périmètre de %s\n" % (self.perimetre(),)

class Carre(Rectangle):
    "Classe de carrés"
    def __init__(self, cote =10):
        Rectangle.__init__(self, cote, cote)
        self.nom ="carré"

if __name__ == "__main__":
    r1 = Rectangle(15, 30)
    r1.mesures()    
    c1 = Carre(13)
    c1.mesures()

Une fois ce module enregistré, vous pouvez l'utiliser de deux manières : soit vous en lancez l'exécution comme celle d'un programme ordinaire, soit vous l'importez dans un script quelconque ou depuis la ligne de commande, pour en utiliser les classes :

>>> import formes
>>> f1 = formes.Rectangle(27, 12)
>>> f1.mesures()
Un rectangle de 27 sur 12
a une surface de 27 * 12 = 324
et un périmètre de (27 + 12) * 2 = 78

>>> f2 = formes.Carre(13)
>>> f2.mesures()
Un carré de 13 sur 13
a une surface de 13 * 13 = 169
et un périmètre de (13 + 13) * 2 = 52

On voit dans ce script que la classe Carre() est construite par dérivation à partir de la classe Rectangle() dont elle hérite toutes les caractéristiques. En d'autres termes, la classe Carre() est une classe fille de la classe Rectangle().

Vous pouvez remarquer encore une fois que le constructeur de la classe Carre() fait appel au constructeur de sa classe parente ( Rectangle.__init__() ), en lui transmettant la référence de l'instance (c'est-à-dire self) comme premier argument.

Quant à l'instruction :

if __name__ == "__main__":

placée à la fin du module, elle sert à déterminer si le module est « lancé » en tant que programme (auquel cas les instructions qui suivent doivent être exécutées), ou au contraire utilisé comme une bibliothèque de classes importée ailleurs. Dans ce cas cette partie du code est sans effet.


Solution

Références

modifier


Classes et Interfaces graphiques

La programmation orientée objet convient particulièrement bien au développement d'applications avec interface graphique. Des bibliothèques de classes comme Tkinter ou wxPython fournissent une base de widgets très étoffée, que nous pouvons adapter à nos besoins par dérivation. Dans ce chapitre, nous allons utiliser à nouveau la bibliothèque Tkinter, mais en appliquant les concepts décrits dans les pages précédentes, et en nous efforçant de mettre en évidence les avantages qu'apporte l'orientation objet dans nos programmes.

« Code des couleurs » : un petit projet bien encapsulé

modifier

Nous allons commencer par un petit projet qui nous a été inspiré par le cours d'initiation à l'électronique. L'application que nous décrivons ci-après permet de retrouver rapidement le code de trois couleurs qui correspond à une résistance électrique de valeur bien déterminée.

Pour rappel, la fonction des résistances électriques consiste à s'opposer (à résister) plus ou moins bien au passage du courant. Les résistances se présentent concrètement sous la forme de petites pièces tubulaires cerclées de bandes de couleur (en général 3). Ces bandes de couleur indiquent la valeur numérique de la résistance, en fonction du code suivant :

 Chaque couleur correspond conventionnellement à l'un des chiffres de zéro à neuf : Noir = 0  ; Brun = 1  ; Rouge = 2  ; Orange = 3  ; Jaune = 4 ; Vert = 5  ; Bleu = 6  ; Violet = 7  ; Gris = 8  ; Blanc = 9.

On oriente la résistance de manière telle que les bandes colorées soient placées à gauche. La valeur de la résistance – exprimée en ohms (Ω) - s'obtient en lisant ces bandes colorées également à partir de la gauche : les deux premières bandes indiquent les deux premiers chiffres de la valeur numérique ; il faut ensuite accoler à ces deux chiffres un nombre de zéros égal à l'indication fournie par la troisième bande. Par exemple supposons qu'à partir de la gauche, les bandes colorées soient jaune, violette et verte et que la valeur de cette résistance est 4700000 Ω, ou 4700 kΩ, ou encore 4,7 MΩ.

Ce système ne permet évidemment de préciser une valeur numérique qu'avec deux chiffres significatifs seulement. Il est toutefois considéré comme largement suffisant pour la plupart des applications électroniques « ordinaires » (radio, TV, etc.)

Cahier des charges de notre programme
 

Notre application doit faire apparaître une fenêtre comportant un dessin de la résistance, ainsi qu'un champ d'entrée dans lequel l'utilisateur peut encoder une valeur numérique. Un bouton « Montrer » déclenche la modification du dessin de la résistance, de telle façon que les trois bandes de couleur se mettent en accord avec la valeur numérique introduite.

Contrainte : Le programme doit accepter toute entrée numérique fournie sous forme entière ou réelle, dans les limites de 10 à 1011 Ω. Par exemple, une valeur telle que 4.78e6 doit être acceptée et arrondie correctement, c'est-à-dire convertie en 4800000 Ω.

Mise en œuvre concrète

Nous construisons cette application simple sous la forme d'une classe. Sa seule utilité présente consiste à nous fournir un espace de noms commun dans lequel nous pouvons encapsuler nos variables et nos fonctions, ce qui nous permet de nous passer de variables globales. En effet :

  • Les variables auxquelles nous souhaitons pouvoir accéder de partout sont déclarées comme des attributs d'instance (nous attachons chacune d'elles à l'instance à l'aide de self).
  • Les fonctions sont déclarées comme des méthodes, et donc attachées elles aussi à self.

Au niveau principal du programme, nous nous contentons d'instancier un objet de la classe ainsi construite (aucune méthode de cet objet n'est activée de l'extérieur).

class Application:
    def __init__(self):
        """Constructeur de la fenêtre principale"""
        self.root =Tk()
        self.root.title('Code des couleurs')
        self.dessineResistance()
        Label(self.root,
              text ="Entrez la valeur de la résistance, en ohms :").grid(row =2)
        Button(self.root, text ='Montrer',
               command =self.changeCouleurs).grid(row =3, sticky = W)
        Button(self.root, text ='Quitter',
               command =self.root.quit).grid(row =3, sticky = E)
        self.entree = Entry(self.root, width =14)
        self.entree.grid(row =3)
        # Code des couleurs pour les valeurs de zéro à neuf :
        self.cc = ['black','brown','red','orange','yellow',
                   'green','blue','purple','grey','white']
        self.root.mainloop()

    def dessineResistance(self):
        """Canevas avec un modèle de résistance à trois lignes colorées"""
        self.can = Canvas(self.root, width=250, height =100, bg ='ivory')
        self.can.grid(row =1, pady =5, padx =5)
        self.can.create_line(10, 50, 240, 50, width =5)        # fils
        self.can.create_rectangle(65, 30, 185, 70, fill ='light grey', width =2)
        # Dessin des trois lignes colorées (noires au départ) :
        self.ligne =[]           # on mémorisera les trois lignes dans 1 liste
        for x in range(85,150,24):
            self.ligne.append(self.can.create_rectangle(x, 30, x+12, 70, fill='black', width=0))

    def changeCouleurs(self):
        """Affichage des couleurs correspondant à la valeur entrée"""
        self.v1ch = self.entree.get()     # la méthode get() renvoie une chaîne
        try:
            v = float(self.v1ch)          # conversion en valeur numérique
        except:
            err =1                        # erreur : entrée non numérique
        else:
            err =0
        if err ==1 or v < 10 or v > 1e11 :
            self.signaleErreur()          # entrée incorrecte ou hors limites
        else:
            li =[0]*3                     # liste des 3 codes à afficher
            logv = int(log10(v))          # partie entière du logarithme
            ordgr = 10**logv              # ordre de grandeur
            # extraction du premier chiffre significatif :
            li[0] = int(v/ordgr)          # partie entière
            decim = v/ordgr - li[0]       # partie décimale
            # extraction du second chiffre significatif :
            li[1] = int(decim*10 +.5)     # +.5 pour arrondir correctement
            # nombre de zéros à accoler aux 2 chiffres significatifs :
            li[2] = logv -1
            # Coloration des 3 lignes :
            for n in range(3):
                self.can.itemconfigure(self.ligne[n], fill =self.cc[li[n]])

    def signaleErreur(self):
        self.entree.configure(bg ='red')  # colorer le fond du champ
        self.root.after(1000, self.videEntree) # après 1 seconde, effacer

    def videEntree(self):
        self.entree.configure(bg ='white')     # rétablir le fond blanc
        self.entree.delete(0, len(self.v1ch))  # enlever les car. présents

# Programme principal :
from Tkinter import *
from math import log10   # logarithmes en base 10
f = Application()        # instanciation de l'objet application
Commentaires
  • Ligne 1 : La classe est définie sans référence à une classe parente (pas de parenthèses). Il s'agira donc d'une nouvelle classe indépendante.
  • Lignes 2 à 14 : Le constructeur de la classe instancie les widgets nécessaires : pour améliorer la lisibilité du programme, on a placé l'instanciation du canevas (avec le dessin de la résistance) dans une méthode séparée dessineResistance(). Les boutons et le libellé ne sont pas mémorisés dans des variables, parce que l'on ne souhaite pas y faire référence ailleurs dans le programme. Le positionnement des widgets dans la fenêtre utilise la méthode grid().
  • Lignes 15-17 : Le code des couleurs est mémorisé dans une simple liste.
  • Ligne 18 : La dernière instruction du constructeur démarre l'application.
  • Lignes 20 à 30 : Le dessin de la résistance se compose d'une ligne et d'un premier rectangle gris clair, pour le corps de la résistance et ses deux fils. Trois autres rectangles figureront les bandes colorées que le programme devra modifier en fonction des entrées de l'utilisateur. Ces bandes sont noires au départ ; elles sont référencées dans la liste self.ligne.
  • Lignes 32 à 53 : Ces lignes contiennent l'essentiel de la fonctionnalité du programme.

L'entrée brute fournie par l'utilisateur est acceptée sous la forme d'une chaîne de caractères.
À la ligne 36, on essaie de convertir cette chaîne en une valeur numérique de type float. Si la conversion échoue, on mémorise l'erreur. Si l'on dispose bien d'une valeur numérique, on vérifie ensuite qu'elle se situe effectivement dans l'intervalle autorisé (de 10 Ω à 1011 Ω). Si une erreur est détectée, on signale à l'utilisateur que son entrée est incorrecte en colorant de rouge le fond du champ d'entrée, qui est ensuite vidé de son contenu (lignes 55 à 61).

  • Lignes 45-46 : Les mathématiques viennent à notre secours pour extraire de la valeur numérique son ordre de grandeur (c'est-à-dire l'exposant de 10 le plus proche). Veuillez consulter votre cours de mathématiques pour de plus amples explications concernant les logarithmes.
  • Lignes 47-48 : Une fois connu l'ordre de grandeur, il devient relativement facile d'extraire du nombre traité ses deux premiers chiffres significatifs. Exemple : Supposons que la valeur entrée soit 31687. Le logarithme de ce nombre est 4,50088... dont la partie entière (4) nous donne l'ordre de grandeur de la valeur entrée (soit 104). Pour extraire de celle-ci son premier chiffre significatif, il suffit de la diviser par 104, soit 10000, et de conserver seulement la partie entière du résultat (3).
  • Lignes 49 à 51 : Le résultat de la division effectuée dans le paragraphe précédent est 3,1687.

Nous récupérons la partie décimale de ce nombre à la ligne 49, soit 0,1687 dans notre exemple.
Si nous le multiplions par dix, ce nouveau résultat comporte une partie entière qui n'est rien d'autre que notre second chiffre significatif (1 dans notre exemple).
Nous pourrions facilement extraire ce dernier chiffre, mais puisque c'est le dernier, nous souhaitons encore qu'il soit correctement arrondi. Pour ce faire, il suffit d'ajouter une demi unité au produit de la multiplication par dix, avant d'en extraire la valeur entière. Dans notre exemple, en effet, ce calcul donnera donc 1,687 + 0,5 = 2,187 , dont la partie entière (2) est bien la valeur arrondie recherchée.

  • Ligne 53 : Le nombre de zéros à accoler aux deux chiffres significatifs correspond au calcul de l'ordre de grandeur. Il suffit de retirer une unité au logarithme.
  • Ligne 56 : Pour attribuer une nouvelle couleur à un objet déjà dessiné dans un canevas, on utilise la méthode itemconfigure(). Nous utilisons donc cette méthode pour modifier l'option fill de chacune des bandes colorées, en utilisant les noms de couleur extraits de la liste self.cc grâce à aux trois indices li[1], li[2] et li[3] qui contiennent les 3 chiffres correspondants.

Exercices

  1. Modifiez le script ci-dessus de telle manière que le fond d'image devienne bleu clair ('light blue'), que le corps de la résistance devienne beige ('beige'), que le fil de cette résistance soit plus fin, et que les bandes colorées indiquant la valeur soient plus larges.
  2. Modifiez le script ci-dessus de telle manière que l'image dessinée soit deux fois plus grande.
  3. Modifiez le script ci-dessus de telle manière qu'il devienne possible d'entrer aussi des valeurs de résistances comprises entre 1 et 10 Ω. Pour ces valeurs, le premier anneau coloré devra rester noir, les deux autres indiqueront la valeur en Ω et dixièmes d' Ω.
  4. Modifiez le script ci-dessus de telle façon que le bouton « Montrer » ne soit plus nécessaire. Dans votre script modifié, il suffira de frapper <Enter> après avoir entré la valeur de la résistance, pour que l'affichage s'active.
  5. Modifiez le script ci-dessus de telle manière que les trois bandes colorées redeviennent noires dans les cas où l'utilisateur fournit une entrée inacceptable.

Solution

  1. Réfléchissez !
  2. Réfléchissez !
  3. Réfléchissez !
  4. Réfléchissez !
  5. Réfléchissez !

« Petit train » : héritage, échange d'informations entre classes

modifier

Dans l'exercice précédent, nous n'avons exploité qu'une seule caractéristique des classes : l'encapsulation. Celle-ci nous a permis d'écrire un programme dans lequel les différentes fonctions (qui sont donc devenues des méthodes) peuvent chacune accéder à un même pool de variables : toutes celles qui sont définies comme étant attachées à self. Toutes ces variables peuvent être considérées en quelque sorte comme des variables globales à l'intérieur de l'objet.

Comprenez bien toutefois qu'il ne s'agit pas de véritables variables globales. Elles restent en effet strictement confinées à l'intérieur de l'objet, et il est déconseillé de vouloir y accéder de l'extérieur[1]. D'autre part, tous les objets que vous instancierez à partir d'une même classe posséderont chacun leur propre jeu de ces variables, qui sont donc bel et bien encapsulées dans ces objets. On les appelle pour cette raison des attributs d'instance.

Nous allons à présent passer à la vitesse supérieure et réaliser une petite application sur la base de plusieurs classes, afin d'examiner comment différents objets peuvent s'échanger des informations par l'intermédiaire de leurs méthodes. Nous allons également profiter de cet exercice pour vous montrer comment vous pouvez définir la classe principale de votre application graphique par dérivation d'une classe Tkinter préexistante, mettant ainsi à profit le mécanisme d'héritage.

Le projet développé ici très simple, mais il pourrait constituer une première étape dans la réalisation d'un logiciel de jeu. Il s'agit d'une fenêtre contenant un canevas et deux boutons. Lorsque l'on actionne le premier de ces deux boutons, un petit train apparaît dans le canevas. Lorsque l'on actionne le second bouton, quelques petits personnages apparaissent à certaines fenêtres des wagons.

 
Cahier des charges

L'application comportera deux classes :

  • La classe Application() sera obtenue par dérivation d'une des classes de base de Tkinter : elle mettra en place la fenêtre principale, son canevas et ses deux boutons.
  • Une classe Wagon(), indépendante, permettra d'instancier dans le canevas 4 objets-wagons similaires, dotés chacun d'une méthode perso(). Celle-ci sera destinée à provoquer l'apparition d'un petit personnage à l'une quelconque des trois fenêtres du wagon. L'application principale invoquera cette méthode différemment pour différents objets-wagons, afin de faire apparaître un choix de quelques personnages.
Implémentation
from Tkinter import *

def cercle(can, x, y, r):
    "dessin d'un cercle de rayon <r> en <x,y> dans le canevas <can>"
    can.create_oval(x-r, y-r, x+r, y+r)

class Application(Tk):
    def __init__(self):
        Tk.__init__(self)        # constructeur de la classe parente
        self.can =Canvas(self, width =475, height =130, bg ="white")
        self.can.pack(side =TOP, padx =5, pady =5)
        Button(self, text ="Train", command =self.dessine).pack(side =LEFT)
        Button(self, text ="Hello", command =self.coucou).pack(side =LEFT)
        
    def dessine(self):
        "instanciation de 4 wagons dans le canevas"
        self.w1 = Wagon(self.can, 10, 30)
        self.w2 = Wagon(self.can, 130, 30)
        self.w3 = Wagon(self.can, 250, 30)
        self.w4 = Wagon(self.can, 370, 30)
        
    def coucou(self):
        "apparition de personnages dans certaines fenêtres"
        self.w1.perso(3)        # 1er wagon, 3e fenêtre
        self.w3.perso(1)        # 3e wagon, 1e fenêtre
        self.w3.perso(2)        # 3e wagon, 2e fenêtre
        self.w4.perso(1)        # 4e wagon, 1e fenêtre
        
class Wagon:
    def __init__(self, canev, x, y):
        "dessin d'un petit wagon en <x,y> dans le canevas <canev>"
        # mémorisation des paramètres dans des variables d'instance :
        self.canev, self.x, self.y = canev, x, y
        # rectangle de base : 95x60 pixels :
        canev.create_rectangle(x, y, x+95, y+60)
        # 3 fenêtres de 25x40 pixels, écartées de 5 pixels :
        for xf in range(x+5, x+90, 30):
            canev.create_rectangle(xf, y+5, xf+25, y+40)
        # 2 roues de rayon égal à 12 pixels  :
        cercle(canev, x+18, y+73, 12)
        cercle(canev, x+77, y+73, 12)
  
    def perso(self, fen):
        "apparition d'un petit personnage à la fenêtre <fen>"
        # calcul des coordonnées du centre de chaque fenêtre :
        xf = self.x + fen*30 -12
        yf = self.y + 25
        cercle(self.canev, xf, yf, 10)      # visage
        cercle(self.canev, xf-5, yf-3, 2)   # œil gauche        
        cercle(self.canev, xf+5, yf-3, 2)   # œil droit
        cercle(self.canev, xf, yf+5, 3)     # bouche

app = Application()
app.mainloop()
Commentaires
  • Lignes 3 à 5 : Nous projetons de dessiner une série de petits cercles. Cette petite fonction nous facilitera le travail en nous permettant de définir ces cercles à partir de leur centre et leur rayon.
  • Lignes 7 à 13 : La classe principale de notre application est construite par dérivation de la classe de fenêtres Tk() importée du module Tkinter[2]. Comme nous l'avons expliqué au chapitre précédent, le constructeur d'une classe dérivée doit activer lui-même le constructeur de la classe parente, en lui transmettant la référence de l'instance comme premier argument. Les lignes 10 à 13 servent à mettre en place le canevas et les boutons.
  • Lignes 15 à 20 : Ces lignes instancient les 4 objets-wagons, produits à partir de la classe correspondante. Ceci pourrait être programmé plus élégamment à l'aide d'une boucle et d'une liste, mais nous le laissons ainsi afin de ne pas alourdir inutilement les explications qui suivent. Nous voulons placer nos objets-wagons dans le canevas, à des emplacements bien précis : il nous faut donc transmettre quelques informations au constructeur de ces objets : au moins la référence du canevas, ainsi que les coordonnées souhaitées. Ces considérations nous font également entrevoir, que lorsque nous définirons la classe Wagon(), nous devrons associer à sa méthode constructeur un nombre égal de paramètres pour réceptionner ces arguments.
  • Lignes 22 à 27 : Cette méthode est invoquée lorsque l'on actionne le second bouton. Elle invoque elle-même la méthode perso() de certains objets-wagons, avec des arguments différents, afin de faire apparaître les personnages aux fenêtres indiquées.
     Ces quelques lignes de code vous montrent donc comment un objet peut communiquer avec un autre en faisant appel à l'une ou l'autre de ses méthodes. Il s'agit là du mécanisme central de la programmation par objets : les objets sont des entités programmées qui s'échangent des messages et interagissent par l'intermédiaire de leurs méthodes.
    Idéalement, la méthode coucou() devrait comporter quelques instructions complémentaires, lesquelles vérifieraient d'abord si les objets-wagons concernés existent bel et bien, avant d'autoriser l'activation d'une de leurs méthodes. Nous n'avons pas inclus ce genre de garde-fou afin que l'exemple reste aussi simple que possible, mais cela entraîne la conséquence que vous ne pouvez pas actionner le second bouton avant le premier. (Pouvez-vous ajouter un correctif ?)
  • Lignes 29-30 : La classe Wagon() ne dérive d'aucune autre classe préexistante. Cependant, étant donné qu'il s'agit d'une classe d'objets graphiques, nous devons munir sa méthode constructeur de paramètres, afin de recevoir la référence du canevas auquel les dessins sont destinés, ainsi que les coordonnées de départ de ces dessins. Dans vos expérimentations éventuelles autour de cet exercice, vous pourriez bien évidemment ajouter encore d'autres paramètres : taille du dessin, orientation, couleur, vitesse, etc.
  • Lignes 31 à 51 : Ces instructions ne nécessitent guère de commentaires. La méthode perso() est dotée d'un paramètre qui indique celle des 3 fenêtres où il faut faire apparaître un petit personnage. Ici aussi nous n'avons pas prévu de garde-fou : vous pouvez invoquer cette méthode avec un argument égal à 4 ou 5, par exemple, ce qui produira des effets incorrects.
  • Lignes 53-54 : Pour démarrer l'application, il ne suffit pas d'instancier un objet de la classe Application() comme dans l'exemple de la rubrique précédente. Il faut également invoquer la méthode mainloop() qu'elle a hérité de sa classe parente. Vous pourriez cependant condenser ces deux instructions en une seule, laquelle serait alors : Application().mainloop()

Exercices

  1. Perfectionnez le script décrit ci-dessus, en ajoutant un paramètre couleur au constructeur de la classe Wagon(), lequel déterminera la couleur de la cabine du wagon. Arrangez-vous également pour que les fenêtres soient noires au départ, et les roues grises (pour réaliser ce dernier objectif, ajoutez aussi un paramètre couleur à la fonction cercle()). À cette même classe Wagon(), ajoutez encore une méthode allumer(), qui servira à changer la couleur des 3 fenêtres (initialement noires) en jaune, afin de simuler l'allumage d'un éclairage intérieur. Ajoutez un bouton à la fenêtre principale, qui puisse déclencher cet allumage. Profitez de l'amélioration de la fonction cercle() pour teinter le visage des petits personnages en rose (pink), leurs yeux et leurs bouches en noir, et instanciez les objets-wagons avec des couleurs différentes.

Solution

  1. from Tkinter import *
    
    def cercle(can, x, y, r, coul ='white'):
        "dessin d'un cercle de rayon <r> en <x,y> dans le canevas <can>"
        can.create_oval(x-r, y-r, x+r, y+r, fill =coul)
    
    class Application(Tk):
        def __init__(self):
            Tk.__init__(self)        # constructeur de la classe parente
            self.can =Canvas(self, width =475, height =130, bg ="white")
            self.can.pack(side =TOP, padx =5, pady =5)
            Button(self, text ="Train", command =self.dessine).pack(side =LEFT)
            Button(self, text ="Hello", command =self.coucou).pack(side =LEFT)
            Button(self, text ="Ecl34", command =self.eclai34).pack(side =LEFT)
            
        def dessine(self):
            "instanciation de 4 wagons dans le canevas"
            self.w1 = Wagon(self.can, 10, 30)
            self.w2 = Wagon(self.can, 130, 30, 'dark green')
            self.w3 = Wagon(self.can, 250, 30, 'maroon')
            self.w4 = Wagon(self.can, 370, 30, 'purple')
            
        def coucou(self):
            "apparition de personnages dans certaines fenêtres"
            self.w1.perso(3)        # 1er wagon, 3e fenêtre
            self.w3.perso(1)        # 3e wagon, 1e fenêtre
            self.w3.perso(2)        # 3e wagon, 2e fenêtre
            self.w4.perso(1)        # 4e wagon, 1e fenêtre
            
        def eclai34(self):
            "allumage de l'éclairage dans les wagons 3 & 4"
            self.w3.allumer()
            self.w4.allumer()
            
    class Wagon:
        def __init__(self, canev, x, y, coul ='navy'):
            "dessin d'un petit wagon en <x,y> dans le canevas <canev>"
            # mémorisation des paramètres dans des variables d'instance :
            self.canev, self.x, self.y = canev, x, y
            # rectangle de base : 95x60 pixels :
            canev.create_rectangle(x, y, x+95, y+60, fill =coul)
            # 3 fenêtres de 25x40 pixels, écartées de 5 pixels :
            self.fen =[]    # pour mémoriser les réf. des fenêtres 
            for xf in range(x +5, x +90, 30):
                self.fen.append(canev.create_rectangle(xf, y+5,
                                    xf+25, y+40, fill ='black'))
            # 2 roues de rayon égal à 12 pixels  :
            cercle(canev, x+18, y+73, 12, 'gray')
            cercle(canev, x+77, y+73, 12, 'gray')
      
        def perso(self, fen):
            "apparition d'un petit personnage à la fenêtre <fen>"
            # calcul des coordonnées du centre de chaque fenêtre :
            xf = self.x + fen*30 -12
            yf = self.y + 25
            cercle(self.canev, xf, yf, 10, "pink")      # visage
            cercle(self.canev, xf-5, yf-3, 2)   # œil gauche        
            cercle(self.canev, xf+5, yf-3, 2)   # œil droit
            cercle(self.canev, xf, yf+5, 3)     # bouche
            
        def allumer(self):
            "déclencher l'éclairage interne du wagon"
            for f in self.fen:
                self.canev.itemconfigure(f, fill ='yellow')
    
    Application().app.mainloop()
    

« OscilloGraphe » : un widget personnalisé

modifier

Le projet qui suit va nous entraîner encore un petit peu plus loin. Nous allons y construire une nouvelle classe de widget, qu'il sera possible d'intégrer dans nos projets futurs comme n'importe quel widget standard. Comme la classe principale de l'exercice précédent, cette nouvelle classe sera construite par dérivation d'une classe Tkinter préexistante.

Le sujet concret de cette application nous est inspiré par le cours de physique. Pour rappel :

Un mouvement vibratoire harmonique se définit comme étant la projection d'un mouvement circulaire uniforme sur une droite. Les positions successives d'un mobile qui effectue ce type de mouvement sont traditionnellement repérées par rapport à une position centrale : on les appelle alors des élongations. L'équation qui décrit l'évolution de l'élongation d'un tel mobile au cours du temps est toujours de la forme  , dans laquelle e représente l'élongation du mobile à tout instant t. Les constantes A, f et φ désignent respectivement l'amplitude, la fréquence et la phase du mouvement vibratoire.

 

Le but du présent projet est de fournir un instrument de visualisation simple de ces différents concepts, à savoir un système d'affichage automatique de graphiques élongation/temps. L'utilisateur pourra choisir librement les valeurs des paramètres A, f et φ, et observer les courbes qui en résultent.

Le widget que nous allons construire d'abord s'occupera de l'affichage proprement dit. Nous construirons ensuite d'autres widgets pour faciliter l'entrée des paramètres A, f et φ.

Veuillez donc encoder le script ci-dessous et le sauvegarder dans un fichier, auquel vous donnerez le nom oscillo.py. Vous réaliserez ainsi un véritable module contenant une classe (vous pourrez par la suite ajouter d'autres classes dans ce même module, si le cœur vous en dit).

from Tkinter import *
from math import sin, pi

class OscilloGraphe(Canvas):
    "Canevas spécialisé, pour dessiner des courbes élongation/temps"
    def __init__(self, boss =None, larg=200, haut=150):
        "Constructeur du graphique : axes et échelle horiz."
        # construction du widget parent :
        Canvas.__init__(self)                           # appel au constructeur 
        self.configure(width=larg, height=haut)         # de la classe parente 
        self.larg, self.haut = larg, haut                        # mémorisation
        # tracé des axes de référence :
        self.create_line(10, haut/2, larg, haut/2, arrow=LAST)   # axe X
        self.create_line(10, haut-5, 10, 5, arrow=LAST)          # axe Y
        # tracé d'une échelle avec 8 graduations :
        pas = (larg-25)/8.          # intervalles de l'échelle horizontale
        for t in range(1, 9):
            stx = 10 + t*pas        # +10 pour partir de l'origine
            self.create_line(stx, haut/2-4, stx, haut/2+4)
        
    def traceCourbe(self, freq=1, phase=0, ampl=10, coul='red'):
        "tracé d'un graphique élongation/temps sur 1 seconde"
        curve =[]                       # liste des coordonnées
        pas = (self.larg-25)/1000.      # l'échelle X correspond à 1 seconde
        for t in range(0,1001,5):       # que l'on divise en 1000 ms.
            e = ampl*sin(2*pi*freq*t/1000 - phase)
            x = 10 + t*pas
            y = self.haut/2 - e*self.haut/25
            curve.append((x,y))
        n = self.create_line(curve, fill=coul, smooth=1)
        return n                        # n = numéro d'ordre du tracé

#### Code pour tester la classe : ####

if __name__ == '__main__':
    root = Tk()
    gra = OscilloGraphe(root, 250, 180)
    gra.pack()
    gra.configure(bg ='ivory', bd =2, relief=SUNKEN)
    gra.traceCourbe(2, 1.2, 10, 'purple')
    root.mainloop()

Le niveau principal du script est constitué par les lignes 35 à 41. Les lignes de code situées après l'instruction if __name__ == '__main__': ne sont pas exécutées si le script est importé en tant que module. Si on lance le script comme application principale, par contre, ces instructions sont exécutées.

Nous disposons ainsi d'un mécanisme intéressant, qui nous permet d'intégrer des instructions de test à l'intérieur des modules, même si ceux-ci sont destinés à être importés dans d'autres scripts.

Lancez donc l'exécution du script de la manière habituelle. Vous devriez obtenir un affichage similaire à celui qui est reproduit à la page précédente.

Expérimentation

Commençons d'abord par expérimenter quelque peu la classe que nous venons de construire. Ouvrez une fenêtre de terminal (Python shell), et entrez les instructions ci-dessous directement à la ligne de commande :

>>> from oscillo import *
>>> g1 = OscilloGraphe()
>>> g1.pack()

Après importation des classes du module oscillo, nous instancions un premier objet g1, de la classe OscilloGraphe().

Puisque nous ne fournissons aucun argument, l'objet possède les dimensions par défaut, définies dans le constructeur de la classe. Remarquons au passage que nous n'avons même pas pris la peine de définir d'abord une fenêtre maître pour y placer ensuite notre widget. Tkinter nous pardonne cet oubli et nous en fournit une automatiquement !

 
>>> g2 = OscilloGraphe(haut=200, larg=250)
>>> g2.pack()
>>> g2.traceCourbe()

Par ces instructions, nous créons un second widget de la même classe, en précisant cette fois ses dimensions (hauteur et largeur, dans n'importe quel ordre).

Ensuite, nous activons la méthode traceCourbe() associée à ce widget. Étant donné que nous ne lui fournissons aucun argument, la sinusoïde qui apparaît correspond aux valeurs prévues par défaut pour les paramètres A, f et φ.

>>> g3 = OscilloGraphe(larg=220)
>>> g3.configure(bg='white', bd=3, relief=SUNKEN)
>>> g3.pack(padx=5,pady=5)
>>> g3.traceCourbe(phase=1.57, coul='purple')
>>> g3.traceCourbe(phase=3.14, coul='dark green')

Pour comprendre la configuration de ce troisième widget, il faut nous rappeler que la classe OscilloGraphe() a été construite par dérivation de la classe Canvas(). Elle hérite donc de toutes les propriétés de celle-ci, ce qui nous permet de choisir la couleur de fond, la bordure, etc., en utilisant les mêmes arguments que ceux qui sont à notre disposition lorsque nous configurons un canevas.

Nous faisons ensuite apparaître deux tracés successifs, en faisant appel deux fois à la méthode traceCourbe(), à laquelle nous fournissons des arguments pour la phase et la couleur.

Exercices

  1. Créez un quatrième widget, de taille 400 x 300, couleur de fond jaune, et faites-y apparaître plusieurs courbes correspondant à des fréquences et des amplitudes différentes.

Solution

  1. Réfléchissez !

Il est temps à présent que nous analysions la structure de la classe qui nous a permis d'instancier tous ces widgets. Nous avons enregistré cette classe dans le module oscillo.py.

Cahier des charges

Nous souhaitons définir une nouvelle classe de widget, capable d'afficher automatiquement les graphiques élongation/temps correspondant à divers mouvements vibratoires harmoniques.

Ce widget doit pouvoir être dimensionné à volonté au moment de son instanciation. Il fait apparaître deux axes cartésiens X et Y munis de flèches. L'axe X représente l'écoulement du temps pendant une seconde au total, et il est muni d'une échelle comportant 8 intervalles.

Une méthode traceCourbe() est associée à ce widget. Elle provoque le tracé du graphique élongation/temps pour un mouvement vibratoire dont on fournit la fréquence (entre 0.25 et 10 Hz), la phase (entre 0 et 2π radians) et l'amplitude (entre 1 et 10 ; échelle arbitraire).

Implémentation
  • Ligne 4 : La classe OscilloGraphe() est créée par dérivation de la classe Canvas(). Elle hérite donc toutes les propriétés de celle-ci : on pourra configurer les objets de cette nouvelle classe en utilisant les nombreuses options déjà disponibles pour la classe Canvas().
  • Ligne 6 : La méthode « constructeur » utilise 3 paramètres, qui sont tous optionnels puisque chacun d'entre eux possède une valeur par défaut. Le paramètre boss ne sert qu'à réceptionner la référence d'une fenêtre maîtresse éventuelle (voir exemples suivants). Les paramètres larg et haut (largeur et hauteur) servent à assigner des valeurs aux options width et height du canevas parent, au moment de l'instanciation.
  • Lignes 9 et 10 : La première opération que doit accomplir le constructeur d'une classe dérivée, c'est activer le constructeur de sa classe parente. En effet : nous ne pouvons hériter toute la fonctionnalité de la classe parente, que si cette fonctionnalité a été effectivement mise en place.
    Nous activons donc le constructeur de la classe Canvas() à la ligne 9 , et nous ajustons deux de ses options à la ligne 10. Notez au passage que nous pourrions condenser ces deux lignes en une seule, qui deviendrait en l'occurrence :
Canvas.__init__(self, width=larg, height=haut)

Nous devons transmettre à ce constructeur la référence de l'instance présente (self) comme premier argument.

  • Ligne 11 : Il est nécessaire de mémoriser les paramètres larg et haut dans des variables d'instance, parce que nous devrons pouvoir y accéder aussi dans la méthode traceCourbe().
  • Lignes 13 et 14 : Pour tracer les axes X et Y, nous utilisons les paramètres larg et haut, ainsi ces axes sont automatiquement mis à dimension. L'option arrow=LAST permet de faire apparaître une petite flèche à l'extrémité de chaque ligne.
  • Lignes 16 à 19 : Pour tracer l'échelle horizontale, on commence par réduire de 25 pixels la largeur disponible, de manière à ménager des espaces aux deux extrémités. On divise ensuite en 8 intervalles, que l'on visualise sous la forme de 8 petits traits verticaux.
  • Ligne 21 : La méthode traceCourbe() pourra être invoquée avec quatre arguments. Chacun d'entre eux pourra éventuellement être omis, puisque chacun des paramètres correspondants possède une valeur par défaut. Il sera également possible de fournir les arguments dans n'importe quel ordre.
  • Lignes 23 à 31 : Pour le tracé de la courbe, la variable t prend successivement toutes les valeurs de 0 à 1000, et on calcule à chaque fois l'élongation e correspondante, à l'aide de la formule théorique (ligne 26). Les couples de valeurs t et e ainsi trouvées sont mises à l'échelle et transformées en coordonnées x, y aux lignes 27 & 28, puis accumulées dans la liste curve.
  • Lignes 30 et 31 : La méthode create_line() trace alors la courbe correspondante en une seule opération, et elle renvoie le numéro d'ordre du nouvel objet ainsi instancié dans le canevas (ce numéro d'ordre nous permettra d'y accéder encore par après : pour l'effacer, par exemple). L'option smooth =1 améliore l'aspect final, par lissage.

Exercices

  1. Modifiez le script de manière à ce que l'axe de référence vertical comporte lui aussi une échelle, avec 5 tirets de part et d'autre de l'origine.
  2. Comme les widgets de la classe Canvas() dont il dérive, votre widget peut intégrer des indications textuelles. Il suffit pour cela d'utiliser la méthode create_text(). Cette méthode attend au moins trois arguments : les coordonnées x et y de l'emplacement où vous voulez faire apparaître votre texte, et puis le texte lui-même, bien entendu. D'autres arguments peuvent être transmis sous forme d'options, pour préciser par exemple la police de caractères et sa taille. Afin de voir comment cela fonctionne, ajoutez provisoirement la ligne suivante dans le constructeur de la classe OscilloGraphe(), puis relancez le script :
    self.create_text(130, 30, text = "Essai", anchor =CENTER)
    

    Utilisez cette méthode pour ajouter au widget les indications suivantes aux extrémités des axes de référence : e (pour « élongation ») le long de l'axe vertical, et t (pour « temps ») le long de l'axe horizontal. Le résultat pourrait ressembler à ceci (figure de gauche) :

       
  3. Vous pouvez compléter encore votre widget, en y faisant apparaître une grille de référence, plutôt que de simples tirets le long des axes. Pour éviter que cette grille ne soit trop visible, vous pouvez colorer ses traits en gris (option fill = 'grey'), comme dans la figure de droite.
  4. Complétez encore votre widget en y faisant apparaître des repères numériques.

Solution

  1. Réfléchissez !
  2. Réfléchissez !
  3. Réfléchissez !
  4. Réfléchissez !

« Curseurs » : un widget composite

modifier

Dans l'exercice précédent, vous avez construit un nouveau type de widget que vous avez sauvegardé dans le module oscillo.py. Conservez soigneusement ce module, car vous l'intégrerez bientôt dans un projet plus complexe.

Pour l'instant, vous allez construire encore un autre widget, plus interactif cette fois. Il s'agira d'une sorte de panneau de contrôle comportant trois curseurs de réglage et une case à cocher. Comme le précédent, ce widget est destiné à être réutilisé dans une application de synthèse.

Présentation du widget « Scale »

modifier
 

Commençons d'abord par découvrir un widget de base, que nous n'avions pas encore utilisé jusqu'ici : Le widget Scale se présente comme un curseur qui coulisse devant une échelle. Il permet à l'utilisateur de choisir rapidement la valeur d'un paramètre quelconque, d'une manière très attrayante.

Le petit script ci-dessous vous montre comment le paramétrer et l'utiliser dans une fenêtre :

from Tkinter import *

def updateLabel(x):
    lab.configure(text='Valeur actuelle = ' + str(x))
    
root = Tk()
Scale(root, length=250, orient=HORIZONTAL, label ='Réglage :',
      troughcolor ='dark grey', sliderlength =20,
      showvalue =0, from_=-25, to=125, tickinterval =25,
      command=updateLabel).pack()
lab = Label(root)
lab.pack()

root.mainloop()

Ces lignes ne nécessitent guère de commentaires.

Vous pouvez créer des widgets Scale de n'importe quelle taille (option length), en orientation horizontale (comme dans notre exemple) ou verticale (option orient = VERTICAL).

Les options from_ (attention : n'oubliez pas le caractère 'souligné' !) et to définissent la plage de réglage. L'intervalle entre les repères numériques est défini dans l'option tickinterval, etc.

La fonction désignée dans l'option command est appelée automatiquement chaque fois que le curseur est déplacé, et la position actuelle du curseur par rapport à l'échelle lui est transmise en argument. Il est donc très facile d'utiliser cette valeur pour effectuer un traitement quelconque. Considérez par exemple le paramètre x de la fonction updateLabel(), dans notre exemple.

Le widget Scale constitue une interface très intuitive et attrayante pour proposer différents réglages aux utilisateurs de vos programmes. Nous allons à présent l'incorporer en plusieurs exemplaires dans une nouvelle classe de widget : un panneau de contrôle destiné à choisir la fréquence, la phase et l'amplitude pour un mouvement vibratoire, dont nous afficherons ensuite le graphique élongation/temps à l'aide du widget oscilloGraphe construit dans les pages précédentes.

Construction d'un panneau de contrôle à trois curseurs

modifier

Comme le précédent, le script que nous décrivons ci-dessous est destiné à être sauvegardé dans un module, que vous nommerez cette fois curseurs.py. Les classes que vous sauvegardez ainsi seront réutilisées (par importation) dans une application de synthèse. Nous attirons votre attention sur le fait que le code ci-dessous peut être raccourci de différentes manières. Nous ne l'avons pas optimisé d'emblée, parce que cela nécessiterait d'y incorporer un concept supplémentaire (les expressions lambda), ce que nous préférons éviter pour l'instant.

Vous savez déjà que les lignes de code placées à la fin du script permettent de tester son fonctionnement. Vous devriez obtenir une fenêtre semblable à celle-ci :

 
from Tkinter import *
from math import pi

class ChoixVibra(Frame):
    """Curseurs pour choisir fréquence, phase & amplitude d'une vibration"""
    def __init__(self, boss =None, coul ='red'):
        Frame.__init__(self)        # constructeur de la classe parente
        # Initialisation de quelques attributs d'instance :
        self.freq, self.phase, self.ampl, self.coul = 0, 0, 0, coul
        # Variable d'état de la case à cocher :
        self.chk = IntVar()                 # 'objet-variable' Tkinter        
        Checkbutton(self, text='Afficher', variable=self.chk,
                    fg = self.coul, command = self.setCurve).pack(side=LEFT)
        # Définition des 3 widgets curseurs :
        Scale(self, length=150, orient=HORIZONTAL, sliderlength =25,
              label ='Fréquence (Hz) :', from_=1., to=9., tickinterval =2,
              resolution =0.25,
              showvalue =0, command = self.setFrequency).pack(side=LEFT)
        Scale(self, length=150, orient=HORIZONTAL, sliderlength =15,
              label ='Phase (degrés) :', from_=-180, to=180, tickinterval =90,
              showvalue =0, command = self.setPhase).pack(side=LEFT)
        Scale(self, length=150, orient=HORIZONTAL, sliderlength =25,
              label ='Amplitude :', from_=1, to=9, tickinterval =2,
              showvalue =0, command = self.setAmplitude).pack(side=LEFT)
        
    def setCurve(self):
        self.event_generate('<Control-Z>')

    def setFrequency(self, f):
        self.freq = float(f)
        self.event_generate('<Control-Z>')
                              
    def setPhase(self, p):
        pp =float(p)
        self.phase = pp*2*pi/360        # conversion degrés -> radians
        self.event_generate('<Control-Z>')

    def setAmplitude(self, a):
        self.ampl = float(a)
        self.event_generate('<Control-Z>')

#### Code pour tester la classe : ###
        
if __name__ == '__main__':
    def afficherTout(event=None):
        lab.configure(text = '%s - %s - %s - %s' %
                         (fra.chk.get(), fra.freq, fra.phase, fra.ampl))                
    root = Tk()
    fra = ChoixVibra(root,'navy')
    fra.pack(side =TOP)
    lab = Label(root, text ='test')
    lab.pack()
    root.bind('<Control-Z>', afficherTout)
    root.mainloop()

Ce panneau de contrôle permettra à vos utilisateurs de régler aisément la valeur des paramètres indiqués (fréquence, phase et amplitude), lesquels pourront alors servir à commander l'affichage de graphiques élongation/temps dans un widget de la classe OscilloGraphe() construite précédemment, comme nous le montrerons dans l'application de synthèse.

Commentaires

modifier
  • Ligne 6 : La méthode constructeur utilise un paramètre optionnel coul. Ce paramètre permettra de choisir une couleur pour le graphique soumis au contrôle du widget. Le paramètre boss sert à réceptionner la référence d'une fenêtre maîtresse éventuelle.
  • Ligne 7 : Activation du constructeur de la classe parente (pour hériter sa fonctionnalité).
  • Ligne 9 : Déclaration de quelques variables d'instance. Leurs vraies valeurs seront déterminées par les méthodes des lignes 29 à 40 (gestionnaires d'événements).
  • Ligne 11 : Cette instruction instancie un objet de la classe IntVar(), laquelle fait partie du module Tkinter au même titre que les classes similaires DoubleVar(), StringVar() et BooleanVar(). Toutes ces classes permettent de définir des variables Tkinter, lesquels sont en fait des objets, mais qui se comportent comme des variables à l'intérieur des widgets Tkinter. Ainsi l'objet référencé dans self.chk contient l'équivalent d'une variable de type entier, dans un format utilisable par Tkinter. Pour accéder à sa valeur depuis Python, il faut utiliser des méthodes spécifiques de cette classe d'objets : la méthode set() permet de lui assigner une valeur, et la méthode get() permet de la récupérer (ce que l'on met en pratique à la ligne 47).
  • Ligne 12 : L'option variable de l'objet checkbutton est associée à la variable Tkinter définie à la ligne précédente. (Nous ne pouvons pas référencer directement une variable ordinaire dans la définition d'un widget Tkinter, parce que Tkinter lui-même est écrit dans un langage qui n'utilise pas les mêmes conventions que Python pour formater ses variables. Les objets construits à partir des classes de variables Tkinter sont donc nécessaires pour assurer l'interface).
  • Ligne 13 : L'option command désigne la méthode que le système doit invoquer lorsque l'utilisateur effectue un clic de souris dans la case à cocher.
  • Lignes 14 à 24 : Ces lignes définissent les trois widgets curseurs, en trois instructions similaires. Il serait plus élégant de programmer tout ceci en une seule instruction, répétée trois fois à l'aide d'une boucle. Cela nécessiterait cependant de faire appel à un concept que nous n'avons pas encore expliqué (les fonctions/expressions lamdba), et la définition du gestionnaire d'événements associé à ces widgets deviendrait elle aussi plus complexe. Conservons donc pour cette fois des instructions séparées : nous nous efforcerons d'améliorer tout cela plus tard.
  • Lignes 26 à 40 : Les 4 widgets définis dans les lignes précédentes possèdent chacun une option command. Pour chacun d'eux, la méthode invoquée dans cette option command est différente : la case à cocher active la méthode setCurve(), le premier curseur active la méthode setFrequency(), le second curseur active la méthode setPhase(), et le troisième curseur active la méthode setAmplitude(). Remarquez bien au passage que l'option command des widgets Scale transmet un argument à la méthode associée (la position actuelle du curseur), alors que la même option command ne transmet rien dans le cas du widget Checkbutton. Ces 4 méthodes (qui sont donc les gestionnaires des événements produits par la case à cocher et les trois curseurs) provoquent elles-mêmes chacune l'émission d'un nouvel événement[3], en faisant appel à la méthode event_generate(). Lorsque cette méthode est invoquée, Python envoie au système d'exploitation exactement le même message-événement que celui qui se produirait si l'utilisateur enfonçait simultanément les touches <Ctrl>, <Maj> et <Z> de son clavier. Nous produisons ainsi un message-événement bien particulier, qui peut être détecté et traité par un gestionnaire d'événement associé à un autre widget (voir page suivante). De cette manière, nous mettons en place un véritable système de communication entre widgets : chaque fois que l'utilisateur exerce une action sur notre panneau de contrôle, celui-ci génère un événement spécifique, qui signale cette action à l'attention des autres widgets présents.
     nous aurions pu choisir une autre combinaison de touches (ou même carrément un autre type d'événement). Notre choix s'est porté sur celle-ci parce qu'il y a vraiment très peu de chances que l'utilisateur s'en serve alors qu'il examine notre programme. Nous pourrons cependant produire nous-mêmes un tel événement au clavier à titre de test, lorsque le moment sera venu de vérifier le gestionnaire de cet événement, que nous mettrons en place par ailleurs.
  • Lignes 42 à 54 : Comme nous l'avions déjà fait pour oscillo.py, nous complétons ce nouveau module par quelques lignes de code au niveau principal. Ces lignes permettent de tester le bon fonctionnement de la classe : elles ne s'exécutent que si on lance le module directement, comme une application à part entière. Veillez à utiliser vous-même cette technique dans vos propres modules, car elle constitue une bonne pratique de programmation : l'utilisateur de modules construits ainsi peut en effet (re)découvrir très aisément leur fonctionnalité (en les exécutant) et la manière de s'en servir (en analysant ces quelques lignes de code). Dans ces lignes de test, nous construisons une fenêtre principale root qui contient deux widgets : un widget de la nouvelle classe ChoixVibra() et un widget de la classe Label(). À la ligne 53, nous associons à la fenêtre principale un gestionnaire d'événement : tout événement du type spécifié déclenche désormais un appel de la fonction afficherTout(). Cette fonction est donc notre gestionnaire d'événement spécialisé, qui est sollicité chaque fois qu'un événement de type <Maj-Ctrl-Z> est détecté par le système d'exploitation. Comme nous l'avons déjà expliqué plus haut, nous avons fait en sorte que de tels événements soient produits par les objets de la classe ChoixVibra(), chaque fois que l'utilisateur modifie l'état de l'un ou l'autre des trois curseurs, ou celui de la case à cocher. Conçue seulement pour effectuer un test, la fonction afficherTout() ne fait rien d'autre que provoquer l'affichage des valeurs des variables associées à chacun de nos quatre widgets, en (re)configurant l'option text d'un widget de classe Label().
  • Ligne 47, expression fra.chk.get() : nous avons vu plus haut que la variable mémorisant l'état de la case à cocher est un objet-variable Tkinter. Python ne peut pas lire directement le contenu d'une telle variable, qui est en réalité un objet-interface. Pour en extraire la valeur, il faut donc faire usage d'une méthode spécifique de cette classe d'objets : la méthode get().

Propagation des évènements

modifier

Le mécanisme de communication décrit ci-dessus respecte la hiérarchie de classes des widgets. Vous aurez noté que la méthode qui déclenche l'événement est associée au widget dont nous sommes en train de définir la classe, par l'intermédiaire de self. En général, un message-événement est en effet associé à un widget particulier (par exemple, un clic de souris sur un bouton est associé à ce bouton), ce qui signifie que le système d'exploitation va d'abord examiner s'il existe un gestionnaire pour ce type d'événement, qui soit lui aussi associé à ce widget. S'il en existe un, c'est celui-là qui est activé, et la propagation du message s'arrête. Sinon, le message-événement est « présenté » successivement aux widgets maîtres, dans l'ordre hiérarchique, jusqu'à ce qu'un gestionnaire d'événement soit trouvé, ou bien jusqu'à ce que la fenêtre principale soit atteinte.

Les événements correspondant à des frappes sur le clavier (telle la combinaison de touches <Maj-Ctrl-Z> utilisée dans notre exercice) sont cependant toujours expédiés directement à la fenêtre principale de l'application. Dans notre exemple, le gestionnaire de cet événement doit donc être associé à la fenêtre root.

Exercices

  1. Votre nouveau widget hérite des propriétés de la classe Frame(). Vous pouvez donc modifier son aspect en modifiant les options par défaut de cette classe, à l'aide de la méthode configure(). Essayez par exemple de faire en sorte que le panneau de contrôle soit entouré d'une bordure de 4 pixels ayant l'aspect d'un sillon (bd = 4, relief = GROOVE). Si vous ne comprenez pas bien ce qu'il faut faire, inspirez-vous du script oscillo.py (ligne 10).
  2. Si l'on assigne la valeur 1 à l'option showvalue des widgets Scale(), la position précise du curseur par rapport à l'échelle est affichée en permanence. Activez donc cette fonctionnalité pour le curseur qui contrôle le paramètre phase.
  3. L'option troughcolor des widgets Scale() permet de définir la couleur de leur glissière. Utilisez cette option pour faire en sorte que la couleur des glissières des 3 curseurs soit celle qui est utilisée comme paramètre lors de l'instanciation de votre nouveau widget.
  4. Modifiez le script de telle manière que les widgets curseurs soient écartés davantage les uns des autres (options padx et pady de la méthode pack()).

Solution

  1. Réfléchissez !
  2. Réfléchissez !
  3. Réfléchissez !
  4. Réfléchissez !

Intégration de widgets composites dans une application synthèse

modifier

Dans les exercices précédents, nous avons construit deux nouvelles classes de widgets : le widget OscilloGraphe(), canevas spécialisé pour le dessin de sinusoïdes, et le widget ChoixVibra(), panneau de contrôle à trois curseurs permettant de choisir les paramètres d'une vibration.

Ces widgets sont désormais disponibles dans les modules oscillo.py et curseurs.py[4].

Nous allons à présent les utiliser dans une application synthèse, qui pourrait illustrer votre cours de physique : un widget OscilloGraphe() y affiche un, deux, ou trois graphiques superposés, de couleurs différentes, chacun d'entre eux étant soumis au contrôle d'un widget ChoixVibra() :

 

Le script correspondant est reproduit ci-après.

Nous attirons votre attention sur la technique mise en œuvre pour provoquer un rafraîchissement de l'affichage dans le canevas par l'intermédiaire d'un événement, chaque fois que l'utilisateur effectue une action quelconque au niveau de l'un des panneaux de contrôle.

Rappelez-vous que les applications destinées à fonctionner dans une interface graphique doivent être conçues comme des « programmes pilotés par les événements ».

En préparant cet exemple, nous avons arbitrairement décidé que l'affichage des graphiques serait déclenché par un événement particulier, tout à fait similaire à ceux que génère le système d'exploitation lorsque l'utilisateur accomplit une action quelconque. Dans la gamme (très étendue) d'événements possibles, nous en avons choisi un qui ne risque guère d'être utilisé pour d'autres raisons, pendant que notre application fonctionne : la combinaison de touches <Maj-Ctrl-Z>.

Lorsque nous avons construit la classe de widgets ChoixVibra(), nous y avons donc incorporé les instructions nécessaires pour que de tels événements soient générés, chaque fois que l'utilisateur actionne l'un des curseurs ou modifie l'état de la case à cocher. Nous allons à présent définir le gestionnaire de cet événement et l'inclure dans notre nouvelle classe : nous l'appellerons montreCourbes() et il se chargera de rafraîchir l'affichage. Étant donné que l'événement concerné est du type <enfoncement d'une touche>, nous devrons cependant le détecter au niveau de la fenêtre principale de l'application.

from oscillo import *
from curseurs import *

class ShowVibra(Frame):
    """Démonstration de mouvements vibratoires harmoniques"""
    def __init__(self, boss =None):
        Frame.__init__(self)        # constructeur de la classe parente
        self.couleur = ['dark green', 'red', 'purple']
        self.trace = [0]*3          # liste des tracés (courbes à dessiner)
        self.controle = [0]*3       # liste des panneaux de contrôle

        # Instanciation du canevas avec axes X et Y : 
        self.gra = OscilloGraphe(self, larg =400, haut=200)
        self.gra.configure(bg ='white', bd=2, relief=SOLID)
        self.gra.pack(side =TOP, pady=5)

        # Instanciation de 3 panneaux de contrôle (curseurs) : 
        for i in range(3):
            self.controle[i] = ChoixVibra(self, self.couleur[i])
            self.controle[i].pack()

        # Désignation de l'événement qui déclenche l'affichage des tracés :    
        self.master.bind('<Control-Z>', self.montreCourbes)
        self.master.title('Mouvements vibratoires harmoniques')
        self.pack()
        
    def montreCourbes(self, event):
        """(Ré)Affichage des trois graphiques élongation/temps"""   
        for i in range(3):

            # D'abord, effacer le tracé précédent (éventuel) :
            self.gra.delete(self.trace[i])

            # Ensuite, dessiner le nouveau tracé :  
            if self.controle[i].chk.get():
                self.trace[i] = self.gra.traceCourbe(
                                    coul = self.couleur[i],
                                    freq = self.controle[i].freq,
                                    phase = self.controle[i].phase,
                                    ampl = self.controle[i].ampl)                    
            
#### Code pour tester la classe : ###
        
if __name__ == '__main__':    
    ShowVibra().mainloop()

Commentaires

modifier
  • Lignes 1-2 : Nous pouvons nous passer d'importer le module Tkinter : chacun de ces deux modules s'en charge déjà.
  • Ligne 4 : Puisque nous commençons à connaître les bonnes techniques, nous décidons de construire l'application elle-même sous la forme d'une classe, dérivée de la classe Frame() : ainsi nous pourrons plus tard l'intégrer toute entière dans d'autres projets, si le cœur nous en dit.
  • Lignes 8-10 : Définition de quelques variables d'instance (3 listes) : les trois courbes tracées seront des objets graphiques, dont les couleurs sont pré-définies dans la liste self.couleur ; nous devons préparer également une liste self.trace pour mémoriser les références de ces objets graphiques, et enfin une liste self.controle pour mémoriser les références des trois panneaux de contrôle.
  • Lignes 13 à 15 : Instanciation du widget d'affichage. Étant donné que la classe OscilloGraphe() a été obtenue par dérivation de la classe Canvas(), il est toujours possible de configurer ce widget en redéfinissant les options spécifiques de cette classe (ligne 13).
  • Lignes 18 à 20 : Pour instancier les trois widgets « panneau de contrôle », on utilise une boucle. Leurs références sont mémorisées dans la liste self.controle préparée à la ligne 10. Ces panneaux de contrôle sont instanciés comme esclaves du présent widget, par l'intermédiaire du paramètre self. Un second paramètre leur transmet la couleur du tracé à contrôler.
  • Lignes 23-24 : Au moment de son instanciation, chaque widget Tkinter reçoit automatiquement un attribut master qui contient la référence de la fenêtre principale de l'application. Cet attribut se révèle particulièrement utile si la fenêtre principale a été instanciée implicitement par Tkinter, comme c'est le cas ici. Rappelons en effet que lorsque nous démarrons une application en instanciant directement un widget tel que Frame, par exemple (c'est ce que nous avons fait à la ligne 4), Tkinter instancie automatiquement une fenêtre maîtresse pour ce widget (un objet de la classe Tk()). Comme cet objet a été créé automatiquement, nous ne disposons d'aucune référence dans notre code pour y accéder, si ce n'est par l'intermédiaire de l'attribut master que Tkinter associe automatiquement à chaque widget. Nous nous servons de cette référence pour redéfinir le bandeau-titre de la fenêtre principale (à la ligne 24), et pour y attacher un gestionnaire d'événement (à la ligne 23).
  • Lignes 27 à 40 : La méthode décrite ici est le gestionnaire des événements <Maj-Ctrl-Z> spécifiquement déclenchés par nos widgets ChoixVibra() (ou « panneaux de contrôle »), chaque fois que l'utilisateur exerce une action sur un curseur ou une case à cocher. Dans tous les cas, les graphiques éventuellement présents sont d'abord effacés (ligne 28) à l'aide de la méthode delete() : le widget OscilloGraphe() a hérité cette méthode de sa classe parente Canvas(). Ensuite, de nouvelles courbes sont retracées, pour chacun des panneaux de contrôle dont on a coché la case « Afficher ». Chacun des objets ainsi dessinés dans le canevas possède un numéro de référence, renvoyé par la méthode traceCourbe() de notre widget OscilloGraphe(). Les numéros de référence de nos dessins sont mémorisés dans la liste self.trace. Ils permettent d'effacer individuellement chacun d'entre eux (cfr. instruction de la ligne 28).
  • Lignes 38-40 : Les valeurs de fréquence, phase & amplitude que l'on transmet à la méthode traceCourbe() sont les attributs d'instance correspondants de chacun des trois panneaux de contrôle, eux-mêmes mémorisés dans la liste self.controle. Nous pouvons récupérer ces attributs en utilisant la qualification des noms par points.

Exercices

  1. Modifiez le script, de manière à obtenir l'aspect ci-dessous (écran d'affichage avec grille de référence, panneaux de contrôle entourés d'un sillon) :
     
  2. Modifiez le script, de manière à faire apparaître et contrôler 4 graphiques au lieu de trois. Pour la couleur du quatrième graphique, choisissez par exemple : 'blue', 'navy', 'maroon', ...
  3. Aux lignes 33-35, nous récupérons les valeurs des fréquence, phase & amplitude choisies par l'utilisateur sur chacun des trois panneaux de contrôle, en accédant directement aux attributs d'instance correspondants. Python autorise ce raccourci - et c'est bien pratique – mais cette technique est dangereuse. Elle enfreint l'une des recommandations de la théorie générale de la « programmation orientée objet », qui préconise que l'accès aux propriétés des objets soit toujours pris en charge par des méthodes spécifiques. Pour respecter cette recommandation, ajoutez à la classe ChoixVibra() une méthode supplémentaire que vous appellerez valeurs(), et qui renverra un tuple contenant les valeurs de la fréquence, la phase et l'amplitude choisies. Les lignes 33 à 35 du présent script pourront alors être remplacées par quelque chose comme :
    freq, phase, ampl = self.control[i].valeurs()</li>
    
  4. Écrivez une petite application qui fait apparaître une fenêtre avec un canevas et un widget curseur (Scale). Dans le canevas, dessinez un cercle, dont l'utilisateur pourra faire varier la taille à l'aide du curseur.
  5. Écrivez un script qui créera deux classes : une classe « Application », dérivée de Frame(), dont le constructeur instanciera un canevas de 400x400 pixels, ainsi que deux boutons. Dans le canevas, vous instancierez un objet de la classe « Visage » décrite ci-après. La classe « Visage » servira à définir des objets graphiques censés représenter des visages humains simplifiés. Ces visages seront constitués d'un cercle principal dans lequel trois ovales plus petits représenteront deux yeux et une bouche (ouverte). Une méthode "fermer" permettra de remplacer l'ovale de la bouche par une ligne horizontale. Une méthode « ouvrir » permettra de restituer la bouche de forme ovale. Les deux boutons définis dans la classe « Application » serviront respectivement à fermer et à ouvrir la bouche de l'objet « Visage » installé dans le canevas.
  6. Exercice de synthèse : élaboration d'un dictionnaire de couleurs. But : réaliser un petit programme utilitaire, qui puisse vous aider à construire facilement et rapidement un nouveau dictionnaire de couleurs, lequel permettrait l'accès technique à une couleur quelconque par l'intermédiaire de son nom usuel en français. Contexte : En manipulant divers objets colorés avec Tkinter, vous avez constaté que cette bibliothèque graphique accepte qu'on lui désigne les couleurs les plus fondamentales sous la forme de chaînes de caractères contenant leur nom en anglais : 'red', 'blue', etc. Vous savez cependant qu'un ordinateur ne peut traiter que des informations numérisées. Cela implique que la désignation d'une couleur quelconque doit nécessairement tôt ou tard être encodée sous la forme d'un nombre. Il faut bien entendu adopter pour cela une convention, et celle-ci peut varier d'un système à un autre. L'une de ces conventions, parmi les plus courantes, consiste à représenter une couleur à l'aide de trois octets, qui indiqueront respectivement les intensités des trois composantes rouge, verte et bleue de cette couleur. Cette convention peut être utilisée avec Tkinter pour accéder à n'importe quelle nuance colorée. Vous pouvez en effet lui indiquer la couleur d'un élément graphique quelconque, à l'aide d'une chaîne de 7 caractères telle que '#00FA4E'. Dans cette chaîne, le premier caractère (#) signifie que ce qui suit est une valeur hexadécimale. Les six caractères suivants représentent les 3 valeurs hexadécimales des 3 composantes R, V et B. Pour visualiser concrètement la correspondance entre une couleur quelconque et son code, vous pouvez essayer le petit programme utilitaire tkColorChooser.py (qui se trouve généralement dans le sous-répertoire /lib-tk de votre installation de Python). Étant donné qu'il n'est pas facile pour les humains que nous sommes de mémoriser de tels codes hexadécimaux, Tkinter est également doté d'un dictionnaire de conversion, qui autorise l'utilisation de noms communs pour un certain nombre de couleurs parmi les plus courantes, mais cela ne marche que pour des noms de couleurs exprimés en anglais. Le but du présent exercice est de réaliser un logiciel qui facilitera la construction d'un dictionnaire équivalent en français, lequel pourrait ensuite être incorporé à l'un ou l'autre de vos propres programmes. Une fois construit, ce dictionnaire serait donc de la forme : {'vert':'#00FF00', 'bleu':'#0000FF', ... etc ...}.
    Cahier des charges
    L'application à réaliser sera une application graphique, construite autour d'une classe. Elle comportera une fenêtre avec un certain nombre de champs d'entrée et de boutons, afin que l'utilisateur puisse aisément encoder de nouvelles couleurs en indiquant à chaque fois son nom français dans un champ, et son code hexadécimal dans un autre. Lorsque le dictionnaire contiendra déjà un certain nombre de données, il devra être possible de le tester, c'est-à-dire d'entrer un nom de couleur en français et de retrouver le code hexadécimal correspondant à l'aide d'un bouton (avec affichage éventuel d'une zone colorée). Un bouton provoquera l'enregistrement du dictionnaire dans un fichier texte. Un autre permettra de reconstruire le dictionnaire à partir du fichier.
  7. Le script ci-dessous correspond à une ébauche de projet dessinant des ensembles de dés à jouer disposés à l'écran de plusieurs manières différentes (cette ébauche pourrait être une première étape dans la réalisation d'un logiciel de jeu). L'exercice consistera à analyser ce script et à le compléter. Vous vous placerez ainsi dans la situation d'un programmeur chargé de continuer le travail commencé par quelqu'un d'autre, ou encore dans celle de l'informaticien prié de participer à un travail d'équipe. Commencez par analyser ce script, et ajoutez-y des commentaires, en particulier aux lignes marquées : #*** , afin de montrer que vous comprenez ce que doit faire le programme à ces emplacements :
    from Tkinter import *
    
    class FaceDom:
        def __init__(self, can, val, pos, taille =70):
            self.can =can
            # ***
            x, y, c = pos[0], pos[1], taille/2
            can.create_rectangle(x -c, y-c, x+c, y+c, fill ='ivory', width =2)
            d = taille/3
            # ***
            self.pList =[]
            # ***
            pDispo = [((0,0),), ((-d,d),(d,-d)), ((-d,-d), (0,0), (d,d))]
            disp = pDispo[val -1]
            # ***
            for p in disp:
                self.cercle(x +p[0], y +p[1], 5, 'red')
        
        def cercle(self, x, y, r, coul):
            # ***
            self.pList.append(self.can.create_oval(x-r, y-r, x+r, y+r, fill=coul))
            
        def effacer(self):
            # ***
            for p in self.pList:
                self.can.delete(p)
            
    class Projet(Frame):
        def __init__(self, larg, haut):
            Frame.__init__(self)
            self.larg, self.haut = larg, haut
            self.can = Canvas(self, bg='dark green', width =larg, height =haut)
            self.can.pack(padx =5, pady =5)
            # ***
            bList = [("A", self.boutA), ("B", self.boutB),
                     ("C", self.boutC), ("D", self.boutD),
                     ("Quitter", self.boutQuit)]
            for b in bList:
                Button(self, text =b[0], command =b[1]).pack(side =LEFT)
            self.pack()
        
        def boutA(self):
            self.d3 = FaceDom(self.can, 3, (100,100), 50)
            
        def boutB(self):
            self.d2 = FaceDom(self.can, 2, (200,100), 80)
            
        def boutC(self):
            self.d1 = FaceDom(self.can, 1, (350,100), 110)
            
        def boutD(self):
            # ***
            self.d3.effacer()
    
        def boutQuit(self):
            self.master.destroy()
            
    Projet(500, 300).mainloop()
    

    Modifiez ensuite ce script, afin qu'il corresponde au cahier des charges suivant :

    • Le canevas devra être plus grand : 600 x 600 pixels.
    • Les boutons de commande devront être déplacés à droite et espacés davantage.
    • La taille des points sur une face de dé devra varier proportionnellement à la taille de cette face

    Variante 1 : Ne conservez que les 2 boutons A et B. Chaque utilisation du bouton A fera apparaître 3 nouveaux dés (de même taille, plutôt petits) disposés sur une colonne (verticale), les valeurs de ces dés étant tirées au hasard entre 1 et 6. Chaque nouvelle colonne sera disposée à la droite de la précédente. Si l'un des tirages de 3 dés correspond à 4, 2, 1 (dans n'importe quel ordre), un message « gagné » sera affiché dans la fenêtre (ou dans le canevas). Le bouton B provoquera l'effacement complet (pas seulement les points !) de tous les dés affichés.

    Variante 2 : Ne conservez que les 2 boutons A et B. Le bouton A fera apparaître 5 dés disposés en quinconce (c.à.d. comme les points d'une face de valeur 5). Les valeurs de ces dés seront tirées au hasard entre 1 et 6, mais il ne pourra pas y avoir de doublons. Le bouton B provoquera l'effacement complet (pas seulement les points !) de tous les dés affichés.

    Variante 3 : Ne conservez que les 3 boutons A, B et C. Le bouton A fera apparaître 13 dés de même taille disposés en cercle. Chaque utilisation du bouton B provoquera un changement de valeur du premier dé, puis du deuxième, du troisième, etc. La nouvelle valeur d'un dé sera à chaque fois égale a sa valeur précédente augmentée d'une unité, sauf dans le cas ou la valeur précédente était 6 : dans ce cas la nouvelle valeur est 1, et ainsi de suite. Le bouton C provoquera l'effacement complet (pas seulement les points !) de tous les dés affichés.

    Variante 4 : Ne conservez que les 3 boutons A, B et C. Le bouton A fera apparaître 12 dés de même taille disposés sur deux lignes de 6. Les valeurs des dés de la première ligne seront dans l'ordre 1, 2, 3, 4, 5, 6. Les valeurs des dés de la seconde ligne seront tirées au hasard entre 1 et 6. Chaque utilisation du bouton B provoquera un changement de valeur aléatoire du premier dé de la seconde ligne, tant que cette valeur restera différente de celle du dé correspondant dans la première ligne. Lorsque le 1er dé de la 2e ligne aura acquis la valeur de son correspondant, c'est la valeur du 2e dé de la seconde ligne qui sera changée au hasard, et ainsi de suite, jusqu'à ce que les 6 faces du bas soient identiques à celles du haut. Le bouton C provoquera l'effacement complet (pas seulement les points !) de tous les dés affichés.

Solution

  1. Réfléchissez !
  2. Réfléchissez !
  3. Réfléchissez !
  4. Réfléchissez !
  5. Réfléchissez !
  6.  
    capture d'écran de l'application
    # Dictionnaire de couleurs
    from Tkinter import *
    # Module donnant accès aux boîtes de dialogue standard pour
    # la recherche de fichiers sur disque :
    from tkFileDialog import asksaveasfile, askopenfile
    
    class Application(Frame):
        '''Fenêtre d'application'''
        def __init__(self):
            Frame.__init__(self)
            self.master.title("Création d'un dictionnaire de couleurs")
    
            self.dico ={}       # création du dictionnaire
    
            # Les widgets sont regroupés dans deux cadres (Frames) : 
            frSup =Frame(self)      # cadre supérieur contenant 6 widgets 
            Label(frSup, text ="Nom de la couleur :",
                  width =20).grid(row =1, column =1)
            self.enNom =Entry(frSup, width =25)        # champ d'entrée pour
            self.enNom.grid(row =1, column =2)         # le nom de la couleur
            Button(frSup, text ="Existe déjà ?", width =12,
                   command =self.chercheCoul).grid(row =1, column =3)
            Label(frSup, text ="Code hexa. corresp. :",
                  width =20).grid(row =2, column =1)
            self.enCode =Entry(frSup, width =25)        # champ d'entrée pour
            self.enCode.grid(row =2, column =2)         # le code hexa.
            Button(frSup, text ="Test", width =12,
                   command =self.testeCoul).grid(row =2, column =3)
            frSup.pack(padx =5, pady =5)
            
            frInf =Frame(self)      # cadre inférieur contenant le reste
            self.test = Label(frInf, bg ="white", width =45,    # zone de test
                              height =7, relief = SUNKEN)
            self.test.pack(pady =5)   
            Button(frInf, text ="Ajouter la couleur au dictionnaire",
                   command =self.ajouteCoul).pack()
            Button(frInf, text ="Enregistrer le dictionnaire", width =25,
                   command =self.enregistre).pack(side = LEFT, pady =5)
            Button(frInf, text ="Restaurer le dictionnaire", width =25,
                   command =self.restaure).pack(side =RIGHT, pady =5)
            frInf.pack(padx =5, pady =5)
            self.pack()        
            
        def ajouteCoul(self):
            "ajouter la couleur présente au dictionnaire"
            if self.testeCoul() ==0:        # une couleur a-t-elle été définie ?
                return       
            nom = self.enNom.get()
            if len(nom) >1:                 # refuser les noms trop petits
                self.dico[nom] =self.cHexa
            else:
                self.test.config(text ="%s : nom incorrect" % nom, bg ='white') 
    
        def chercheCoul(self):
            "rechercher une couleur déjà inscrite au dictionnaire"
            nom = self.enNom.get()
            if self.dico.has_key(nom):
                self.test.config(bg =self.dico[nom], text ="")
            else:
                self.test.config(text ="%s : couleur inconnue" % nom, bg ='white') 
        
        def testeCoul(self):
            "vérifier la validité d'un code hexa. - afficher la couleur corresp."
            try:
                self.cHexa =self.enCode.get()
                self.test.config(bg =self.cHexa, text ="")
                return 1
            except:
                self.test.config(text ="Codage de couleur incorrect", bg ='white')
                return 0
    
        def enregistre(self):
            "enregistrer le dictionnaire dans un fichier texte"
            # Cette méthode utilise une boîte de dialogue standard pour la
            # sélection d'un fichier sur disque. Tkinter fournit toute une série
            # de fonctions associées à ces boîtes, dans le module tkFileDialog.
            # La fonction ci-dessous renvoie un objet-fichier ouvert en écriture :
            ofi =asksaveasfile(filetypes=[("Texte",".txt"),("Tous","*")]) 
            for clef, valeur in self.dico.items():
                ofi.write("%s %s\n" % (clef, valeur))
            ofi.close()
    
        def restaure(self):
            "restaurer le dictionnaire à partir d'un fichier de mémorisation"
            # La fonction ci-dessous renvoie un objet-fichier ouvert en lecture :
            ofi =askopenfile(filetypes=[("Texte",".txt"),("Tous","*")]) 
            lignes = ofi.readlines()
            for li in lignes:
                cv = li.split()     # extraction de la clé et la valeur corresp.
                self.dico[cv[0]] = cv[1]
            ofi.close()
    
    if __name__ == '__main__':
        Application().mainloop()
    
  7. (variante 3) :
    from Tkinter import *
    from random import randrange
    from math import sin, cos, pi
    
    class FaceDom:
        def __init__(self, can, val, pos, taille =70):
            self.can =can        
            x, y, c = pos[0], pos[1], taille/2
            self. carre = can.create_rectangle(x -c, y-c, x+c, y+c,
                                               fill ='ivory', width =2)
            d = taille/3         
            # disposition des points sur la face, pour chacun des 6 cas :
            self.pDispo = [((0,0),),
                           ((-d,d),(d,-d)),
                           ((-d,-d), (0,0), (d,d)),
                           ((-d,-d),(-d,d),(d,-d),(d,d)),
                           ((-d,-d),(-d,d),(d,-d),(d,d),(0,0)),
                           ((-d,-d),(-d,d),(d,-d),(d,d),(d,0),(-d,0))]
                        
            self.x, self.y, self.dim = x, y, taille/15
            self.pList =[]      # liste contenant les points de cette face 
            self.tracer_points(val)
            
        def tracer_points(self, val):
            # créer les dessins de points correspondant à la valeur val :
            disp = self.pDispo[val -1]
            for p in disp:
                self.cercle(self.x +p[0], self.y +p[1], self.dim, 'red')
            self.val = val
            
        def cercle(self, x, y, r, coul):
            self.pList.append(self.can.create_oval(x-r, y-r, x+r, y+r, fill=coul))
            
        def effacer(self, flag =0):
            for p in self.pList:
                self.can.delete(p)
            if flag:
                self.can.delete(self.carre)
            
    class Projet(Frame):
        def __init__(self, larg, haut):
            Frame.__init__(self)
            self.larg, self.haut = larg, haut
            self.can = Canvas(self, bg='dark green', width =larg, height =haut)
            self.can.pack(padx =5, pady =5)
            # liste des boutons à installer, avec leur gestionnaire :
            bList = [("A", self.boutA), ("B", self.boutB),
                     ("C", self.boutC), ("Quitter", self.boutQuit)]
            bList.reverse()         # inverser l'ordre de la liste
            for b in bList:
                Button(self, text =b[0], command =b[1]).pack(side =RIGHT, padx=3)
            self.pack()
            self.des =[]            # liste qui contiendra les faces de dés
            self.actu =0            # réf. du dé actuellement sélectionné
            
        def boutA(self):
            if len(self.des):
                return              # car les dessins existent déjà !
            a, da = 0, 2*pi/13
            for i in range(13):
                cx, cy = self.larg/2, self.haut/2
                x = cx + cx*0.75*sin(a)             # pour disposer en cercle,
                y = cy + cy*0.75*cos(a)             # on utilise la trigono !
                self.des.append(FaceDom(self.can, randrange(1,7) , (x,y), 65))
                a += da
    
        def boutB(self):
            # incrémenter la valeur du dé sélectionné. Passer au suivant :
            v = self.des[self.actu].val
            v = v % 6
            v += 1        
            self.des[self.actu].effacer()
            self.des[self.actu].tracer_points(v)
            self.actu += 1
            self.actu = self.actu % 13
    
        def boutC(self):
            for i in range(len(self.des)):
                self.des[i].effacer(1)
            self.des =[]
            self.actu =0
            
        def boutQuit(self):
            self.master.destroy()
            
    Projet(600, 600).mainloop()
    
  1. Comme nous l'avons déjà signalé précédemment, Python vous permet d'accéder aux attributs d'instance en utilisant la qualification des noms par points. D'autres langages de programmation l'interdisent, ou bien ne l'autorisent que moyennant une déclaration particulière de ces attributs (distinction entre attributs privés et publics). Sachez en tous cas que ce n'est pas recommandé : le bon usage de la programmation orientée objet stipule en effet que vous ne devez pouvoir accéder aux attributs des objets que par l'intermédiaire de méthodes spécifiques.
  2. Tkinter autorise également de construire la fenêtre principale d'une application par dérivation d'une classe de widget (le plus souvent, il s'agira d'un widget Frame()). La fenêtre englobant ce widget sera automatiquement ajoutée.
  3. En fait, on devrait plutôt appeler cela un message (qui est lui-même la notification d'un événement). Programmes pilotés par des événements.
  4. Il va de soi que nous pourrions rassembler toutes les classes que nous construisons dans un seul module.



Fichiers

Jusqu'à présent, les programmes que nous avons réalisés ne traitaient qu'un très petit nombre de données. Nous pouvions donc à chaque fois inclure ces données dans le corps du programme lui-même (par exemple dans une liste). Cette façon de procéder devient cependant tout à fait inadéquate lorsque l'on souhaite traiter une quantité d'information plus importante.

Utilité des fichiers

modifier

Imaginons par exemple que nous voulons écrire un petit programme exerciseur qui fasse apparaître à l'écran des questions à choix multiple, avec traitement automatique des réponses de l'utilisateur. Comment allons-nous mémoriser le texte des questions elles-mêmes ?

L'idée la plus simple consiste à placer chacun de ces textes dans une variable, en début de programme, avec des instructions d'affectation du genre :

a = "Quelle est la capitale du Guatémala ?"
b = "Qui a succédé à Henri IV ?"
c = "Combien font 26 × 43 ?"
	etc.

Cette idée est malheureusement beaucoup trop simpliste. Tout va se compliquer en effet lorsque nous essayerons d'élaborer la suite du programme, c'est-à-dire les instructions qui devront servir à sélectionner au hasard l'une ou l'autre de ces questions pour les présenter à l'utilisateur. Employer par exemple une longue suite d'instructions if ... elif ... elif ... comme dans l'exemple ci-dessous n'est certainement pas la bonne solution (ce serait d'ailleurs bien pénible à écrire : n'oubliez pas que nous souhaitons traiter un grand nombre de questions !) :

if choix == 1:
    selection = a
elif choix == 2:
    selection = b
elif choix == 3:
    selection = c
	... etc.

La situation se présente déjà beaucoup mieux si nous faisons appel à une liste :

liste = ["Qui a vaincu Napoléon à Waterloo ?",
         "Comment traduit-on 'informatique' en anglais ?",
         "Quelle est la formule chimique du méthane ?", ... etc ...]

On peut en effet extraire n'importe quel élément de cette liste à l'aide de son indice. Exemple :

print liste[2]        ===>  "Quelle est la formule chimique du méthane ?"

(rappel : l'indiçage commence à partir de zéro)

Même si cette façon de procéder est déjà nettement meilleure que la précédente, nous sommes toujours confrontés à plusieurs problèmes gênants :

  • La lisibilité du programme va se détériorer très vite lorsque le nombre de questions deviendra important. En corollaire, nous accroîtrons la probabilité d'insérer l'une ou l'autre erreur de syntaxe dans la définition de cette longue liste. De telles erreurs seront bien difficiles à débusquer.
  • L'ajout de nouvelles questions, ou la modification de certaines d'entre elles, imposeront à chaque fois de rouvrir le code source du programme. En corollaire, il deviendra malaisé de retravailler ce même code source, puisqu'il comportera de nombreuses lignes de données encombrantes.
  • L'échange de données avec d'autres programmes (peut-être écrits dans d'autres langages) est tout simplement impossible, puisque ces données font partie du programme lui-même.

Cette dernière remarque nous suggère la direction à prendre : il est temps que nous apprenions à séparer les données, et les programmes qui les traitent, dans des fichiers différents.

Pour que cela devienne possible, nous devrons doter nos programmes de divers mécanismes permettant de créer des fichiers, d'y envoyer des données et de les récupérer par après.

Les langages de programmation proposent des jeux d'instructions plus ou moins sophistiqués pour effectuer ces tâches. Lorsque les quantités de données deviennent très importantes, il devient d'ailleurs rapidement nécessaire de structurer les relations entre ces données, et l'on doit alors élaborer des systèmes appelés bases de données relationnelles, dont la gestion peut s'avérer très complexe. Ce sera là l'affaire de logiciels très spécialisés tels que Oracle, IBM DB, Adabas, PostgreSQL, MySQL, etc. Python est parfaitement capable de dialoguer avec ces systèmes, mais nous laisserons cela pour un peu plus tard (voir chapitre Gestion d'une base de données).

Nos ambitions présentes sont plus modestes. Nos données ne se comptent pas encore par centaines de milliers, aussi nous pouvons nous contenter de mécanismes simples pour les enregistrer dans un fichier de taille moyenne, et les en extraire ensuite.

Travailler avec des fichiers

modifier

L'utilisation d'un fichier ressemble beaucoup à l'utilisation d'un livre. Pour utiliser un livre, vous devez d'abord le trouver (à l'aide de son titre), puis l'ouvrir. Lorsque vous avez fini de l'utiliser, vous le refermez. Tant qu'il est ouvert, vous pouvez y lire des informations diverses, et vous pouvez aussi y écrire des annotations, mais généralement vous ne faites pas les deux à la fois. Dans tous les cas, vous pouvez vous situer à l'intérieur du livre, notamment en vous aidant des numéros de pages. Vous lisez la plupart des livres en suivant l'ordre normal des pages, mais vous pouvez aussi décider de consulter n'importe quel paragraphe dans le désordre.

Tout ce que nous venons de dire des livres s'applique aussi aux fichiers informatiques. Un fichier se compose de données enregistrées sur votre disque dur, sur une disquette ou sur un CD-ROM. Vous y accédez grâce à son nom (lequel peut inclure aussi un nom de répertoire). Vous pouvez toujours considérer le contenu d'un fichier comme une suite de caractères, ce qui signifie que vous pouvez traiter ce contenu, ou une partie quelconque de celui-ci, à l'aide des fonctions servant à traiter les chaînes de caractères.

Noms de fichiers - Répertoire courant

modifier

Pour simplifier les explications qui vont suivre, nous indiquerons seulement des noms simples pour les fichiers que nous allons manipuler. Si vous procédez ainsi dans vos exercices, les fichiers en question seront créés et/ou recherchés par Python dans le répertoire courant. Celui-ci est habituellement le répertoire où se trouve le script lui-même, sauf si vous lancez ce script depuis la fenêtre d'un shell IDLE, auquel cas le répertoire courant est défini au lancement de IDLE lui-même (Sous Windows, la définition de ce répertoire fait partie des propriétés de l'icône de lancement).

Si vous travaillez avec IDLE, vous souhaiterez donc certainement forcer Python à changer son répertoire courant, afin que celui-ci corresponde à vos attentes. Pour ce faire, utilisez les commandes suivantes en début de session. (Nous supposons ici que le répertoire visé est le répertoire /home/jules/exercices. Vous pouvez franchement utiliser cette syntaxe (c'est-à-dire des caractères / et non \ en guise de séparateurs : c'est la convention en vigueur dans le monde Unix). Python effectuera automatiquement les conversions nécessaires, suivant que vous travaillez sous MacOS, Linux, ou Windows.[1]

>>> from os import chdir
>>> chdir("/home/jules/exercices")

La première commande importe la fonction chdir() du module os. Le module os contient toute une série de fonctions permettant de dialoguer avec le système d'exploitation (os = operating system), quel que soit celui-ci.

La seconde commande provoque le changement de répertoire (chdir = change directory)

Notes
  • Vous avez également la possibilité d'insérer ces commandes en début de script, ou encore d'indiquer le chemin d'accès complet dans le nom des fichiers que vous manipulez, mais cela risque peut-être d'alourdir l'écriture de vos programmes.
  • Choisissez de préférence des noms de fichiers courts. Évitez dans toute la mesure du possible les caractères accentués, les espaces et les signes typographiques spéciaux.

Les deux formes d'importation

modifier

Les lignes d'instructions que nous venons d'utiliser sont l'occasion d'expliquer un mécanisme intéressant. Vous savez qu'en complément des fonctions intégrées dans le module de base, Python met à votre disposition une très grande quantité de fonctions plus spécialisées, qui sont regroupées dans des modules. Ainsi vous connaissez déjà fort bien le module math et le module Tkinter.

Pour utiliser les fonctions d'un module, il suffit de les importer. Mais cela peut se faire de deux manières différentes, comme nous allons le voir ci-dessous. Chacune des deux méthodes présente des avantages et des inconvénients.

Voici un exemple de la première méthode :

>>>>>> import os
>>> rep_cour = os.getcwd()
>>> print rep_cour
C:\Python22\essais

La première ligne de cet exemple importe l'intégralité du module os, lequel contient de nombreuses fonctions intéressantes pour l'accès au système d'exploitation. La seconde ligne utilise la fonction getcwd() du module os[2]. Comme vous pouvez le constater, la fonction getcwd() renvoie le nom du répertoire courant (getcwd = get current working directory).

Par comparaison, voici un exemple similaire utilisant la seconde méthode d'importation :

>>> from os import getcwd
>>> rep_cour = getcwd() 
>>> print rep_cour
C:\Python22\essais

Dans ce nouvel exemple, nous n'avons importé du module os que la fonction getcwd() seule. Importée de cette manière, la fonction s'intègre à notre propre code comme si nous l'avions écrite nous-mêmes. Dans les lignes où nous l'utilisons, il n'est pas nécessaire de rappeler qu'elle fait partie du module os.

Nous pouvons de la même manière importer plusieurs fonctions du même module :

>>> from math import sqrt, pi, sin, cos
>>> print pi
3.14159265359
>>> print sqrt(5)    # racine carrée de 5
2.2360679775
>>> print sin(pi/6)  # sinus d'un angle de 30°
0.5

Nous pouvons même importer toutes les fonctions d'un module, comme dans :

from Tkinter import *

Cette méthode d'importation présente l'avantage d'alléger l'écriture du code. Elle présente l'inconvénient (surtout dans sa dernière forme, celle qui importe toutes les fonctions d'un module) d'encombrer l'espace de noms courant. Il se pourrait alors que certaines fonctions importées aient le même nom que celui d'une variable définie par vous-même, ou encore le même nom qu'une fonction importée depuis un autre module. (Si cela se produit, l'un des deux noms en conflit n'est évidemment plus accessible).

Dans les programmes d'une certaine importance, qui font appel à un grand nombre de modules d'origines diverses, il sera donc toujours préférable de privilégier plutôt la première méthode, c'est-à-dire celle qui utilise des noms pleinement qualifiés.

On fait généralement exception à cette règle dans le cas particulier du module Tkinter, parce que les fonctions qu'il contient sont très sollicitées (dès lors que l'on décide d'utiliser ce module).

Écriture séquentielle dans un fichier

modifier

Sous Python, l'accès aux fichiers est assuré par l'intermédiaire d'un objet-fichier que l'on crée à l'aide de la fonction interne open(). Après avoir appelé cette fonction, vous pouvez lire et écrire dans le fichier en utilisant les méthodes spécifiques de cet objet-fichier.

L'exemple ci-dessous vous montre comment ouvrir un fichier « en écriture », y enregistrer deux chaînes de caractères, puis le refermer. Notez bien que si le fichier n'existe pas encore, il sera créé automatiquement. Par contre, si le nom utilisé concerne un fichier préexistant qui contient déjà des données, les caractères que vous y enregistrerez viendront s'ajouter à la suite de ceux qui s'y trouvent déjà. Vous pouvez faire tout cet exercice directement à la ligne de commande :

file_path = 'mon_dossier/mon_fichier'
if (os.path.isfile(file_path)):
    obFichier = open(file_path,'a')
    obFichier.write('Bonjour, fichier !')
    obFichier.write("Quel beau temps, aujourd'hui !")
    obFichier.close()
Notes
  • La première ligne crée l'objet-fichier « obFichier », lequel fait référence à un fichier véritable (sur disque ou disquette) dont le nom sera Monfichier. Ne confondez pas le nom de fichier avec le nom de l'objet-fichier qui y donne accès. À la suite de cet exercice, vous pouvez vérifier qu'il s'est bien créé sur votre système (dans le répertoire courant) un fichier dont le nom est Monfichier (et vous pouvez en visualiser le contenu à l'aide d'un éditeur quelconque).
  • La fonction open() attend deux arguments, qui doivent être des chaînes de caractères. Le premier argument est le nom du fichier à ouvrir, et le second est le mode d'ouverture. a indique qu'il faut ouvrir ce fichier en mode « ajout » (append), ce qui signifie que les données à enregistrer doivent être ajoutées à la fin du fichier, à la suite de celles qui s'y trouvent éventuellement déjà. Nous aurions pu utiliser aussi le mode w (pour write), mais lorsqu'on utilise ce mode, Python crée toujours un nouveau fichier (vide), et l'écriture des données commence à partir du début de ce nouveau fichier. S'il existe déjà un fichier de même nom, celui-ci est effacé au préalable.
    Exemple : file = open(u'fichier_à_lire_puis_compléter','r+b')
  • La méthode write() réalise l'écriture proprement dite. Les données à écrire doivent être fournies en argument. Ces données sont enregistrées dans le fichier les unes à la suite des autres (c'est la raison pour laquelle on parle de fichier à accès séquentiel). Chaque nouvel appel de write() continue l'écriture à la suite de ce qui est déjà enregistré.
  • La méthode close() referme le fichier. Celui-ci est désormais disponible pour tout usage.

Lecture séquentielle d'un fichier

modifier

Vous allez maintenant rouvrir le fichier, mais cette fois « en lecture », de manière à pouvoir y relire les informations que vous avez enregistrées dans l'étape précédente :

>>> ofi = open('Monfichier', 'r')
>>> t = ofi.read()
>>> print t
Bonjour, fichier ! Quel beau temps, aujourd'hui !
>>> ofi.close()

Comme on pouvait s'y attendre, la méthode read() lit les données présentes dans le fichier et les transfère dans une variable de type « chaîne » (string) . Si on utilise cette méthode sans argument, la totalité du fichier est transférée.

Notes
  • Le fichier que nous voulons lire s'appelle Monfichier. L'instruction d'ouverture de fichier devra donc nécessairement faire référence à ce nom-là. Si le fichier n'existe pas, nous obtenons un message d'erreur. Exemple :
    >>> ofi = open('Monficier','r')
    IOError: [Errno 2] No such file or directory: 'Monficier'
    
    Par contre, nous ne sommes tenus à aucune obligation concernant le nom à choisir pour l'objet-fichier. C'est un nom de variable quelconque. Ainsi donc, dans notre première instruction, nous avons choisi de créer un objet-fichier ofi, faisant référence au fichier réel Monfichier, lequel est ouvert en lecture (argument r).
  • Les deux chaînes de caractères que nous avions entrées dans le fichier sont à présent accolées en une seule. C'est normal, puisque nous n'avons fourni aucun caractère de séparation lorsque nous les avons enregistrées.
  • La méthode read() peut également être utilisée avec un argument. Celui-ci indiquera combien de caractères doivent être lus, à partir de la position déjà atteinte dans le fichier :
    >>> ofi = open('Monfichier', 'r')
    >>> t = ofi.read(7)
    >>> print t
    Bonjour
    >>> t = ofi.read(15)
    >>> print t
    , fichier !Quel
    


    S'il ne reste pas assez de caractères au fichier pour satisfaire la demande, la lecture s'arrête tout simplement à la fin du fichier :

    >>> t = ofi.read(1000)
    >>> print t
     beau temps, aujourd'hui !
    

    Si la fin du fichier est déjà atteinte, read() renvoie une chaîne vide :

    >>> t = ofi.read()
    >>> print t
    
    
    >>> ofi.close()
    

Exemple avec une boucle

modifier

Il va de soi que les boucles de programmation s'imposent lorsque l'on doit traiter un fichier dont on ne connaît pas nécessairement le contenu à l'avance. L'idée de base consistera à lire ce fichier morceau par morceau, jusqu'à ce que l'on ait atteint la fin du fichier.

La fonction ci-dessous illustre cette idée. Elle copie l'intégralité d'un fichier, quelle que soit sa taille, en transférant des portions de 50 caractères à la fois :

def copieFichier(source, destination):
    "copie intégrale d'un fichier" 
    fs = open(source, 'r')
    fd = open(destination, 'w')
    while 1:
        txt = fs.read(50)
        if txt =="":
            break
        fd.write(txt)
    fs.close()
    fd.close()
    return

Si vous voulez tester cette fonction, vous devez lui fournir deux arguments : le premier est le nom du fichier original, le second est le nom à donner au fichier qui accueillera la copie. Exemple :

copieFichier('Monfichier','Tonfichier')

Fichiers texte

modifier

Un fichier texte est un fichier qui contient des caractères imprimables et des espaces organisés en lignes successives, ces lignes étant séparées les unes des autres par un caractère spécial non-imprimable appelé « marqueur de fin de ligne »[3].

Il est très facile de traiter ce genre de fichiers sous Python. Les instructions suivantes créent un fichier texte de quatre lignes :

>>> f = open("Fichiertexte", "w")
>>> f.write("Ceci est la ligne un\nVoici la ligne deux\n")
>>> f.write("Voici la ligne trois\nVoici la ligne quatre\n")
>>> f.close()

Notez bien le marqueur de fin de ligne \n inséré dans les chaînes de caractères, aux endroits où l'on souhaite séparer les lignes de texte dans l'enregistrement. Sans ce marqueur, les caractères seraient enregistrés les uns à la suite des autres, comme dans les exemples précédents.

Lors des opérations de lecture, les lignes d'un fichier texte peuvent être extraites séparément les unes des autres. La méthode readline(), par exemple, ne lit qu'une seule ligne à la fois (en incluant le caractère de fin de ligne) :

>>> f = open('Fichiertexte','r')
>>> t = f.readline()
>>> print t
Ceci est la ligne un
>>> print f.readline()
Voici la ligne deux


La méthode readlines() transfère toutes les lignes restantes dans une liste de chaînes :

>>> t = f.readlines()
>>> print t
['Voici la ligne trois\012', 'Voici la ligne quatre\012']
>>> f.close()
Remarques
  • La liste apparaît ci-dessus en format brut, avec des apostrophes pour délimiter les chaînes, et les caractères spéciaux sous forme de codes numériques. Vous pourrez bien évidemment parcourir cette liste (à l'aide d'une boucle while, par exemple) pour en extraire les chaînes individuelles.
  • La méthode readlines() permet donc de lire l'intégralité d'un fichier en une instruction seulement. Cela n'est possible toutefois que si le fichier à lire n'est pas trop gros (Puisqu'il est copié intégralement dans une variable, c'est-à-dire dans la mémoire vive de l'ordinateur, il faut que la taille de celle-ci soit suffisante). Si vous devez traiter de gros fichiers, utilisez plutôt la méthode readline() dans une boucle, comme le montrera l'exemple de la page suivante.
  • Notez bien que readline() est une méthode qui renvoie une chaîne de caractères, alors que la méthode readlines() renvoie une liste. À la fin du fichier, readline() renvoie une chaîne vide, tandis que readlines() renvoie une liste vide.

Le script qui suit vous montre comment créer une fonction destinée à effectuer un certain traitement sur un fichier texte. En l'occurrence, il s'agit ici de recopier un fichier texte en omettant toutes les lignes qui commencent par un caractère # :

def filtre(source,destination):
    "recopier un fichier en éliminant les lignes de remarques"
    fs = open(source, 'r')
    fd = open(destination, 'w')
    while 1:
        txt = fs.readline()
        if txt =='':
            break
        if txt[0] != '#':
            fd.write(txt)
    fs.close()
    fd.close()
    return

Pour appeler cette fonction, vous devez utiliser deux arguments : le nom du fichier original, et le nom du fichier destiné à recevoir la copie filtrée. Exemple :

filtre('test.txt', 'test_f.txt')

Enregistrement et restitution de variables diverses

modifier

L'argument de la méthode write() doit être une chaîne de caractères. Avec ce que nous avons appris jusqu'à présent, nous ne pouvons donc enregistrer d'autres types de valeurs qu'en les transformant d'abord en chaînes de caractères.

Nous pouvons réaliser cela à l'aide de la fonction intégrée str() :

>>> x = 52
>>> f.write(str(x))

Si nous enregistrons les valeurs numériques en les transformant d'abord en chaînes de caractères, nous risquons de ne plus pouvoir les re-transformer correctement en valeurs numériques lorsque nous allons relire le fichier. Exemple :

>>> a = 5
>>> b = 2.83
>>> c = 67
>>> f = open('Monfichier', 'w')
>>> f.write(str(a))
>>> f.write(str(b))
>>> f.write(str(c))
>>> f.close()
>>> f = open('Monfichier', 'r')
>>> print(f.read())
52.8367
>>> f.close()

Nous avons enregistré trois valeurs numériques. Mais comment pouvons-nous les distinguer dans la chaîne de caractères résultante, lorsque nous effectuons la lecture du fichier ? C'est impossible ! Rien ne nous indique d'ailleurs qu'il y a là trois valeurs plutôt qu'une seule, ou 2, ou 4,…

Il existe plusieurs solutions à ce genre de problèmes. L'une des meilleures consiste à importer un module Python spécialisé : le module pickle[4]. Voici comment il s'utilise :

>>> import pickle
>>> f = open('Monfichier', 'wb')
>>> pickle.dump(a, f)
>>> pickle.dump(b, f)
>>> pickle.dump(c, f)
>>> f.close()
>>> f = open('Monfichier', 'rb')
>>> t = pickle.load(f)
>>> print(t, type(t))
5 <type 'int'>
>>> t = pickle.load(f)
>>> print(t, type(t))
2.83 <type 'float'>
>>> t = pickle.load(f)
>>> print(t, type(t))
67 <type 'int'>
>>> f.close()

Pour cet exemple, on considère que les variables a, b et c contiennent les mêmes valeurs que dans l'exemple précédent. La fonction dump() du module pickle attend deux arguments : le premier est la variable à enregistrer, le second est l'objet fichier dans lequel on travaille. La fonction pickle.load() effectue le travail inverse, c'est-à-dire la restitution de chaque variable avec son type.

Vous pouvez aisément comprendre ce que font exactement les fonctions du module pickle en effectuant une lecture « classique » du fichier résultant, à l'aide de la méthode read() par exemple.

Lister des fichiers

modifier

La fonction os.listdir() liste les fichiers du dossier courant, ou ceux du dossier en paramètre s'il est mentioné.

Exercices

modifier

Exercices

  1. Écrivez un script qui compte dans un fichier texte quelconque le nombre de lignes contenant des caractères numériques.
  2. Écrivez un script qui compte le nombre de mots contenus dans un fichier texte.
  3. Écrivez un script qui recopie un fichier texte en veillant à ce que chaque ligne commence par une majuscule.
  4. Écrivez un script qui recopie un fichier texte en fusionnant (avec la précédente) les lignes qui ne commencent pas par une majuscule.
  5. Vous disposez d'un fichier contenant des valeurs numériques. Considérez que ces valeurs sont les diamètres d'une série de sphères. Écrivez un script qui utilise les données de ce fichier pour en créer un autre, organisé en lignes de texte qui exprimeront « en clair » les autres caractéristiques de ces sphères (surface de section, surface extérieure et volume), dans des phrases telles que :
    Diam. 46.20 cm Section = 1676.39 cm² Surf. = 6705.54 cm². Vol. = 51632.67 cm³
    Diam. 120.00 cm Section = 11309.73 cm² Surf. = 45238.93 cm². Vol. = 904778.68 cm³
    Diam. 0.03 cm Section = 0.00 cm² Surf. = 0.00 cm². Vol. = 0.00 cm³
    Diam. 13.90 cm Section = 151.75 cm² Surf. = 606.99 cm². Vol. = 1406.19 cm³
    Diam. 88.80 cm Section = 6193.21 cm² Surf. = 24772.84 cm². Vol. = 366638.04 cm³
    etc.
  6. Vous avez à votre disposition un fichier texte dont les lignes représentent des valeurs numériques de type réel, sans exposant (et encodées sous forme de chaînes de caractères).
    Écrivez un script qui recopie ces valeurs dans un autre fichier en les arrondissant de telle sorte que leur partie décimale ne comporte plus qu'un seul chiffre après la virgule, celui-ci ne pouvant être que 0 ou 5 (l'arrondi doit être correct).

Solution

  1. Réfléchissez !
  2. # Comptage du nombre de mots dans un texte
    
    fiSource = raw_input("Nom du fichier à traiter : ")
    fs = open(fiSource, 'r')
    
    n = 0           # variable compteur
    while 1:
        ch = fs.readline()
        if ch == "":
            break
        # conversion de la chaîne lue en une liste de mots :
        li = ch.split()
        # totalisation des mots :
        n = n + len(li)    
    fs.close()
    print("Ce fichier texte contient un total de %s mots" % (n))
    
  3. # Conversion en majuscule du premier caractère de chaque ligne
    
    fiSource = raw_input("Nom du fichier à traiter : ")
    fiDest = raw_input("Nom du fichier destinataire : ")
    fs = open(fiSource, 'r')
    fd = open(fiDest, 'w')
    
    while 1:
        ch = fs.readline()
        if ch == "":
            break
        if ch[0] >= "A" and ch[0] <= "Z":
            # le premier car. est une majuscule. On passe.
            pass
        else:
            # Reconstruction de la chaîne:
            pc = ch[0].upper()      # Premier caractère converti
            rc = ch[1:]             # toute le reste de la chaîne  
            ch = pc + rc            # fusion
            # variante utilisant une méthode encore plus intégrée :
            # ch = ch.capitalize()
        # Transcription :
        fd.write(ch)
    
    fd.close()
    fs.close()
    
  4. # Fusion de lignes pour former des phrases
    
    fiSource = raw_input("Nom du fichier à traiter : ")
    fiDest = raw_input("Nom du fichier destinataire : ")
    fs = open(fiSource, 'r')
    fd = open(fiDest, 'w')
    
    
    # On lit d'abord la première ligne :
    ch1 = fs.readline()
    # On lit ensuite les suivantes, en les fusionnant si nécessaire :
    while 1:
        ch2 = fs.readline()
        if ch2 == "":
            break
        # Si la chaîne lue commence par une majuscule, on transcrit
        # la précédente dans le fichier destinataire, et on la
        # remplace par celle que l'on vient de lire :
        if ch2[0] >= "A" and ch2[0] <= "Z":
            fd.write(ch1)
            ch1 = ch2
        # Sinon, on la fusionne avec la précédente :
        else:
            ch1 = ch1[:-1] + " " + ch2
            # (veiller à enlever de ch1 le caractère de fin de ligne)
            
    fd.write(ch1)        # ne pas oublier de transcrire la dernière !
    fd.close()
    fs.close()
    
  5. # Caractéristiques de sphères :
    # Le fichier de départ est un fichier <texte> dont chaque ligne contient
    # un nombre réel (encodé sous la forme d'une chaîne de caractères)    
    
    from math import pi
    
    def caractSphere(d):
        "renvoie les caractéristiques d'une sphère de diamètre d"
        d = float(d)        # conversion de l'argument (=chaîne) en réel
        r = d/2             # rayon
        ss = pi*r**2        # surface de section
        se = 4*pi*r**2      # surface extérieure
        v = 4./3*pi*r**3    # volume  (! la 1e division doit être réelle !)
        # Le marqueur de conversion %8.2f utilisé ci-dessous formate le nombre
        # affiché de manière à occuper 8 caractères au total, en arrondissant
        # de manière à conserver deux chiffres après la virgule : 
        ch = "Diam. %6.2f cm Section = %8.2f cm² " % (d, ss)
        ch = ch +"Surf. = %8.2f cm². Vol. = %9.2f cm³" % (se, v)
        return ch
    
    fiSource = raw_input("Nom du fichier à traiter : ")
    fiDest = raw_input("Nom du fichier destinataire : ")
    fs = open(fiSource, 'r')
    fd = open(fiDest, 'w')
    while 1:
        diam = fs.readline()
        if diam == "" or diam == "\n":
            break
        fd.write(caractSphere(diam) + "\n")         # enregistrement
    fd.close()
    fs.close()
    
  6. # Mise en forme de données numériques
    # Le fichier traité est un fichier <texte> dont chaque ligne contient un nombre
    # réel (sans exposants et encodé sous la forme d'une chaîne de caractères)    
    
    def arrondir(reel):
        "représentation arrondie à .0 ou .5 d'un nombre réel"
        ent = int(reel)             # partie entière du nombre
        fra = reel - ent            # partie fractionnaire
        if fra < .25 :
            fra = 0
        elif fra < .75 :
            fra = .5
        else:
            fra = 1
        return ent + fra    
    
    fiSource = raw_input("Nom du fichier à traiter : ")
    fiDest = raw_input("Nom du fichier destinataire : ")
    fs = open(fiSource, 'r')
    fd = open(fiDest, 'w')
    while 1:
        ligne = fs.readline()
        if ligne == "" or ligne == "\n":
            break
        n = arrondir(float(ligne))      # conversion en <float>, puis arrondi
        fd.write(str(n) + "\n")         # enregistrement
    
    fd.close()
    fs.close()
    

Exercices

  1. Écrivez un script qui permette de créer et de relire aisément un fichier texte. Votre programme demandera d'abord à l'utilisateur d'entrer le nom du fichier. Ensuite il lui proposera le choix, soit d'enregistrer de nouvelles lignes de texte, soit d'afficher le contenu du fichier.
    L'utilisateur devra pouvoir entrer ses lignes de texte successives en utilisant simplement la touche <Enter> pour les séparer les unes des autres. Pour terminer les entrées, il lui suffira d'entrer une ligne vide (c'est-à-dire utiliser la touche <Enter> seule).
    L'affichage du contenu devra montrer les lignes du fichier séparées les unes des autres de la manière la plus naturelle (les codes de fin de ligne ne doivent pas apparaître).
  2. Considérons que vous avez à votre disposition un fichier texte contenant des phrases de différentes longueurs. Écrivez un script qui recherche et affiche la phrase la plus longue.
  3. Écrivez un script qui génère automatiquement un fichier texte contenant les tables de multiplication de 2 à 30 (chacune d'entre elles incluant 20 termes seulement).
  4. Écrivez un script qui recopie un fichier texte en triplant tous les espaces entre les mots.
  5. Vous avez à votre disposition un fichier texte dont chaque ligne est la représentation d'une valeur numérique de type réel (mais sans exposants). Par exemple :
    14.896
    7894.6
    123.278
    etc.
    Écrivez un script qui recopie ces valeurs dans un autre fichier en les arrondissant en nombres entiers (l'arrondi doit être correct).
  6. Écrivez un script qui compare les contenus de deux fichiers et signale la première différence rencontrée.
  7. À partir de deux fichiers préexistants A et B, construisez un fichier C qui contienne alternativement un élément de A, un élément de B, un élément de A, ... et ainsi de suite jusqu'à atteindre la fin de l'un des deux fichiers originaux. Complétez ensuite C avec les éléments restant sur l'autre.
  8. Écrivez un script qui permette d'encoder un fichier texte dont les lignes contiendront chacune les noms, prénom, adresse, code postal et n° de téléphone de différentes personnes (considérez par exemple qu'il s'agit des membres d'un club)
  9. Écrivez un script qui recopie le fichier utilisé dans l'exercice précédent, en y ajoutant la date de naissance et le sexe des personnes (l'ordinateur devra afficher les lignes une par une, et demander à l'utilisateur d'entrer pour chacune les données complémentaires).
  10. Considérons que vous avez fait les exercices précédents et que vous disposez à présent d'un fichier contenant les coordonnées d'un certain nombre de personnes. Écrivez un script qui permette d'extraire de ce fichier les lignes qui correspondent à un code postal bien déterminé.
  11. Modifiez le script de l'exercice précédent, de manière à retrouver les lignes correspondant à des prénoms dont la première lettre est située entre F et M (inclus) dans l'alphabet.
  12. Écrivez des fonctions qui effectuent le même travail que celles du module "pickle". Ces fonctions doivent permettre l'enregistrement de variables diverses dans un fichier texte, en les accompagnant systématiquement d'informations concernant leur format exact.

Solution

  1. #(éditeur simple, pour lire et écrire dans un fichier 'texte') :
    def sansDC(ch):
        "cette fonction renvoie la chaîne ch amputée de son dernier caractère"
        nouv = ""
        i, j = 0, len(ch) -1        
        while i < j:
            nouv = nouv + ch[i]
            i = i + 1
        return nouv    
    
    def ecrireDansFichier():
        of = open(nomF, 'a')
        while 1:
            ligne = raw_input("entrez une ligne de texte (ou <Enter>) : ")
            if ligne == '':
                break
            else:
                of.write(ligne + '\n')
        of.close()
    
    def lireDansFichier():
        of = open(nomF, 'r')
        while 1:
            ligne = of.readline()
            if ligne == "":
                break
            # afficher en omettant le dernier caractère (= fin de ligne) :
            print sansDC(ligne)
        of.close()        
        
    nomF = raw_input('Nom du fichier à traiter : ')
    choix = raw_input('Entrez "e" pour écrire, "c" pour consulter les données : ')
    
    if choix =='e':    
        ecrireDansFichier()
    else:
        lireDansFichier()
    
  2. Réfléchissez !
  3. #(génération des tables de multiplication de 2 à 30) :
    def tableMulti(n):
        # Fonction générant la table de multiplication par n (20 termes)
        # La table sera renvoyée sous forme d'une chaîne de caractères :
        i, ch = 0, ""
        while i < 20:        
            i = i + 1
            ch = ch + str(i * n) + " "
        return ch
    
    NomF = raw_input("Nom du fichier à créer : ")
    fichier = open(NomF, 'w')
    
    # Génération des tables de 2 à 30 :
    table = 2
    while table < 31:
        fichier.write(tableMulti(table) + '\n')
        table = table + 1
    fichier.close()
    Exercice 9.4 :
    # Triplement des espaces dans un fichier texte.
    # Ce script montre également comment modifier le contenu d'un fichier
    # en le transférant d'abord tout entier dans une liste, puis en
    # réenregistrant celle-ci après modifications
    
    def triplerEspaces(ch):
        "fonction qui triple les espaces entre mots dans la chaîne ch"
        i, nouv = 0, ""
        while i < len(ch):
            if ch[i] == " ":
                nouv = nouv + "   "
            else:
                nouv = nouv + ch[i]
            i = i +1    
        return nouv
    
    NomF = raw_input("Nom du fichier : ")
    fichier = open(NomF, 'r+')              # 'r+' = mode read/write
    lignes = fichier.readlines()            # lire toutes les lignes
    
    n=0
    while n < len(lignes):
        lignes[n] = triplerEspaces(lignes[n])
        n =n+1
        
    fichier.seek(0)                         # retour au début du fichier
    fichier.writelines(lignes)              # réenregistrement
    fichier.close()
    
  4. Réfléchissez !
  5. # Mise en forme de données numériques.
    # Le fichier traité est un fichier texte dont chaque ligne contient un nombre
    # réel (sans exposants et encodé sous la forme d'une chaîne de caractères)    
    
    def valArrondie(ch):
        "représentation arrondie du nombre présenté dans la chaîne ch"
        f = float(ch)       # conversion de la chaîne en un nombre réel
        e = int(f + .5)     # conversion en entier (On ajoute d'abord
                            # 0.5 au réel pour l'arrondir correctement)
        return str(e)       # reconversion en chaîne de caractères
         
    fiSource = raw_input("Nom du fichier à traiter : ")
    fiDest = raw_input("Nom du fichier destinataire : ")
    fs = open(fiSource, 'r')
    fd = open(fiDest, 'w')
    
    while 1:
        ligne = fs.readline()       # lecture d'une ligne du fichier
        if ligne == "" or ligne == "\n":
            break
        ligne = valArrondie(ligne)
        fd.write(ligne +"\n")
        
    fd.close()
    fs.close()
    Exercice 9.6 :
    # Comparaison de deux fichiers, caractère par caractère :
    
    fich1 = raw_input("Nom du premier fichier : ")
    fich2 = raw_input("Nom du second fichier : ")
    fi1 = open(fich1, 'r')
    fi2 = open(fich2, 'r')
    
    c, f = 0, 0                 # compteur de caractères et "drapeau" 
    while 1:
        c = c + 1
        car1 = fi1.read(1)      # lecture d'un caractère dans chacun
        car2 = fi2.read(1)      # des deux fichiers
        if car1 =="" or car2 =="":
            break
        if car1 != car2 :
            f = 1
            break               # différence trouvée
    
    fi1.close()
    fi2.close()
    
    print "Ces 2 fichiers",
    if f ==1:
        print "diffèrent à partir du caractère n°", c
    else:
        print "sont identiques."
    
  6. Réfléchissez !
  7. # Combinaison de deux fichiers texte pour en faire un nouveau
    
    fichA = raw_input("Nom du premier fichier : ")
    fichB = raw_input("Nom du second fichier : ")
    fichC = raw_input("Nom du fichier destinataire : ")
    fiA = open(fichA, 'r')
    fiB = open(fichB, 'r')
    fiC = open(fichC, 'w')
    
    while 1:
        ligneA = fiA.readline()    
        ligneB = fiB.readline()
        if ligneA =="" and ligneB =="":
            break               # On est arrivé à la fin des 2 fichiers
        if ligneA != "":
            fiC.write(ligneA)
        if ligneB != "":    
            fiC.write(ligneB)
    
    fiA.close()
    fiB.close()
    fiC.close()
    
  8. # Enregistrer les coordonnées des membres d'un club
    
    def encodage():
        "renvoie la liste des valeurs entrées, ou une liste vide"
        print "*** Veuillez entrer les données (ou <Enter> pour terminer) :"
        while 1:
            nom = raw_input("Nom : ")
            if nom == "":
                return []
            prenom = raw_input("Prénom : ")
            rueNum = raw_input("Adresse (N° et rue) : ")
            cPost = raw_input("Code postal : ")
            local = raw_input("Localité : ")
            tel = raw_input("N° de téléphone : ")
            print nom, prenom, rueNum, cPost, local, tel
            ver = raw_input("Entrez <Enter> si c'est correct, sinon <n> ")
            if ver == "":
                break
        return [nom, prenom, rueNum, cPost, local, tel]
    
    def enregistrer(liste):
        "enregistre les données de la liste en les séparant par des <#>"
        i = 0
        while i < len(liste):
            of.write(liste[i] + "#")
            i = i + 1
        of.write("\n")              # caractère de fin de ligne    
        
    nomF = raw_input('Nom du fichier destinataire : ')
    of = open(nomF, 'a')
    while 1:
        tt = encodage()
        if tt == []:
            break
        enregistrer(tt)
    
    of.close()
    
  9. # Ajouter des informations dans le fichier du club
    
    def traduire(ch):
        "convertir une ligne du fichier source en liste de données"
        dn = ""                 # chaîne temporaire pour extraire les données  
        tt = []                 # la liste à produire
        i = 0
        while i < len(ch):
            if ch[i] == "#":
                tt.append(dn)   # on ajoute la donnée à la liste, et   
                dn =""          # on réinitialise la chaine temporaire
            else:    
                dn = dn + ch[i]
            i = i + 1
        return tt    
        
    def encodage(tt):
        "renvoyer la liste tt, complétée avec la date de naissance et le sexe"
        print "*** Veuillez entrer les données (ou <Enter> pour terminer) :"
        # Affichage des données déjà présentes dans la liste :
        i = 0
        while i < len(tt):
            print tt[i],
            i = i +1
        print
        while 1:
            daNai = raw_input("Date de naissance : ")
            sexe = raw_input("Sexe (m ou f) : ")
            print daNai, sexe
            ver = raw_input("Entrez <Enter> si c'est correct, sinon <n> ")
            if ver == "":
                break
        tt.append(daNai)
        tt.append(sexe)
        return tt
    
    def enregistrer(tt):
        "enregistrer les données de la liste tt en les séparant par des <#>"
        i = 0
        while i < len(tt):
            fd.write(tt[i] + "#")
            i = i + 1
        fd.write("\n")          # caractère de fin de ligne
    
    fSource = raw_input('Nom du fichier source : ')
    fDest = raw_input('Nom du fichier destinataire : ')
    fs = open(fSource, 'r')
    fd = open(fDest, 'w')
    while 1:
        ligne = fs.readline()           # lire une ligne du fichier source
        if ligne =="" or ligne =="\n":
            break
        liste = traduire(ligne)         # la convertir en une liste
        liste = encodage(liste)         # y ajouter les données supplémentaires
        enregistrer(liste)              # sauvegarder dans fichier dest.
    
    fd.close()
    fs.close()
    
  10. # Recherche de lignes particulières dans un fichier texte :
    
    def chercheCP(ch):
        "recherche dans ch la portion de chaîne contenant le code postal"
        i, f, ns = 0, 0, 0          # ns est un compteur de codes #
        cc = ""                     # chaîne à construire 
        while i < len(ch):
            if ch[i] =="#":
                ns = ns +1
                if ns ==3:          # le CP se trouve après le 3e code #
                    f = 1           # variable "drapeau" (flag)
                elif ns ==4:        # inutile de lire après le 4e code #
                    break
            elif f ==1:             # le caractère lu fait partie du
                cc = cc + ch[i]     # CP recherché -> on mémorise
            i = i +1
        return cc    
            
    nomF = raw_input("Nom du fichier à traiter : ")
    codeP = raw_input("Code postal à rechercher : ")
    fi = open(nomF, 'r')
    while 1:
        ligne = fi.readline()
        if ligne =="":
            break
        if chercheCP(ligne) == codeP:
            print ligne
    fi.close()
    
  11. Réfléchissez !
  12. Réfléchissez !

Exercices

  1. Complétez l'exercice (mini-système de base de données) en lui ajoutant deux fonctions : l'une pour enregistrer le dictionnaire résultant dans un fichier texte, et l'autre pour reconstituer ce dictionnaire à partir du fichier correspondant. Chaque ligne de votre fichier texte correspondra à un élément du dictionnaire. Elle sera formatée de manière à bien séparer :
    • la clé et la valeur (c'est-à-dire le nom de la personne, d'une part, et l'ensemble : « âge + taille », d'autre part.
    • dans l'ensemble « âge + taille », ces deux données numériques.
    Vous utiliserez donc deux caractères séparateurs différents, par exemple « @ » pour séparer la clé et la valeur, et « # » pour séparer les données constituant cette valeur :
    Juliette@18#1.67
    Jean-Pierre@17#1.78
    Delphine@19#1.71
    Anne-Marie@17#1.63
    
    etc.
    
  2. Améliorez encore le script de l'exercice précédent, en utilisant un dictionnaire pour diriger le flux d'exécution du programme au niveau du menu principal. Votre programme affichera par exemple :
    Choisissez :
    (R)écupérer un dictionnaire préexistant sauvegardé dans un fichier
    (A)jouter des données au dictionnaire courant
    (C)onsulter le dictionnaire courant
    (S)auvegarder le dictionnaire courant dans un fichier
    (T)erminer :
    
    Suivant le choix opéré par l'utilisateur, vous effectuerez alors l'appel de la fonction correspondante en la sélectionnant dans un dictionnaire de fonctions.

Solution

  1. Sauvegarde d'un dictionnaire :
    def enregistrement():
        fich = raw_input("Entrez le nom du fichier de sauvegarde : ")
        ofi = open(fich, "w")
        # parcours du dictionnaire entier, converti au préalable en une liste :
        for cle, valeur in dico.items(): 
            # utilisation du formatage des chaînes pour créer l'enregistrement :
            ofi.write("%s@%s#%s\n" % (cle, valeur[0], valeur[1]))
        ofi.close()
    
    def lectureFichier():
        fich = raw_input("Entrez le nom du fichier de sauvegarde : ")
        try:
            ofi = open(fich, "r")
        except:
            print "*** fichier inexistant ***"
            return
    
        while 1:
            ligne = ofi.readline()
            if ligne =='':              # détection de la fin de fichier
                break
            enreg = ligne.split("@")    # restitution d'une liste [clé,valeur]
            cle = enreg[0]
            valeur = enreg[1][:-1]      # élimination du caractère de fin de ligne
            data = valeur.split("#")    # restitution d'une liste [âge, taille]
            age, taille = int(data[0]), float(data[1])
            dico[cle] = (age, taille)   # reconstitution du dictionnaire
        ofi.close()
    

    Ces deux fonctions peuvent être appelées respectivement à la fin et au début du programme principal, comme dans l'exemple ci-dessous :

    dico ={}
    lectureFichier()        
    while 1:
        choix = raw_input("Choisissez : (R)emplir - (C)onsulter - (T)erminer : ")
        if choix.upper() == 'T':
            break
        elif choix.upper() == 'R':
            remplissage()
        elif choix.upper() == 'C':
            consultation()
    enregistrement()
    
  2. Cet exercice complète le précédent. On ajoute encore deux petites fonctions, et on réécrit le corps principal du programme pour diriger le flux d'exécution en se servant d'un dictionnaire :
    # Contrôle du flux d'exécution à l'aide d'un dictionnaire
    def sortie():
        print "*** Job terminé ***"
        return 1                        # afin de provoquer la sortie de la boucle 
        
    def autre():
        print "Veuillez frapper R, A, C, S ou T, svp."
        
    
    dico ={}
    fonc ={"R":lectureFichier, "A":remplissage, "C":consultation,
           "S":enregistrement, "T":sortie}
    while 1:
        choix = raw_input("Choisissez :\n" +\
        "(R)écupérer un dictionnaire préexistant sauvegardé dans un fichier\n" +\
        "(A)jouter des données au dictionnaire courant\n" +\
        "(C)onsulter le dictionnaire courant\n" +\
        "(S)auvegarder le dictionnaire courant dans un fichier\n" +\
        "(T)erminer : ")
        # l'instruction ci-dessous appelle une fonction différente pour
        # chaque choix, par l'intermédiaire du dictionnaire <fonc> :
        if fonc.get(choix, autre)():
            break
        # Rem : toutes les fonctions appelées ici renvoient <None> par défaut,
        #       sauf la fonction sortie() qui renvoie 1 => sortie de la boucle
    

Lister des fichiers et dossiers

modifier
    items = os.walk(folder)
    for root, directories, files in items:
        for directory in directories:
            print(directory)
        for file in files:
            print(file)
    return

Créer un dossier s'il n'existe pas

modifier
file_path = 'subfolder/test.txt'
parent_folder = os.path.dirname(file_path)

if not os.path.exists(parent_folder):
    os.mkdir(parent_folder)

file_object = codecs.open(fichier, 'a', 'utf-8')

Supprimer des fichiers et dossiers

modifier

Pour un fichier :

file_path = 'subfolder/test.txt'
os.unlink(file_path)

Pour un dossier vide :

os.rmdir(dir_path)

Pour un dossier non vide :

import shutil
shutil.rmtree(dir_path)

Références

modifier
  1. Dans le cas de Windows, vous pouvez également inclure dans ce chemin la lettre qui désigne le périphérique de stockage où se trouve le fichier. Par exemple : D:/home/jules/exercices.
  2. Le point séparateur exprime donc ici une relation d'appartenance. Il s'agit d'un exemple de la qualification des noms qui sera de plus en plus largement exploitée dans la suite de ce cours. Relier ainsi des noms à l'aide de points est une manière de désigner sans ambiguïté des éléments faisant partie d'ensembles, lesquels peuvent eux-mêmes faire partie d'ensembles plus vastes, etc. Par exemple, l'étiquette systeme.machin.truc désigne l'élément truc, qui fait partie de l'ensemble machin, lequel fait lui-même partie de l'ensemble systeme. Nous verrons de nombreux exemples de cette technique de désignation, notamment lors de notre étude des classes d'objets.
  3. Suivant le système d'exploitation utilisé, le codage correspondant au marqueur de fin de ligne peut être différent. Sous Windows, par exemple, il s'agit d'une séquence de deux caractères (Retour chariot et Saut de ligne), alors que dans les systèmes de type Unix (comme Linux) il s'agit d'un seul saut de ligne, MacOS pour sa part utilisant un seul retour chariot. En principe, vous n'avez pas à vous préoccuper de ces différences. Lors des opérations d'écriture, Python utilise la convention en vigueur sur votre système d'exploitation. Pour la lecture, Python interprète correctement chacune des trois conventions (qui sont donc considérées comme équivalentes).
  4. En anglais, le terme pickle signifie "conserver". Le module a été nommé ainsi parce qu'il sert effectivement à enregistrer des données en conservant leur type.

Sources

modifier



Exceptions

Principe

modifier

Une exception est un signal d'erreur associé à un type de données donnant des informations sur l'erreur. En Python, il s'agit d'un objet.

La capture des exceptions permet l'exécution d'instructions par un interpréteur ou par le processeur (langage compilé) lorsqu'une erreur est détectée au cours de l'exécution d'un programme. En règle générale, lorsque l'erreur n'est pas capturée, l'exécution du programme est alors interrompue, et un message d'erreur plus ou moins explicite est affiché.

Exemple en Python 2 :

>>> print 55/0
ZeroDivisionError: integer division or modulo

Exemple en Python 3 :

>>> print(55/0)
ZeroDivisionError: division by zero
 D'autres informations complémentaires sont affichées, qui indiquent notamment à quel endroit du script l'erreur a été détectée, mais nous ne les reproduisons pas ici.

Le message d'erreur proprement dit comporte deux parties séparées par un double point : d'abord le type d'erreur, et ensuite une information spécifique de cette erreur.

Dans de nombreux cas, il est possible de prévoir à l'avance certaines des erreurs qui risquent de se produire à tel ou tel endroit du programme, et d'inclure à cet endroit des instructions particulières, qui seront activées seulement si ces erreurs se produisent. Dans les langages de niveau élevé comme Python, il est également possible d'associer un mécanisme de surveillance à tout un ensemble d'instructions, et donc de simplifier le traitement des erreurs qui peuvent se produire dans n'importe laquelle de ces instructions.

Syntaxe

modifier

Un mécanisme de ce type s'appelle en général mécanisme de traitement des exceptions. Celui de Python utilise l'ensemble d'instructions try - except – else, qui permettent d'intercepter une erreur et d'exécuter une portion de script spécifique de cette erreur. Il fonctionne comme suit :

Le bloc d'instructions qui suit directement une instruction try est exécuté par Python sous réserve. Si une erreur survient pendant l'exécution de l'une de ces instructions, alors Python annule cette instruction fautive et exécute à sa place le code inclus dans le bloc qui suit l'instruction except. Si aucune erreur ne s'est produite dans les instructions qui suivent try, alors c'est le bloc qui suit l'instruction else qui est exécuté (si cette instruction est présente). Dans tous les cas, l'exécution du programme peut se poursuivre ensuite avec les instructions ultérieures.

Considérons par exemple un script qui demande à l'utilisateur d'entrer un nom de fichier, lequel fichier étant destiné à être ouvert en lecture. Si le fichier n'existe pas, nous ne voulons pas que le programme se « plante ». Nous voulons qu'un avertissement soit affiché, et éventuellement que l'utilisateur puisse essayer d'entrer un autre nom.


try:
	# Code pouvant générer une exception
except MonException:
	# Code en cas d'exception
else:
	# Code en cas de non exception
finally:
	#code dans tous les cas
Exemple : Exemple de gestion d'exception


Exemple

modifier

Exemple de gestion d'exception : intercepter une division par zéro.

a = int(input("Veuillez saisir le numérateur : "))
b = int(input("Veuillez saisir le dénominateur : "))

try:
    c = a / b
    print(" Le résultat est :" + c)
except ZeroDivisionError as e:
    print(e.status_code)
    print(str(e))
    print(" Pas de division par zéro SVP")
finally:
    print("Fin du script")

Autre exemple avec un fichier :

filename = raw_input("Veuillez entrer un nom de fichier : ")
try:
    f = open(filename, "r")
except:
    print "Le fichier", filename, "est introuvable"

 

C'est une mauvaise pratique de ne pas préciser d’exception pour tout attraper comme ci-dessus. Il faut plutôt utiliser la classe mère de toutes les exceptions : BaseException

Si nous estimons que ce genre de test est susceptible de rendre service à plusieurs endroits d'un programme, nous pouvons aussi l'inclure dans une fonction :

def existe(fname):
    try:
        f = open(fname,'r')
        f.close()
        return 1
    except:
        return 0

filename = raw_input("Veuillez entrer le nom du fichier : ")
if existe(filename):
    print "Ce fichier existe bel et bien."
else:
    print "Le fichier", filename, "est introuvable."

Il est également possible de faire suivre l'instruction "try" de plusieurs blocs "except", chacun d'entre eux traitant un type d'erreur spécifique.

Pour attraper plusieurs exceptions :

try:
    from lib import *
except (ImportError, ModuleNotFoundError) as e:
    from src.lib import *

Lancer une exception

modifier

Un programme Python peut lancer une exception lorsqu'il détecte une erreur, en utilisant l'instruction raise :

def construire_table(taille):
    if (taille < 1):
        raise Exception("La taille spécifiée est invalide.")
    # On peut continuer car taille est supérieur ou égal à 1 ...

Testé dans l'interpréteur interactif de Python :

>>> construire_table(1)
>>> construire_table(2)
>>> construire_table(0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in construire_table
Exception: La taille spécifiée est invalide.

Créer une exception

modifier

Il est possible de définir un nouveau type d'exception pour un type d'erreur spécifique. Pour cela, il faut créer une nouvelle classe dérivant de la classe BaseException ou d'une de ses sous-classes (Exception par exemple).

class InvalidSizeException(Exception):
    def __init__(self, taille):
        Exception.__init__(self, f"Taille invalide : {taille}")

def construire_table(taille):
    if (taille < 1):
        raise InvalidSizeException(taille)
    # On peut continuer car taille est supérieur ou égal à 1 ...

Testé dans l'interpréteur interactif de Python :

>>> construire_table(2)
>>> construire_table(1)
>>> construire_table(0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in construire_table
__main__.InvalidSizeException: Taille invalide : 0
>>> construire_table(-1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in construire_table
__main__.InvalidSizeException: Taille invalide : -1


Bases de données

Python contient des modules pour bases de données, comme MySQL, PostgreSQL, SQLite, IBM Informix Dynamic Server et Berkeley DB.

Par exemple pour installer le premier :

  1. Sur Unix, taper : apt-get install python-mysqldb.
  2. Sur Windows, télécharger en cliquant ici ou .

L'exemple ci-dessous permet d'exécuter une requête SQL de sélection :

1  import MySQLdb
2  db = MySQLdb.connect("machine", "dbuser", "password", "dbname")
3  cursor = db.cursor()
4  query = """SELECT * FROM sampletable"""
5  lines = cursor.execute(query)
6  data = cursor.fetchall()
7  db.close()
  1. On ouvre l'accès aux fonctions du Module MySQLdb.
  2. On tente d'établir une connexion à la base de données nommée (si cela ne fonctionne pas, essayer de pinger le nom de machine mentionné pour diagnostiquer un problème réseau).
  3. La ligne trois définit l'objet "cursor" qui va servir d'interface avec la base de données.
  4. On prépare ensuite la commande en langage SQL (ce qui pourrait aussi être défini en tout début du programme).
  5. On exécute cette requête dans la base.
  6. On formate les données brutes du résultat
  7. On clos la connexion.

Remarque : quand il y a beaucoup de lignes, il est préférable d'utiliser row = cursor.fetchone() pour une meilleure visibilité :

1  import MySQLdb
2  db = MySQLdb.connect("machine", "dbuser", "password", "dbname")
3  cursor = db.cursor()
4  query = """SELECT * FROM sampletable"""
5  lines = cursor.execute(query)
6  while True:
7    row = cursor.fetchone()
8    if row == None: break
9  db.close()

Le résultat du fetchone() est de type tuple.

Par ailleurs, la connexion à la base (en ligne 2) peut être enregistrée dans un fichier de configuration, celle-ci devient alors :

import MySQLdb
db = MySQLdb.connect(read_default_file="~/.my.cnf")
...

Postgres

modifier
import psycopg2
conn = psycopg2.connect("dbname=test")
cursor = conn.cursor()
cursor.execute("select * from test");
for i in cursor.next():
    print i
conn.close()

Installation de Gadfly

modifier

Gadfly est un système de bases de données écrit en Python.

Depuis le site http://sourceforge.net/projects/gadfly, télécharger le paquetage gadfly-1.0.0.tar.gz. Il s'agit d'un fichier archive comprimé. Copiez ce fichier dans un répertoire temporaire.

Sous Windows

modifier

Dans un répertoire temporaire quelconque, décomprimez le fichier archive à l'aide d'un logiciel tel que Winzip.

Ouvrez une fenêtre DOS et entrez dans le sous-répertoire qui s'est créé automatiquement.

Lancez la commande : python setup.py install

C'est tout. Vous pouvez éventuellement améliorer les performances, en ajoutant l'opération suivante :

Dans le sous-répertoire qui s'est créé, ouvrez le sous-répertoire kjbuckets, puis le sous-répertoire qui correspond à votre version de Python. Recopiez le fichier *.pyd qui s'y trouve dans le répertoire racine de votre installation de Python.

Lorsque tout est terminé, effacez le contenu de votre répertoire temporaire.

Sous Linux

modifier

En tant qu'administrateur (root), choisissez un répertoire temporaire quelconque et décomprimez-y le fichier archive à l'aide de l'utilitaire tar, qui fait certainement partie de votre distribution. Entrez simplement la commande : tar -xvzf gadfly-1.0.0.tar.gz

Entrez dans le sous-répertoire qui s'est créé automatiquement : cd gadfly-1.0.0

Lancez la commande : python setup.py install

C'est tout. Si votre système Linux comporte un compilateur C, vous pouvez améliorer les performances de Gadfly en recompilant la bibliothèque kjbuckets .

Pour ce faire, entrez encore les deux commandes suivantes :

  • cd kjbuckets
  • python setup.py install

Lorsque tout est terminé, effacez tout le contenu du répertoire temporaire.

Liens externes

modifier



Gestion d'une base de données

Les bases de données sont des outils de plus en plus fréquemment utilisés. Elles permettent de stocker des données nombreuses dans un seul ensemble bien structuré. Lorsqu'il s'agit de bases de données relationnelles, il devient en outre tout à fait possible d'éviter l'« enfer des doublons ». Vous avez sûrement été déjà confrontés à ce problème :

Des données identiques ont été enregistrées dans plusieurs fichiers différents. Lorsque vous souhaitez modifier ou supprimer l'une de ces données, vous devez ouvrir et modifier tous les fichiers qui la contiennent ! Le risque d'erreur est très réel, qui conduit inévitablement à des incohérences, sans compter la perte de temps que cela représente.

Les bases de données constituent la solution à ce type de problème. Python vous permet d'en utiliser de nombreux systèmes, mais nous n'en examinerons que deux dans nos exemples : Gadfly et MySQL.

Les bases de données

modifier

Il existe de nombreux types de bases de données. On peut par exemple déjà considérer comme une base de données élémentaire, un fichier qui contient une liste de noms et d'adresses.

Si la liste n'est pas trop longue, et si l'on ne souhaite pas pouvoir y effectuer des recherches en fonction de critères complexes, il va de soi que l'on peut accéder à ce type de données en utilisant des instructions simples, telles celles que nous avons abordées dans Programmation Python/Bases de données.

La situation se complique cependant très vite si l'on souhaite pouvoir effectuer des sélections et des tris parmi les données, surtout si celles-ci deviennent très nombreuses. La difficulté augmente encore si les données sont répertoriées dans différents ensembles reliés par un certain nombre de relations hiérarchiques, et si plusieurs utilisateurs doivent pouvoir y accéder en parallèle.

Imaginez par exemple que la direction de votre école vous confie la charge de mettre au point un système de bulletins informatisé. En y réfléchissant quelque peu, vous vous rendrez compte rapidement que cela suppose la mise en œuvre de toute une série de tables différentes : une table des noms d'élèves (laquelle pourra bien entendu contenir aussi d'autres informations spécifiques à ces élèves : adresse, date de naissance, etc.) ; une table contenant la liste des cours (avec le nom du professeur titulaire, le nombre d'heures enseignées par semaine, etc.) ; une table mémorisant les travaux pris en compte pour l'évaluation (avec leur importance, leur date, leur contenu, etc.) ; une table décrivant la manière dont les élèves sont groupés par classes ou par options, les cours suivis par chacun, etc., etc.

Vous comprenez bien que ces différentes tables ne sont pas indépendantes. Les travaux effectués par un même élève sont liés à des cours différents. Pour établir le bulletin de cet élève, il faut donc extraire des données de la table des travaux, bien sûr, mais en relation avec des informations trouvées dans d'autres tables (celles des cours, des classes, des options, etc.)

SGBDR - Le modèle client/serveur

modifier

Les programmes informatiques capables de gérer efficacement de tels ensembles de données complexes sont forcément complexes, eux aussi. On appelle ces programmes des SGBDR (Systèmes de Gestion de Bases de Données Relationnelles). Il s'agit d'applications informatiques de première importance pour les entreprises. Certaines sont les fleurons de sociétés spécialisées (IBM, Oracle, Microsoft, Informix, Sybase...) et sont en général vendues à des prix fort élevés. D'autres ont été développées dans des centres de recherche et d'enseignement universitaires (PostgreSQL, MySQL…); elles sont alors en général tout à fait gratuites.

Ces systèmes ont chacun leurs spécificités et leurs performances, mais la plupart fonctionnant sur le modèle client/serveur : cela signifie que la plus grosse partie de l'application (ainsi que la base de données prise en charge) est installée en un seul endroit, en principe sur une machine puissante (cet ensemble constituant donc le serveur), alors que l'autre partie, beaucoup plus simple, est installée sur un nombre indéterminé de postes de travail, et on appelle celles-ci des clients.

Les clients sont reliés au serveur, en permanence ou non, par divers procédés et protocoles (éventuellement par l'intermédiaire de l'internet). Chacun d'entre eux peut accéder à une partie plus ou moins importante des données, avec autorisation ou non de modifier certaines d'entre elles, d'en ajouter ou d'en supprimer, en fonction de règles d'accès bien déterminées. (Ces règles sont définies par un administrateur de la base de données).

Le serveur et ses clients sont en fait des applications distinctes qui s'échangent des informations. Imaginez par exemple que vous êtes l'un des utilisateurs du système. Pour accéder aux données, vous devez lancer l'exécution d'une application cliente sur un poste de travail quelconque. Dans son processus de démarrage, l'application cliente commence par établir la connexion avec le serveur et la base de données[1]. Lorsque la connexion est établie, l'application cliente peut interroger le serveur en lui envoyant une requête sous une forme convenue. Il s'agit par exemple de retrouver une information précise. Le serveur exécute alors la requête en recherchant les données correspondantes dans la base, puis il expédie en retour une certaine réponse au client.

Cette réponse peut être l'information demandée, ou encore un message d'erreur en cas d'insuccès.

La communication entre le client et le serveur est donc faite de requêtes et de réponses. Les requêtes sont de véritables instructions expédiées du client au serveur, non seulement pour extraire des données de la base, mais aussi pour en ajouter, en supprimer, en modifier, etc.

Le langage SQL - Gadfly

modifier

Étant donnée la diversité des SGBDR existants, on pourrait craindre que chacun d'eux nécessite l'utilisation d'un langage particulier pour les requêtes qu'on lui adresse. En fait, de grands efforts ont été accomplis un peu partout pour la mise au point d'un langage commun, et il existe à présent un standard bien établi : SQL (Structured Query Language, ou langage de requêtes structuré)[2].

Vous aurez probablement l'occasion de rencontrer SQL dans d'autres domaines (bureautique, par exemple). Dans le cadre de cette introduction à l'apprentissage de la programmation avec Python, nous allons nous limiter à la présentation de deux exemples : la mise en œuvre d'un petit SGBDR réalisé exclusivement à l'aide de Python, et l'ébauche d'un logiciel client plus ambitieux destiné à communiquer avec un serveur de bases de données MySQL.

Notre première réalisation utilisera un module nommé Gadfly. Entièrement écrit en Python, ce module ne fait pas partie de la distribution standard et doit donc être installé séparément. Il intègre un large sous-ensemble de commandes SQL. Ses performances ne sont évidemment pas comparables à celles d'un gros SGBDR spécialisé[3], mais elles sont tout à fait excellentes pour la gestion de bases de données modestes. Absolument portable comme Python lui-même, Gadfly fonctionnera indifféremment sous Windows , Linux ou MacOS. De même, les répertoires contenant des bases de données produites sous Gadfly pourront être utilisées sans modification depuis l'un ou l'autre de ces systèmes.

Si vous souhaitez développer une application qui doit gérer des relations relativement complexes dans une petite base de données, le module Gadfly peut vous faciliter grandement la tâche.

Mise en œuvre d'une base de données simple avec Gadfly

modifier

Nous allons ci-après examiner comment mettre en place une application simple, qui fasse office à la fois de serveur et de client sur la même machine.

Création de la base de données

modifier

Comme vous vous y attendez certainement, il suffit d'importer le module Gadfly pour accéder aux fonctionnalités correspondantes[4].

Vous devez ensuite créer une instance (un objet) de la classe Gadfly :

import gadfly
baseDonn = gadfly.gadfly()

L'objet baseDonn ainsi créé est votre moteur de base de données local, lequel effectuera la plupart de ses opérations en mémoire vive. Ceci permet une exécution très rapide des requêtes.

Pour créer la base de données proprement dite, il faut employer la méthode "startup" de cet objet :

baseDonn.startup("mydata","E:/Python/essais/gadfly")

Le premier paramètre transmis, mydata, est le nom choisi pour la base de données (vous pouvez évidemment choisir un autre nom !). Le second paramètre est le répertoire où l'on souhaite installer cette base de données. (Ce répertoire doit avoir été créé au préalable, et toute base de données de même nom qui préexisterait dans ce répertoire est écrasée sans avertissement).

Les trois lignes de code que vous venez d'entrer sont suffisantes : vous disposez dès à présent d'une base de données fonctionnelle, dans laquelle vous pouvez créer différentes tables, puis ajouter, supprimer ou modifier des données dans ces tables.

Pour toutes ces opérations, vous allez utiliser le langage SQL.

Afin de pouvoir transmettre vos requêtes SQL à l'objet baseDonn , vous devez cependant mettre en œuvre un curseur. Il s'agit d'une sorte de tampon mémoire intermédiaire, destiné à mémoriser temporairement les données en cours de traitement, ainsi que les opérations que vous effectuez sur elles, avant leur transfert définitif dans de vrais fichiers. Cette technique permet donc d'annuler si nécessaire une ou plusieurs opérations qui se seraient révélées inadéquates (Vous pouvez en apprendre davantage sur ce concept en consultant l'un des nombreux manuels qui traitent du langage SQL). Veuillez à présent examiner le petit script ci-dessous, et noter que les requêtes SQL sont des chaînes de caractères, prises en charge par la méthode "execute" de l'objet curseur :

cur = baseDonn.cursor()
cur.execute("create table membres (age integer, nom varchar, taille float)")
cur.execute("insert into membres(age, nom, taille) values (21,'Dupont',1.83)")
cur.execute("INSERT INTO MEMBRES(AGE, NOM, TAILLE) VALUES (15,'Suleau',1.57)")
cur.execute("Insert Into Membres(Age, Nom, Taille) Values (18,'Forcas',1.69)")
baseDonn.commit()

La première des lignes ci-dessus crée l'objet curseur cur. Les chaînes de caractères comprises entre guillemets dans les 4 lignes suivantes contiennent des requêtes SQL très classiques. Notez bien que le langage SQL ne tient aucun compte de la casse des caractères : vous pouvez encoder vos requêtes SQL indifféremment en majuscules ou en minuscules (ce qui n'est pas le cas pour les instructions Python environnantes, bien entendu !)

La seconde ligne crée une table nommée membres, laquelle contiendra des enregistrements de 3 champs : le champ age de type « nombre entier », le champ nom de type « chaîne de caractères » (de longueur variable[5]) et le champ taille, de type « nombre réel » (à virgule flottante). Le langage SQL autorise en principe d'autres types, mais ils ne sont pas implémentés dans Gadfly.

Les trois lignes qui suivent sont similaires. Nous y avons mélangé majuscules et minuscules pour bien montrer que la casse n'est pas significative en SQL. Ces lignes servent à insérer trois enregistrements dans la table membres.

À ce stade des opérations, les enregistrements n'ont pas encore été transférés dans de véritables fichiers sur disque. Il est donc possible de revenir en arrière. Le transfert sur disque est activé par la méthode commit() de la dernière ligne d'instructions.

Connexion à une base de données existante

modifier

Supposons qu'à la suite des opérations ci-dessus, nous décidions de terminer le script, ou même d'éteindre l'ordinateur. Comment devrons-nous procéder par la suite pour accéder à nouveau à notre base de données ?

L'accès à une base de données existante ne nécessite que deux lignes de code :

import gadfly
baseDonn = gadfly.gadfly("mydata","E:/Python/essais/gadfly")

Ces deux lignes suffisent en effet pour transférer en mémoire vive les tables contenues dans les fichiers enregistrés sur disque. La base de données peut désormais être interrogée et modifiée :

cur = baseDonn.cursor()
cur.execute("select * from membres")
print cur.pp()

La première de ces trois lignes ouvre un curseur. La requête émise dans la seconde ligne demande la sélection d'un ensemble d'enregistrements, qui seront transférés de la base de données au curseur. Dans le cas présent, la sélection n'en n'est pas vraiment une : on y demande en effet d'extraire tous les enregistrements de la table membres (le symbole * est fréquemment utilisé en informatique avec la signification « tout » ou « tous »).

La méthode pp() utilisée sur le curseur, dans la troisième ligne, provoque un affichage de tout ce qui est contenu dans le curseur sous une forme pré-formatée (les données présentes sont automatiquement disposées en colonnes). « pp » doit en effet être compris comme « pretty print ».

Si vous préférez contrôler vous-même la mise en page des informations, il vous suffit d'utiliser à sa place la méthode fetchall(), laquelle renvoie une liste de tuples. Essayez par exemple :

for x in cur.fetchall():
    print x, x[0], x[1], x[2]

Vous pouvez bien entendu ajouter des enregistrements supplémentaires :

cur.execute("Insert Into Membres(Age, Nom, Taille) Values (19,'Ricard',1.75)")

Pour modifier un ou plusieurs enregistrements, exécutez une requête du type :

cur.execute("update membres set nom ='Gerart' where nom='Ricard'")

Pour supprimer un ou plusieurs enregistrements, utilisez une requête telle que :

cur.execute("delete from membres where nom='Gerart'")

Si vous effectuez toutes ces opérations à la ligne de commande de Python, vous pouvez en observer le résultat à tout moment en effectuant un pretty print comme expliqué plus haut. Étant donné que toutes les modifications apportées au curseur se passent en mémoire vive, rien n'est enregistré définitivement tant que vous n'exécutez pas l'instruction baseDonn.commit().

Vous pouvez donc annuler toutes les modifications apportées depuis le commit() précédent, en refermant la connexion à l'aide de l'instruction :

baseDonn.close()

Recherches dans une base de données

modifier

Exercices

  1. Avant d'aller plus loin, et à titre d'exercice de synthèse, nous allons vous demander de créer entièrement vous-même une base de données Musique qui contiendra les deux tables suivantes (cela représente un certain travail, mais il faut que vous puissiez disposer d'un certain nombre de données pour pouvoir expérimenter les fonctions de recherche et de tri) :
    oeuvres
    comp (chaîne)
    titre (chaîne)
    duree (entier)
    interpr (chaîne)
    compositeurs
    comp (chaîne)
    a_naiss (entier)
    a_mort (entier)

Solution

  1. (Création de la base de données "musique") :
    import gadfly
    
    connex = gadfly.gadfly()
    connex.startup("musique","E:/Python/essais/gadfly")
    cur = connex.cursor()
    requete = "create table compositeurs (comp varchar, a_naiss integer,\
               a_mort integer)" 
    cur.execute(requete)
    requete = "create table oeuvres (comp varchar, titre varchar,\
               duree integer, interpr varchar)" 
    cur.execute(requete)
    
    print "Entrée des enregistrements, table des compositeurs :"
    while 1:
        nm = raw_input("Nom du compositeur (<Enter> pour terminer) : ")
        if nm =='':
            break
        an = raw_input("Année de naissance : ")
        am = raw_input("Année de mort : ")
        requete ="insert into compositeurs(comp, a_naiss, a_mort) values \
                     ('%s', %s, %s)" % (nm, an, am)
        cur.execute(requete)
    # Affichage des données entrées, pour vérification :
    cur.execute("select * from compositeurs")
    print cur.pp()
    
    print "Entrée des enregistrements, table des œuvres musicales :"
    while 1:
        nom = raw_input("Nom du compositeur (<Enter> pour terminer) : ")
        if nom =='':
            break
        tit = raw_input("Titre de l’œuvre : ")
        dur = raw_input("durée (minutes) : ")
        int = raw_input("interprète principal : ")
        requete ="insert into oeuvres(comp, titre, duree, interpr) values \
                     ('%s', '%s', %s, '%s')" % (nom, tit, dur, int)
        cur.execute(requete)
    # Affichage des données entrées, pour vérification :
    cur.execute("select * from oeuvres")
    print cur.pp()
    
    connex.commit()
    

Commencez à remplir la table compositeurs avec les données qui suivent (... et profitez de cette occasion pour faire la preuve des compétences que vous maîtrisez déjà, en écrivant un petit script pour vous faciliter l'entrée des informations : une boucle s'impose !)

comp            a_naiss  a_mort

Mozart          1756     1791
Beethoven       1770     1827
Handel          1685     1759
Schubert        1797     1828
Vivaldi         1678     1741
Monteverdi      1567     1643
Chopin          1810     1849		
Bach            1685     1750

Dans la table œuvres, entrez les données suivantes :

comp            titre                           duree       interpr

Vivaldi	        Les quatre saisons              20          T. Pinnock
Mozart          Concerto piano N°12             25          M. Perahia
Brahms          Concerto violon N°2             40          A. Grumiaux	
Beethoven       Sonate "au clair de lune"       14          W. Kempf
Beethoven       Sonate "pathétique"             17          W. Kempf
Schubert        Quintette "la truite"           39          SE of London
Haydn           La création                     109         H. Von Karajan
Chopin          Concerto piano N°1              42          M.J. Pires
Bach            Toccata & fugue                 9           P. Burmester
Beethoven       Concerto piano N°4              33          M. Pollini
Mozart          Symphonie N°40                  29          F. Bruggen
Mozart          Concerto piano N°22             35          S. Richter
Beethoven       Concerto piano N°3              37          S. Richter


Les champs a_naiss et a_mort contiennent respectivement l'année de naissance et l'année de la mort des compositeurs. La durée des œuvres est fournie en minutes. Vous pouvez évidemment ajouter autant d'enregistrements d'œuvres et de compositeurs que vous le voulez, mais ceux qui précèdent devraient suffire pour la suite de la démonstration.

Pour ce qui va suivre, nous supposerons donc que vous avez effectivement encodé les données des deux tables décrites ci-dessus. (Si vous éprouvez des difficultés à écrire le script nécessaire, nous en donnons un exemple, voir la solution de l'exercice précédent).

Le petit script ci-dessous est fourni à titre purement indicatif. Il s'agit d'un client SQL rudimentaire, qui vous permet de vous connecter à la base de données « musique » qui devrait à présent exister dans l'un de vos répertoires, d'y ouvrir un curseur et d'utiliser celui-ci pour effectuer des requêtes. Notez encore une fois que rien n'est transcrit sur le disque tant que la méthode commit() n'a pas été invoquée.

# Utilisation d'une petite base de données acceptant les requêtes SQL

import gadfly

baseDonn = gadfly.gadfly("musique","E:/Python/essais/gadfly")
cur = baseDonn.cursor()
while 1:
    print "Veuillez entrer votre requête SQL (ou <Enter> pour terminer) :"
    requete = raw_input()
    if requete =="":
        break
    try:
        cur.execute(requete)        # tentative d'exécution de la requête SQL
    except:
        print '*** Requête incorrecte ***'
    else:    
        print cur.pp()              # affichage du résultat de la requête
    print

choix = raw_input("Confirmez-vous l'enregistrement (o/n) ? ")
if choix[0] == "o" or choix[0] == "O":
    baseDonn.commit()
else:
    baseDonn.close()

Cette application très simple n'est évidemment qu'un exemple. Il faudrait y ajouter la possibilité de choisir la base de données ainsi que le répertoire de travail. Pour éviter que le script ne se « plante » lorsque l'utilisateur encode une requête incorrecte, nous avons utilisé ici le traitement des exceptions déjà décrit à la page

À faire... 


.

La requête select

modifier

L'une des instructions les plus puissantes du langage SQL est l'instruction select, dont nous allons à présent explorer quelques fonctionnalités. Rappelons encore une fois que nous n'abordons ici qu'une très petite partie du sujet : la description détaillée de SQL peut occuper plusieurs livres.

Lancez donc le script ci-dessus, et analysez attentivement ce qui se passe lorsque vous proposez les requêtes suivantes :

select *  from oeuvres
select *  from oeuvres where comp = 'Mozart'
select comp, titre, duree  from oeuvres order by comp
select titre, comp from oeuvres where comp='Beethoven' or comp='Mozart' order by comp
select count(*) from oeuvres 
select sum(duree) from oeuvres
select avg(duree) from oeuvres
select sum(duree) from oeuvres where comp='Beethoven'
select * from oeuvres where duree >35 order by duree desc

Pour chacune de ces requêtes, tâchez d'exprimer le mieux possible ce qui se passe. Fondamentalement, vous activez sur la base de données des filtres de sélection et des tris. Les requêtes suivantes sont plus élaborées, car elles concernent les deux tables à la fois.

select o.titre, c.nom, c.a_naiss from oeuvres o, compositeurs c where o.comp = c.comp
select comp from oeuvres intersect select comp from compositeurs
select comp from oeuvres except select comp from compositeurs
select comp from compositeurs except select comp from oeuvres
select distinct comp from oeuvres union select comp from compositeurs

Il ne nous est pas possible de développer davantage le langage de requêtes dans le cadre restreint de ces notes. Nous allons cependant examiner encore un exemple de réalisation Python faisant appel à un système de bases de données, mais en supposant cette fois qu'il s'agisse de dialoguer avec un système serveur indépendant (lequel pourrait être par exemple un gros serveur de bases de données d'entreprise, un serveur de documentation dans une école, etc.).

Ébauche d'un logiciel client pour MySQL

modifier

Pour terminer ce chapitre, nous allons vous proposer dans les pages qui suivent un exemple de réalisation concrète. Il ne s'agira pas d'un véritable logiciel (le sujet exigerait qu'on lui consacre un ouvrage spécifique), mais plutôt d'une ébauche d'analyse, destinée à vous montrer comment vous pouvez « penser comme un programmeur » lorsque vous abordez un problème complexe.

Les techniques que nous allons mettre en œuvre ici sont de simples suggestions, dans lesquelles nous essayerons d'utiliser au mieux les outils que vous avez découverts au cours de votre apprentissage dans les chapitres précédents, à savoir : les structures de données de haut niveau (listes et dictionnaires), et la programmation par objets. Il va de soi que les options retenues dans cet exercice restent largement critiquables : vous pouvez bien évidemment traiter les mêmes problèmes en utilisant des approches différentes.

Notre objectif concret est d'arriver à réaliser rapidement un client rudimentaire, capable de dialoguer avec un « vrai » serveur de bases de données tel que MySQL. Nous voudrions que notre client reste un petit utilitaire très généraliste : qu'il soit capable de mettre en place une petite base de données comportant plusieurs tables, qu'il puisse servir à produire des enregistrements pour chacune d'elles, qu'il permette de tester le résultat de requêtes SQL basiques.

Dans les lignes qui suivent, nous supposerons que vous avez déjà accès à un serveur MySQL, sur lequel une base de données « discotheque » aura été créée pour l'utilisateur « jules », lequel s'identifie à l'aide du mot de passe « abcde ». Ce serveur peut être situé sur une machine distante accessible via un réseau, ou localement sur votre ordinateur personnel.

L'installation et la configuration d'un serveur MySQL sortent du cadre de cet ouvrage, mais ce n'est pas une tâche bien compliquée. C'est même fort simple si vous travaillez sous Linux, installé depuis une distribution « classique » telle que Debian, RedHat, SuSE ou Mandrake. Il vous suffit d'installer les paquetages MySQL-server et Python-MySQL, de démarrer le service MySQL, puis d'entrer les commandes :

mysqladmin  -u  root  password  xxxx

Cette première commande définit le mot de passe de l'administrateur principal de MySQL. Elle doit être exécutée par l'administrateur du système Linux (root), avec un mot de passe de votre choix. On se connecte ensuite au serveur sous le compte administrateur ainsi défini (le mot de passe sera demandé) :

mysql  -u  root  mysql  -p
grant  all  privileges  on  *.*  to jules@localhost  identified  by  'abcde';
grant  all  privileges  on  *.*  to jules@"%"  identified  by  'abcde';
\q

Ces commandes définissent un nouvel utilisateur « jules » pour le système MySQL, et cet utilisateur devra se connecter le mot de passe « abcde » (Les deux lignes autorisent respectivement l'accès local et l'accès via réseau).

Le nom d'utilisateur est quelconque : il ne doit pas nécessairement correspondre à un utilisateur système.

L'utilisateur « jules » peut à présent se connecter et créer des bases de données :

mysql  -u  jules  -p
create database discotheque;
\q

etc.

À ce stade, le serveur MySQL est prêt à dialoguer avec le client Python décrit dans ces pages.

Décrire la base de données dans un dictionnaire d'application

modifier

Une application dialoguant avec une base de données est presque toujours une application complexe. Elle comporte donc de nombreuses lignes de code, qu'il s'agit de structurer le mieux possible en les regroupant dans des classes (ou au moins des fonctions) bien encapsulées.

En de nombreux endroits du code, souvent fort éloignés les uns des autres, des blocs d'instructions doivent prendre en compte la structure de la base de données, c'est-à-dire son découpage en un certain nombre de tables et de champs, ainsi que les relations qui établissent une hiérarchie dans les enregistrements.

Or, l'expérience montre que la structure d'une base de données est rarement définitive. Au cours d'un développement, on réalise souvent qu'il est nécessaire de lui ajouter ou de lui retirer des champs, parfois même de remplacer une table mal conçue par deux autres, etc. Il n'est donc pas prudent de programmer des portions de code trop spécifiques d'une structure particulière, « en dur ».

Au contraire, il est hautement recommandable de décrire plutôt la structure complète de la base de données en un seul endroit du programme, et d'utiliser ensuite cette description comme référence pour la génération semi-automatique des instructions particulières concernant telle table ou tel champ. On évite ainsi, dans une large mesure, le cauchemar de devoir traquer et modifier un grand nombre d'instructions un peu partout dans le code, chaque fois que la structure de la base de données change un tant soit peu. Au lieu de cela, il suffit de changer seulement la description de référence, et la plus grosse partie du code reste correcte sans nécessiter de modification.

 Nous tenons là une idée maîtresse pour réaliser des applications robustes : un logiciel destiné au traitement de données devrait toujours être construit sur la base d'un dictionnaire d'application.

Ce que nous entendons ici par « dictionnaire d'application » ne doit pas nécessairement revêtir la forme d'un dictionnaire Python. N'importe quelle structure de données peut convenir, l'essentiel étant de se construire une référence centrale décrivant les données que l'on se propose de manipuler, avec peut-être aussi un certain nombre d'informations concernant leur mise en forme.

Du fait de leur capacité à rassembler en une même entité des données de n'importe quel type, les listes, tuples et dictionnaires de Python conviennent parfaitement pour ce travail. Dans l'exemple des pages suivantes, nous avons utilisé nous-mêmes un dictionnaire, dont les valeurs sont des listes de tuples, mais vous pourriez tout aussi bien opter pour une organisation différente des mêmes informations.

Tout cela étant bien établi, il nous reste encore à régler une question d'importance : où allons-nous installer concrètement ce dictionnaire d'application ?

Ses informations devront pouvoir être consultées depuis n'importe quel endroit du programme. Il semble donc obligatoire de l'installer dans une variable globale, de même d'ailleurs que d'autres données nécessaires au fonctionnement de l'ensemble de notre logiciel. Or vous savez que l'utilisation de variables globales n'est pas recommandée : elle comporte des risques, qui augmentent avec la taille du programme. De toute façon, les variables dites globales ne sont en fait globales qu'à l'intérieur d'un même module. Si nous souhaitons organiser notre logiciel comme un ensemble de modules (ce qui constitue par ailleurs une excellente pratique), nous n'aurons accès à nos variables globales que dans un seul d'entre eux.

Pour résoudre ce petit problème, il existe cependant une solution simple et élégante : regrouper dans une classe particulière toutes les variables qui nécessitent un statut global pour l'ensemble de l'application. Ainsi encapsulées dans l'espace de noms d'une classe, ces variables peuvent être utilisées sans problème dans n'importe quel module : il suffit en effet que celui-ci importe la classe en question. De plus, l'utilisation de cette technique entraîne une conséquence intéressante : le caractère « global » des variables définies de cette manière apparaît très clairement dans leur nom qualifié, puisque ce nom commence par celui de la classe contenante.

Si vous choisissez, par exemple, un nom explicite tel que Glob pour la classe destinée à accueillir vos variables « globales », vous vous assurez de devoir faire référence à ces variables partout dans votre code avec des noms tout aussi explicites tels que Glob.ceci , Glob.cela , etc[6].

C'est cette technique que vous allez découvrir à présent dans les premières lignes de notre script. Nous y définissons effectivement une classe Glob(), qui n'est donc rien d'autre qu'un simple conteneur. Aucun objet ne sera instancié à partir de celle classe, laquelle ne comporte d'ailleurs aucune méthode. Nos variables « globales » y sont définies comme de simples variables de classe, et nous pourrons donc y faire référence dans tout le reste du programme en tant qu'attributs de Glob. Le nom de la base de données, par exemple, pourra être retrouvé partout dans la variable Glob.dbName ; le nom ou l'adresse IP du serveur dans la variable Glob.host, etc. :


class Glob:
    """Espace de noms pour les variables et fonctions <pseudo-globales>"""

    dbName = "discotheque"      # nom de la base de données
    user = "jules"              # propriétaire ou utilisateur
    passwd = "abcde"            # mot de passe d'accès
    host = "192.168.0.235"      # nom ou adresse IP du serveur

    # Structure de la base de données.  Dictionnaire des tables & champs :
    dicoT ={"compositeurs":[('id_comp', "k", "clé primaire"),
                            ('nom', 25, "nom"),
                            ('prenom', 25, "prénom"),
                            ('a_naiss', "i", "année de naissance"),
                            ('a_mort', "i", "année de mort")],
            "oeuvres":[('id_oeuv', "k", "clé primaire"),
                       ('id_comp', "i", "clé compositeur"),
                       ('titre', 50, "titre de l’œuvre"),
                       ('duree', "i", "durée (en minutes)"),
                       ('interpr', 30, "interprète principal")]}

Le dictionnaire d'application décrivant la structure de la base de données est contenu dans la variable Glob.dicoT.

Il s'agit d'un dictionnaire Python, dont les clés sont les noms des tables. Quant aux valeurs, chacune d'elles est une liste contenant la description de tous les champs de la table, sous la forme d'autant de tuples.

Chaque tuple décrit donc un champ particulier de la table. Pour ne pas encombrer notre exercice, nous avons limité cette description à trois informations seulement : le nom du champ, son type et un bref commentaire. Dans une véritable application, il serait judicieux d'ajouter encore d'autres informations ici, concernant par exemple des valeurs limites éventuelles pour les données de ce champ, le formatage à leur appliquer lorsqu'il s'agit de les afficher à l'écran ou de les imprimer, le texte qu'il faut placer en haut de colonne lorsque l'on veut les présenter dans un tableau, etc.

Il peut vous paraître assez fastidieux de décrire ainsi très en détail la structure de vos données, alors que vous voudriez probablement commencer tout de suite une réflexion sur les divers algorithmes à mettre en œuvre afin de les traiter. Sachez cependant que si elle est bien faite, une telle description structurée vous fera certainement gagner beaucoup de temps par la suite, parce qu'elle vous permettra d'automatiser pas mal de choses. En outre, vous devez vous convaincre que cette tâche un peu ingrate vous prépare à bien structurer aussi le reste de votre travail : organisation des formulaires, tests à effectuer, etc.

Définir une classe d'objets-interfaces

modifier

La classe Glob() décrite à la rubrique précédente sera donc installée en début de script, ou bien dans un module séparé importé en début de script. Pour la suite de l'exposé, nous supposerons que c'est cette dernière formule qui est retenue : nous avons sauvegardé la classe Glob() dans un module nommé dict_app.py, d'où nous pouvons à présent l'importer dans le script suivant.

Ce nouveau script définit une classe d'objets-interfaces. Nous voulons en effet essayer de mettre à profit ce que nous avons appris dans les chapitres précédents, et donc privilégier la programmation par objets, afin de créer des portions de code bien encapsulées et largement réutilisables.

Les objets-interfaces que nous voulons construire seront similaires aux objets-fichiers que nous avons abondamment utilisés pour la gestion des fichiers au chapitre 9. Vous vous rappelez par exemple que nous ouvrons un fichier en créant un objet-fichier, à l'aide de la fonction-fabrique open(). D'une manière similaire, nous ouvrirons la communication avec la base de données en commençant par créer un objet-interface à l'aide de la classe GestionBD(), ce qui établira la connexion. Pour lire ou écrire dans un fichier ouvert, nous utilisons diverses méthodes de l'objet-fichier. D'une manière analogue, nous effectuerons nos opérations sur la base de données par l'intermédiaire des diverses méthodes de l'objet-interface.

import MySQLdb, sys
from dict_app import *

class GestionBD:
    """Mise en place et interfaçage d'une base de données MySQL"""
    def __init__(self, dbName, user, passwd, host, port =3306):
        "Établissement de la connexion - Création du curseur"
        try:
            self.baseDonn = MySQLdb.connect(db =dbName,
                  user =user, passwd =passwd, host =host, port =port)
        except Exception, err:
            print 'La connexion avec la base de données a échoué :\n'\
                  'Erreur détectée :\n%s' % err
            self.echec =1
        else:    
            self.cursor = self.baseDonn.cursor()   # création du curseur
            self.echec =0

    def creerTables(self, dicTables):
        "Création des tables décrites dans le dictionnaire <dicTables>."
        for table in dicTables:            # parcours des clés du dict.
            req = "CREATE TABLE %s (" % table
            pk =''
            for descr in dicTables[table]:
                nomChamp = descr[0]        # libellé du champ à créer
                tch = descr[1]             # type de champ à créer
                if tch =='i':
                    typeChamp ='INTEGER'
                elif tch =='k':
                    # champ 'clé primaire' (incrémenté automatiquement)
                    typeChamp ='INTEGER AUTO_INCREMENT'   
                    pk = nomChamp
                else:
                    typeChamp ='VARCHAR(%s)' % tch                
                req = req + "%s %s, " % (nomChamp, typeChamp)
            if pk == '':
                req = req[:-2] + ")"
            else:
                req = req + "CONSTRAINT %s_pk PRIMARY KEY(%s))" % (pk, pk)
            self.executerReq(req)

    def supprimerTables(self, dicTables):
        "Suppression de toutes les tables décrites dans <dicTables>"
        for table in dicTables.keys():
            req ="DROP TABLE %s" % table
            self.executerReq(req) 
        self.commit()                       # transfert -> disque

    def executerReq(self, req):
        "Exécution de la requête <req>, avec détection d'erreur éventuelle"
        try:
            self.cursor.execute(req)
        except Exception, err:
            # afficher la requête et le message d'erreur système :
            print "Requête SQL incorrecte :\n%s\nErreur détectée :\n%s"\
                   % (req, err)
            return 0
        else:
            return 1

    def resultatReq(self):
        "renvoie le résultat de la requête précédente (un tuple de tuples)"
        return self.cursor.fetchall()

    def commit(self):
        if self.baseDonn:
            self.baseDonn.commit()         # transfert curseur -> disque        

    def close(self):
        if self.baseDonn:
            self.baseDonn.close()
Commentaires
  • Lignes 1-2 : Outre notre propre module dict_app qui contient les variables « globales », nous importons le module sys qui contient quelques fonctions système, et le module MySQLdb qui contient tout ce qui est nécessaire pour communiquer avec MySQL. Rappelons que ce module ne fait pas partie de la distribution standard de Python, et qu'il doit donc être installé séparément.
  • Ligne 5 : Lors de la création des objets-interfaces, nous devrons fournir les paramètres de la connexion : nom de la base de données, nom de son utilisateur, nom ou adresse IP de la machine où est situé le serveur. Le numéro du port de communication est habituellement celui que nous avons prévu par défaut. Toutes ces informations sont supposées être en votre possession.
  • Lignes 8 à 17 : Il est hautement recommandable de placer le code servant à établir la connexion à l'intérieur d'un gestionnaire d'exceptions try-except-else, car nous ne pouvons pas présumer que le serveur sera nécessairement accessible. Remarquons au passage que la méthode __init__() ne peut pas renvoyer de valeur (à l'aide de l'instruction return), du fait qu'elle est invoquée automatiquement par Python lors de l'instanciation d'un objet. En effet : ce qui est renvoyé dans ce cas au programme appelant est l'objet nouvellement construit. Nous ne pouvons donc pas signaler la réussite ou l'échec de la connexion au programme appelant à l'aide d'une valeur de retour. Une solution simple à ce petit problème consiste à mémoriser le résultat de la tentative de connexion dans un attribut d'instance (variable self.echec), que le programme appelant peut ensuite tester quand bon lui semble.
À faire... 

Ajouter un exemple

  • Lignes 19 à 40 : Cette méthode automatise la création de toutes les tables de la base de données, en tirant profit de la description du dictionnaire d'application, lequel doit lui être transmis en argument. Une telle automatisation sera évidemment d'autant plus appréciable, que la structure de la base de données sera plus complexe (Imaginez par exemple une base de données contenant 35 tables !). Afin de ne pas alourdir la démonstration, nous avons restreint les capacités de cette méthode à la création de champs des types integer et varchar. Libre à vous d'ajouter les instructions nécessaires pour créer des champs d'autres types. Si vous détaillez le code, vous constaterez qu'il consiste simplement à construire une requête SQL pour chaque table, morceau par morceau, dans la chaîne de caractères req. Celle-ci est ensuite transmise à la méthode executerReq() pour exécution. Si vous souhaitez visualiser la requête ainsi construite, vous pouvez évidemment ajouter une instruction print req juste après la ligne 40. Vous pouvez également ajouter à cette méthode la capacité de mettre en place les contraintes d'intégrité référentielle, sur la base d'un complément au dictionnaire d'application qui décrirait ces contraintes. Nous ne développons pas cette question ici, mais cela ne devrait pas vous poser de problème si vous savez de quoi il retourne.
  • Lignes 42 à 47 : Beaucoup plus simple que la précédente, cette méthode utilise le même principe pour supprimer toutes les tables décrites dans le dictionnaire d'application.
  • Lignes 49 à 59 : Cette méthode transmet simplement la requête à l'objet curseur. Son utilité est de simplifier l'accès à celui-ci et de produire un message d'erreur si nécessaire.
  • Lignes 61 à 71 : Ces méthodes ne sont que de simples relais vers les objets produits par le module MySQLdb : l'objet-connecteur produit par la fonction-fabrique MySQLdb.connect(), et l'objet curseur correspondant. Elles permettent de simplifier légèrement le code du programme appelant.

Construire un générateur de formulaires

modifier

Nous avons ajouté cette classe à notre exercice pour vous expliquer comment vous pouvez utiliser le même dictionnaire d'application afin d'élaborer du code généraliste. L'idée développée ici est de réaliser une classe d'objets-formulaires capables de prendre en charge l'encodage des enregistrements de n'importe quelle table, en construisant automatiquement les instructions d'entrée adéquates grâce aux informations tirées du dictionnaire d'application.

Dans une application véritable, ce formulaire trop simpliste devrait certainement être fortement remanié, et il prendrait vraisemblablement la forme d'une fenêtre spécialisée, dans laquelle les champs d'entrée et leurs libellés pourraient encore une fois être générés de manière automatique. Nous ne prétendons donc pas qu'il constitue un bon exemple, mais nous voulons simplement vous montrer comment vous pouvez automatiser sa construction dans une large mesure. Tâchez de réaliser vos propres formulaires en vous servant de principes semblables.

class Enregistreur:
    """classe pour gérer l'entrée d'enregistrements divers"""
    def __init__(self, bd, table):
        self.bd =bd
        self.table =table
        self.descriptif =Glob.dicoT[table]   # descriptif des champs

    def entrer(self):
        "procédure d'entrée d'un enregistrement entier"
        champs ="("           # ébauche de chaîne pour les noms de champs
        valeurs ="("          # ébauche de chaîne pour les valeurs
        # Demander successivement une valeur pour chaque champ :
        for cha, type, nom in self.descriptif:
            if type =="k":    # on ne demandera pas le n° d'enregistrement
                continue      # à l'utilisateur (numérotation auto.)
            champs = champs + cha + ","
            val = raw_input("Entrez le champ %s :" % nom)
            if type =="i":
                valeurs = valeurs + val +","
            else:
                valeurs = valeurs + "'%s'," % (val)

        champs = champs[:-1] + ")"    # supprimer la dernière virgule,
        valeurs = valeurs[:-1] + ")"  # ajouter une parenthèse
        req ="INSERT INTO %s %s VALUES %s" % (self.table, champs, valeurs)
        self.bd.executerReq(req)

        ch =raw_input("Continuer (O/N) ? ")
        if ch.upper() == "O":
            return 0
        else:
            return 1
Commentaires
  • Lignes 1 à 6 : Au moment de leur instanciation, les objets de cette classe reçoivent la référence de l'une des tables du dictionnaire. C'est ce qui leur donne accès au descriptif des champs.
  • Ligne 8 : Cette méthode entrer() génère le formulaire proprement dit. Elle prend en charge l'entrée des enregistrements dans la table, en s'adaptant à leur structure propre grâce au descriptif trouvé dans le dictionnaire. Sa fonctionnalité concrète consiste encore une fois à construire morceau par morceau une chaîne de caractères qui deviendra une requête SQL, comme dans la méthode creerTables() de la classe GestionBD() décrite à la rubrique précédente. Vous pourriez bien entendu ajouter à la présente classe encore d'autres méthodes, pour gérer par exemple la suppression et/ou la modification d'enregistrements.
  • Lignes 12 à 21 : L'attribut d'instance self.descriptif contient une liste de tuples, et chacun de ceux-ci est fait de trois éléments, à savoir le nom d'un champ, le type de données qu'il est censé recevoir, et sa description « en clair ». La boucle for de la ligne 13 parcourt cette liste et affiche pour chaque champ un message d'invite construit sur la base de la description qui accompagne ce champ. Lorsque l'utilisateur a entré la valeur demandée, celle-ci et formatée dans une chaîne en construction. Le formatage s'adapte aux conventions du langage SQL, conformément au type requis pour le champ.
  • Lignes 23 à 26 : Lorsque tous les champs ont été parcourus, la requête proprement dite est assemblée et exécutée. Si vous souhaitez visualiser cette requête, vous pouvez bien évidemment ajouter une instruction print req juste après la ligne 25.

Le corps de l'application

modifier

Il ne nous paraît pas utile de développer davantage encore cet exercice dans le cadre d'un manuel d'initiation. Si le sujet vous intéresse, vous devriez maintenant en savoir assez pour commencer déjà quelques expériences personnelles. Veuillez alors consulter les bons ouvrages de référence, comme par exemple Python : How to program de Deitel & coll., ou encore les sites web consacrés aux extensions de Python.

Le script qui suit est celui d'une petite application destinée à tester les classes décrites dans les pages qui précèdent. Libre à vous de la perfectionner, ou alors d'en écrire une autre tout à fait différente !

###### Programme principal : #########

# Création de l'objet-interface avec la base de données : 
bd = GestionBD(Glob.dbName, Glob.user, Glob.passwd, Glob.host)
if bd.echec:
    sys.exit()
    
while 1:
    print "\nQue voulez-vous faire :\n"\
          "1) Créer les tables de la base de données\n"\
          "2) Supprimer les tables de la base de données ?\n"\
          "3) Entrer des compositeurs\n"\
          "4) Entrer des œuvres\n"\
          "5) Lister les compositeurs\n"\
          "6) Lister les œuvres\n"\
          "7) Exécuter une requête SQL quelconque\n"\
          "9) terminer ?                         Votre choix :",
    ch = int(raw_input())
    if ch ==1:
        # création de toutes les tables décrites dans le dictionnaire :
        bd.creerTables(Glob.dicoT)
    elif ch ==2:
        # suppression de toutes les tables décrites dans le dic. :
        bd.supprimerTables(Glob.dicoT)     
    elif ch ==3 or ch ==4:
        # création d'un <enregistreur> de compositeurs ou d'œuvres :
        table ={3:'compositeurs', 4:'oeuvres'}[ch]
        enreg =Enregistreur(bd, table)
        while 1:
            if enreg.entrer():
                break
    elif ch ==5 or ch ==6:
        # listage de tous les compositeurs, ou toutes les œuvres :
        table ={5:'compositeurs', 6:'oeuvres'}[ch]
        if bd.executerReq("SELECT * FROM %s" % table):
            # analyser le résultat de la requête ci-dessus :
            records = bd.resultatReq()      # ce sera un tuple de tuples
            for rec in records:             # => chaque enregistrement
                for item in rec:            # => chaque champ dans l'enreg.
                    print item,
                print
    elif ch ==7:
        req =raw_input("Entrez la requête SQL : ")
        if bd.executerReq(req):
            print bd.resultatReq()          # ce sera un tuple de tuples
    else:
        bd.commit()
        bd.close()
        break
Commentaires

On supposera bien évidemment que les classes décrites plus haut soient présentes dans le même script, ou qu'elles aient été importées.

  • Lignes 3 à 6 : L'objet-interface est créé ici. Si la création échoue, l'attribut d'instance bd.echec contient la valeur 1. Le test des lignes 5 et 6 permet alors d'arrêter l'application immédiatement (la fonction exit() du module sys sert spécifiquement à cela).
  • Ligne 8 : Le reste de l'application consiste à proposer sans cesse le même menu, jusqu'à ce que l'utilisateur choisisse l'option n° 9.
  • Lignes 27 et 28 : La classe Enregistreur() accepte de gérer les enregistrements de n'importe quelle table. Afin de déterminer laquelle doit être utilisée lors de l'instanciation, on utilise un petit dictionnaire qui indique quel nom retenir, en fonction du choix opéré par l'utilisateur (option n° 3 ou n° 4).
  • Lignes 29 à 31 : La méthode entrer() de l'objet-enregistreur renvoie une valeur 0 ou 1 suivant que l'utilisateur ait choisi de continuer à entrer des enregistrements, ou bien d'arrêter. Le test de cette valeur permet d'interrompre la boucle de répétition en conséquence.
  • Lignes 35 et 44 : La méthode executerReq() renvoie une valeur 0 ou 1 suivant que la requête ait été acceptée ou non par le serveur. On peut donc tester cette valeur pour décider si le résultat doit être affiché ou non.

Exercices

  1. Modifiez le script décrit dans ces pages de manière à ajouter une table supplémentaire à la base de données. Ce pourrait être par exemple une table « orchestres », dont chaque enregistrement contiendrait le nom de l'orchestre, le nom de son chef, et le nombre total d'instruments.
  2. Ajoutez d'autres types de champ à l'une des tables (par exemple un champ de type float (réel) ou de type date), et modifiez le script en conséquence.

Solution

  1. Réfléchissez !
  2. Réfléchissez !
  1. Il vous faudra certainement entrer quelques informations pour obtenir l'accès : adresse du serveur sur le réseau, nom de la base de données, nom d'utilisateur, mot de passe, ...
  2. Quelques variantes subsistent entre différentes implémentations du SQL, pour des requêtes très spécifiques, mais la base reste cependant la même.
  3. Gadfly se révèle relativement efficace pour la gestion de bases de données de taille moyenne, en mode mono-utilisateur. Pour gérer de grosses bases de données en mode multi-utilisateur, il faut faire appel à des SGDBR plus ambitieux tels que PostgreSQL, pour lesquels des modules clients Python existent aussi (Pygresql, par ex.).
  4. Le module Gadfly est disponible gratuitement sur l'internet. Voir http://sourceforge.net/projects/gadfly
  5. Veuillez noter qu'en SQL, les chaînes de caractères doivent être délimitées par des apostrophes. Si vous souhaitez que la chaîne contienne elle-même une ou plusieurs apostrophes, il vous suffit de doubler celles-ci.
  6. Vous pourriez également placer vos variables « globales » dans un module nommé Glob.py, puis importer celui-ci. Utiliser un module ou une classe comme espace de noms pour stocker des variables sont donc des techniques assez similaires. L'utilisation d'une classe est peut-être un peu plus souple et plus lisible, puisque la classe peut accompagner le reste du script, alors qu'un module est nécessairement un fichier distinct.


L'interface CGI

Description

modifier

L'interface CGI (pour Common Gateway Interface) est un composant de la plupart des logiciels serveurs de pages web. Il s'agit d'une passerelle qui leur permet de communiquer avec d'autres logiciels tournant sur le même ordinateur. Avec CGI, vous pouvez écrire des scripts dans différents langages (Perl, C, Tcl, PHP, Python ...).

Plutôt que de limiter le web à des documents écrits à l'avance, CGI permet de générer des pages web sur le champ, en fonction des données que fournit l'internaute par l'intermédiaire de son logiciel de navigation ou de données stockées sur le serveur que le script peut également modifier, par exemple en utilisant une base de données. Vous pouvez utiliser les scripts CGI pour créer une large palette d'applications : des services d'inscription en ligne, des outils de recherche dans des bases de données, des instruments de sondage d'opinions, des jeux, etc.

L'apprentissage de la programmation CGI peut faire l'objet de manuels entiers. Dans cet ouvrage d'initiation, nous vous expliquerons seulement quelques principes de base, afin de vous faire comprendre, par comparaison, l'énorme avantage que présentent les modules serveurs d'applications spécialisés tels que Karrigell, CherryPy ou Zope, pour le programmeur désireux de développer un site web interactif.

Installation

modifier
Pour plus de détails voir : Apache/CGI.

Par défaut, lire un fichier .py en HTTP renvoie son contenu. Pour que le serveur compile et exécute le code source, il faut que la configuration suivante figure dans sa configuration, ou bien que les scripts soient placés dans un répertoire contenant un fichier nommé .htaccess, avec les lignes[1] :

AddHandler cgi-script .py
Options +ExecCGI

 

Sur les serveurs Unix les fichiers ne sont pas exécutables par défaut, il faut donc le préciser pour chacun avec la commande : chmod +x *.py.

Exemples

modifier

Le module cgitb sert aux éventuels débogages :

#!C:\Program Files (x86)\Python\python.exe
# -*- coding: UTF-8 -*-
print "Content-type: text/html; charset=utf-8\n\n"
print "<html><head><title>Répertoire local</title></head><body>"
import cgitb
cgitb.enable()
import os
print "Le fichier CGI se trouve dans :"
print os.path.dirname(__file__)
print "</body></html>"

L'utilisation d'un formulaire nécessite un import cgi[2]. Pour une base de données MySQL c'est import MySQLdb[3]. Soit le fichier suivant CGI_MySQL.py utilisant les deux :

#!C:\Program Files (x86)\Python\python.exe
# -*- coding: UTF-8 -*-
print "Content-type: text/html; charset=utf-8\n\n"
print "<html><head><title>CGI BDD</title></head><body>"
print "<h1>Extraction MySQL</h1>"
print "<ul>"
import cgitb
cgitb.enable()
import cgi, MySQLdb
formulaire = cgi.FieldStorage()
if formulaire.getvalue('nom') == None:
	print "<h2>Rechercher un nom</h2>"
	print '''
	<form action="CGI_MySQL.py" method="post">
	<input type="text" name="nom" />
	<input type="submit"></form>
		'''
else:
	print "<h2>Résultat</h2>"
	print "Liste des correspondances pour " + formulaire.getvalue('nom') + " :"
	connection = MySQLdb.connect(user='login1', passwd='mdp1', db='base1')
	cursor = connection.cursor()
	cursor.execute("SELECT page_title FROM page WHERE nom ='"+formulaire.getvalue('nom')+"'")
	for row in cursor.fetchall():
		print "<li>%s</li>" % row[0]
	connection.close()
print "</ul>"
print "</body></html>"

Une interaction CGI rudimentaire

modifier

Veuillez donc encoder le document HTML ci-dessous à l'aide d'un éditeur quelconque :

<HTML>
<HEAD><TITLE>Exercice avec Python</TITLE></HEAD>
<BODY>

<DIV ALIGN="center">
<IMG SRC="penguin.gif">
<H2>Page Web interactive</H2>
<P>Cette page est associée à un script Python</P>

<FORM ACTION="http://localhost/cgi-bin/input_query.py" METHOD="post">
<INPUT TYPE="submit" NAME="send" VALUE="Exécuter le script">
</FORM>

</DIV></BODY></HTML>

Vous savez certainement déjà que les balises initiales <HTML>, <HEAD>, <TITLE>, <BODY>, ainsi que les balises finales correspondantes, sont communes à tous les documents HTML. Nous ne détaillerons donc pas leur rôle ici.

La balise <DIV> utilisée à la ligne 5 sert habituellement à diviser un document HTML en sections distinctes. Nous l'utilisons ici pour définir une section dans laquelle tous les éléments seront centrés (horizontalement) sur la page.

À la ligne 6, nous insérons une petite image.

La ligne 7 définit une ligne de texte comme étant un titre de 2e importance.

La ligne 8 est un paragraphe ordinaire.

Les lignes 10 à 12 contiennent le code important (pour ce qui nous occupe ici). Les balises <FORM> et </FORM> définissent en effet un formulaire, c'est-à-dire une portion de page Web susceptible de contenir divers widgets à l'aide desquels l'internaute pourra exercer une certaine activité : champs d'entrée, boutons, cases à cocher, boutons radio, etc.

La balise FORM doit contenir deux indications essentielles : l'action à accomplir lorsque le formulaire sera expédié (il s'agit en fait de fournir ici l'adresse URL du logiciel à invoquer pour traiter les données transmises), et la méthode à utiliser pour transmettre l'information (en ce qui nous concerne, ce sera toujours la méthode post).

Dans notre exemple, le logiciel que nous voulons invoquer est un script Python nommé input_query.py qui est situé dans un répertoire particulier du serveur d'intranet. Sur de nombreux serveurs, ce répertoire s'appelle souvent cgi-bin, par pure convention. Nous supposerons ici que l'administrateur de votre intranet scolaire vous autorise à installer vos scripts Python dans le même répertoire que celui où vous placez vos pages web personnelles.

Vous devrez donc modifier la ligne 10 de notre exemple, en remplaçant le nom de domaine dans l'adresse http://localhost/cgi-bin/input_query.py.

La ligne 11 contient la balise qui définit un widget de type « bouton d'envoi » (balise <INPUT TYPE="submit">). Le texte qui doit apparaître sur le bouton est précisé par l'attribut VALUE ="texte". L'indication NAME est facultative dans le cas présent. Elle mentionne le nom du widget lui-même (au cas où le logiciel destinataire en aurait besoin).

Lorsque vous aurez terminé l'encodage de ce document, sauvegardez-le dans le répertoire que l'on vous a attribué spécifiquement pour y placer vos pages, sous un nom quelconque, mais de préférence avec l'extension .html ou .htm (par exemple : essai.html).

Le script Python input_query.py est détaillé ci-dessous. Comme déjà signalé plus haut, vous pouvez installer ce script dans le même répertoire que votre document HTML initial :

#! /usr/bin/python
# -*- coding: utf-8 -*-
# Affichage d'un formulaire HTML simplifié :
print "Content-Type: text/html\n"
print """
<H3><FONT COLOR="Royal blue">
Page web produite par un script Python
</FONT></H3>

<FORM ACTION="print_result.py" METHOD="post">
<P>Veuillez entrer votre nom dans le champ ci-dessous, s.v.p. :</P>
<P><INPUT NAME="visiteur" SIZE=20 MAXLENGTH=20 TYPE="text"></P>
<P>Veuillez également me fournir une phrase quelconque :</P>
<TEXTAREA NAME="phrase" ROWS=2 COLS=50>Mississippi</TEXTAREA>
<P>J'utiliserai cette phrase pour établir un histogramme.</P>
<INPUT TYPE="submit" NAME="send" VALUE="Action">
</FORM>
"""

Ce script ne fait rien d'autre que d'afficher une nouvelle page web, laquelle contient encore une fois un formulaire, mais celui-ci nettement plus élaboré que le précédent.

La première ligne est absolument nécessaire : elle indique à l'interface CGI qu'il faut lancer l'interpréteur Python pour pouvoir exécuter le script. La seconde ligne spécifie l'encodage du code source.

La ligne 4 est indispensable. Elle permet à l'interpréteur Python d'initialiser un véritable document HTML qui sera transmis au serveur web. Celui-ci pourra à son tour le réexpédier au logiciel navigateur de l'internaute, et celui-ci le verra donc s'afficher dans la fenêtre de navigation.

La suite est du pur code HTML, traité par Python comme une simple chaîne de caractères que l'on affiche à l'aide de l'instruction print. Pour pouvoir y insérer tout ce que nous voulons, y compris les sauts à la ligne, les apostrophes, les guillemets, etc., nous délimitons cette chaîne de caractères à l'aide de « triples guillemets » (Rappelons également ici que les sauts à la ligne sont complètement ignorés en HTML : nous pouvons donc en utiliser autant que nous voulons pour « aérer » notre code et le rendre plus lisible).

Un formulaire HTML pour l'acquisition des données

modifier

Analysons à présent le code HTML lui-même. Nous y trouvons essentiellement un nouveau formulaire, qui comporte plusieurs paragraphes, parmi lesquels on peut reconnaître quelques widgets. La ligne 10 indique le nom du script CGI auquel les données du formulaire seront transmises : il s'agira bien évidemment d'un autre script Python.

À la ligne 12, on trouve la définition d'un widget de type « champ d'entrée » (Balise INPUT, avec TYPE="text"). L'utilisateur est invité à y encoder son nom. Le paramètre MAXLENGTH définit une longueur maximale pour la chaîne de caractères qui sera entrée ici (20 caractères, en l'occurrence). Le paramètre SIZE définit la taille du champ tel qu'il doit apparaître à l'écran, et le paramètre NAME est le nom que nous choisissons pour la variable destinée à mémoriser la chaîne de caractères attendue.

Un second champ d'entrée un peu différent est défini à la ligne 14 (balise TEXTAREA). Il s'agit d'un réceptacle plus vaste, destiné à accueillir des textes de plusieurs lignes. (Ce champ est automatiquement pourvu d'ascenseurs si le texte à insérer se révèle trop volumineux). Ses paramètres ROWS et COLS sont assez explicites. Entre les balises initiale et finale, on peut insérer un texte par défaut (Mississippi dans notre exemple).

Comme dans l'exemple précédent, la ligne 16 contient la définition du bouton qu'il faudra actionner pour transmettre les données au script CGI destinataire, lequel est décrit ci-après.

Un script CGI pour le traitement des données

modifier

Le mécanisme utilisé à l'intérieur d'un script CGI pour réceptionner les données transmises par un formulaire HTML est fort simple, comme vous pouvez l'analyser dans l'exemple ci-dessous :

#! /usr/bin/python
# Traitement des données transmises par un formulaire HTML

import cgi                        # Module d'interface avec le serveur web
form = cgi.FieldStorage()         # Réception de la requête utilisateur :
                                  # il s'agit d'une sorte de dictionnaire
if form.has_key("phrase"):        # La clé n'existera pas si le champ
   text = form["phrase"].value    # correspondant est resté vide
else:
   text ="*** le champ phrase était vide ! ***"

if form.has_key("visiteur"):      # La clé n'existera pas si le champ
   nomv = form["visiteur"].value  # correspondant est resté vide
else:
   nomv ="mais vous ne m'avez pas indiqué votre nom"

print "Content-Type: text/html\n"
print """
<H3>Merci, %s !</H3>
<H4>La phrase que vous m'avez fournie était : </H4>
<H3><FONT Color="red"> %s </FONT></H3>""" % (nomv, text)

histogr ={}
for c in text:
   histogr[c] = histogr.get(c, 0) +1

liste = histogr.items()       # conversion en une liste de tuples
liste.sort()                  # tri de la liste
print "<H4>Fréquence de chaque caractère dans la phrase :</H4>"
for c, f in liste:
   print 'le caractère <B>"%s"</B> apparaît %s fois <BR>' % (c, f)

Les lignes 4 et 5 sont les plus importantes :

Le module cgi importé à la ligne 4 assure la connexion du script Python avec l'interface CGI , laquelle permet de dialoguer avec le serveur web.

À la ligne 5, la fonction FieldStorage() de ce module renvoie un objet qui contient l'ensemble des données transmises par le formulaire HTML. Nous plaçons cet objet, lequel est assez semblable à un dictionnaire classique, dans la variable form.

Par rapport à un véritable dictionnaire, l'objet placé dans form présente la différence essentielle qu'il faudra lui appliquer la méthode value() pour en extraire les données. Les autres méthodes applicables aux dictionnaires, telles la méthode has_key(), par exemple, peuvent être utilisées de la manière habituelle.

Une caractéristique importante de l'objet dictionnaire retourné par FieldStorage() est qu'il ne possédera aucune clé pour les champs laissés vides dans le formulaire HTML correspondant.

Dans notre exemple, le formulaire comporte deux champs d'entrée, auxquels nous avons associé les noms visiteur et phrase. Si ces champs ont effectivement été complétés par l'utilisateur, nous trouverons leurs contenus dans l'objet dictionnaire, aux index « visiteur » et « phrase ». Par contre, si l'un ou l'autre de ces champs n'a pas été complété, l'index correspondant n'existera tout simplement pas. Avant toute forme de traitement de valeurs, il est donc indispensable de s'assurer de la présence de chacun des index attendus, et c'est ce que nous faisons aux lignes 7 à 15.

Exercices

  1. Pour vérifier ce qui précède, vous pouvez par exemple désactiver (en les transformant en commentaires) les lignes 7, 9, 10, 12, 14 & 15 du script. Si vous testez le fonctionnement de l'ensemble, vous constaterez que tout se passe bien si l'utilisateur complète effectivement les champs qui lui sont proposés. Si l'un des champs est laissé vide, par contre, une erreur se produit.

Solution

  1. Réfléchissez !
 le script étant lancé par l'intermédiaire d'une page web, les messages d'erreur de Python ne seront pas affichés dans cette page, mais plutôt enregistrés dans le journal des événements du serveur web. Veuillez consulter l'administrateur de ce serveur pour savoir comment vous pouvez accéder à ce journal. De toute manière, attendez-vous à ce que la recherche des erreurs dans un script CGI soit plus ardue que dans une application ordinaire.

Le reste du script est assez classique.

  • Aux lignes 17 à 21, nous ne faisons qu'afficher les données transmises par le formulaire. Veuillez noter que les variables nomv et text doivent exister au préalable, ce qui rend indispensables les lignes 9, 10, 14 et 15.
  • Aux lignes 23, 24 et 25, nous nous servons d'un dictionnaire pour construire un histogramme simple.
  • À la ligne 27, nous convertissons le dictionnaire résultant en une liste de tuples, pour pouvoir trier celle-ci dans l'ordre alphabétique à la ligne 28.
  • La boucle for des lignes 30 et 31 se passe de commentaires.

Références

modifier
  1. (en) « HOWTO Use Python in the web »
  2. http://fr.openclassrooms.com/informatique/cours/apercu-de-la-cgi-avec-python
  3. https://pypi.python.org/pypi/MySQL-python/1.2.5


Applications web

Vous avez certainement déjà appris par ailleurs un grand nombre de choses concernant la rédaction de pages web. Vous savez que ces pages sont des documents au format HTML, que l'on peut consulter via un réseau (intranet ou internet) à l'aide d'un logiciel appelé browser web ou navigateur (ex : Firefox, Google Chrome, Konqueror, Internet Explorer, ...).

Les pages HTML sont installées dans les répertoires publics d'un autre ordinateur où fonctionne en permanence un logiciel appelé serveur Web (Apache, IIS, Zope, ...). Lorsqu'une connexion a été établie entre cet ordinateur et le vôtre, votre logiciel navigateur peut dialoguer avec le logiciel serveur (par l'intermédiaire de toute une série de dispositifs matériels et logiciels dont nous ne traiterons pas ici : lignes téléphoniques, routeurs, caches, protocoles de communication ...).

Le protocole HTTP qui gère la transmission des pages web autorise l'échange de données dans les deux sens. Mais dans la grande majorité des cas, le transfert d'informations n'a pratiquement lieu que dans un seul, à savoir du serveur vers le navigateur : des textes, des images, des fichiers divers lui sont expédiés en grand nombre (ce sont les pages consultées) ; en revanche, le navigateur n'envoie guère au serveur que de toutes petites quantités d'information : essentiellement les adresses URL des pages que l'internaute désire consulter.

Pages web interactives

modifier

Vous savez cependant qu'il existe des sites web où vous êtes invité à fournir vous-même des quantités d'information plus importantes : vos références personnelles pour l'inscription à un club ou la réservation d'une chambre d'hôtel, votre numéro de carte de crédit pour la commande d'un article sur un site de commerce électronique, votre avis ou vos suggestions, etc.

Dans un cas comme ceux-là, vous vous doutez bien que l'information transmise doit être prise en charge, du côté du serveur, par un programme spécifique. Il faut donc que les pages web destinées à accueillir cette information soient dotées d'un mécanisme assurant son transfert vers le logiciel destiné à le traiter. Il faudra également que ce logiciel puisse lui-même transmettre en retour une information au serveur, afin que celui-ci puisse présenter le résultat de l'opération à l'internaute, sous la forme d'une nouvelle page web.

Le but du présent chapitre est de vous expliquer comment vous pouvez vous servir de vos compétences de programmeur Python pour ajouter une telle interactivité à un site web, en y intégrant de véritables applications.

Remarque importante : Ce que nous allons expliquer dans les paragraphes qui suivent sera directement fonctionnel sur l'intranet de votre établissement scolaire ou de votre entreprise (à la condition toutefois que l'administrateur de cet intranet ait configuré son serveur de manière appropriée). En ce qui concerne l'internet, par contre, les choses sont un peu plus compliquées. Il va de soi que l'installation de logiciels sur un ordinateur serveur relié à l'internet ne peut se faire qu'avec l'accord de son propriétaire. Si un fournisseur d'accès à l'internet a mis a votre disposition un certain espace où vous êtes autorisé à installer des pages web « statiques » (c'est-à-dire de simples documents à consulter), cela ne signifie pas pour autant que vous pourrez y faire fonctionner des scripts Python. Pour que cela puisse marcher, vous devrez demander une autorisation et un certain nombre de renseignements à votre fournisseur d'accès. Il faudra en particulier lui demander si vous pouvez activer des scripts CGI écrits en Python à partir de vos pages, et dans quel(s) répertoire(s) vous pouvez les installer.

Un serveur web en pur Python !

modifier

Dans les pages précédentes, nous vous avons expliqué quelques rudiments de programmation CGI afin que vous puissiez mieux comprendre comment fonctionne une application web. Mais si vous voulez véritablement développer une telle application (par exemple un site web personnel doté d'une certaine interactivité), vous constaterez rapidement que l'interface CGI est un outil trop sommaire. Son utilisation telle quelle dans des scripts se révèle fort lourde, et il est donc préférable de faire appel à des outils plus élaborés.

L'intérêt pour le développement web est devenu très important, et il existe donc une forte demande pour des interfaces et des environnements de programmation bien adaptés à cette tâche. Or, même s'il ne peut pas prétendre à l'universalité de langages tels que C/C++, Python est déjà largement utilisé un peu partout dans le monde pour écrire des programmes très ambitieux, y compris dans le domaine des serveurs d'applications web. La robustesse et la facilité de mise en œuvre du langage ont séduit de nombreux développeurs de talent, qui ont réalisé des outils de développement web de très haut niveau. Plusieurs de ces applications peuvent vous intéresser si vous souhaitez réaliser vous-même des sites web interactifs de différents types.

Les produits existants sont pour la plupart des logiciels libres. Ils permettent de couvrir une large gamme de besoins, depuis le petit site personnel de quelques pages, jusqu'au gros site commercial collaboratif, capable de répondre à des milliers de requêtes journalières, et dont les différents secteurs sont gérés sans interférence par des personnes de compétences variées (infographistes, programmeurs, spécialistes de bases de données, etc.).

Le plus célèbre de ces produits est le logiciel Zope, déjà adopté par de grands organismes privés et publics pour le développement d'intranets et d'extranets collaboratifs. Il s'agit en fait d'un système serveur d'applications, très performant, sécurisé, presque entièrement écrit en Python, et que l'on peut administrer à distance à l'aide d'une simple interface web. Il ne nous est pas possible de décrire l'utilisation de Zope dans ces pages : le sujet est trop vaste, et un livre entier n'y suffirait pas. Sachez cependant que ce produit est parfaitement capable de gérer de très gros sites d'entreprise en offrant d'énormes avantages par rapport à des solutions classiques telles que PHP ou Java.

D'autres outils moins ambitieux mais tout aussi intéressants sont disponibles. Tout comme Zope, la plupart d'entre eux peuvent être téléchargés librement depuis l'internet. Le fait qu'ils soient écrits en Python assure en outre leur portabilité : vous pourrez donc les employer aussi bien sous Windows que sous Linux ou MacOs. Chacun d'eux peut être utilisé en conjonction avec un serveur web « classique » tel que Apache ou Xitami (c'est préférable si le site à réaliser est destiné à supporter une charge de connexions très importante), mais certains d'entre eux intègrent en outre leur propre serveur web, ce qui leur permet de fonctionner également de manière tout à fait autonome. Cette possibilité se révèle particulièrement intéressante au cours de la mise au point d'un site, car elle facilite la recherche des erreurs.

Cette totale autonomie alliée à la grande facilité de leur mise en œuvre fait de ces produits de fort bonnes solutions pour la réalisation de sites web d'intranet spécialisés, notamment dans des petites et moyennes entreprises, des administrations, ou dans des écoles. Si vous souhaitez développer une application Python qui soit accessible par l'intermédiaire d'un simple navigateur web, via un intranet d'entreprise (ou même via l'internet, si la charge prévisible n'est pas trop importante), ces applications sont faites pour vous.

Il en existe une grande variété : Poor man's Zope, Spyce, Karrigell, Webware, Cherrypy, Quixote, Twisted, etc. Choisissez en fonction de vos besoins : vous n'aurez que l'embarras du choix.

Dans les lignes qui suivent, nous allons décrire une petite application web fonctionnant à l'aide de Karrigell. Vous pouvez trouver ce système à l'adresse : http://karrigell.sourceforge.net. Il s'agit d'une solution de développement web simple, bien documentée en anglais et en français (son auteur, Pierre Quentel, est en effet originaire de Bretagne, tout comme le mot karrigell, d'ailleurs, lequel signifie « charrette »).

Installation de Karrigell

modifier

L'installation de Karrigell est un jeu d'enfant : il vous suffit d'extraire dans un répertoire quelconque le fichier archive que vous aurez téléchargé depuis l'internet. L'opération de désarchivage crée automatiquement un sous-répertoire nommé Karrigell-numéro de version. C'est ce répertoire que nous considérerons comme répertoire racine dans les lignes qui suivent.

Si vous ne comptez pas utiliser le serveur de bases de données Gadfly[1] qui vous est fourni en complément de Karrigell lui-même, c'est tout ! Sinon, entrez dans le sous-répertoire gadfly-1.0.0 et lancez la commande : python setup.py install (Sous Linux, il faut être root). Vous devez effectuer cette opération si vous souhaitez visualiser la totalité de la démonstration intégrée.

Démarrage du serveur :

modifier

Il s'agit donc bel et bien de mettre en route un serveur web, auquel vous pourrez accéder ensuite à l'aide d'un navigateur quelconque, localement ou par l'intermédiaire d'un réseau. Avant de le faire démarrer, il est cependant conseillé de jeter un petit coup d’œil dans son fichier de configuration, lequel se nomme Karrigell.ini et se trouve dans le répertoire-racine.

Par défaut, Karrigell attend les requêtes http sur le port n° 80. Et c'est bien ce numéro de port que la plupart des logiciels navigateurs utilisent eux-mêmes par défaut. Cependant, si vous installez Karrigell sur une machine Linux dont vous n'êtes pas l'administrateur, vous n'avez pas le droit d'utiliser les numéros de port inférieurs à 1024 (pour des raisons de sécurité). Si vous êtes dans ce cas, vous devez donc modifier le fichier de configuration afin que Karrigell utilise un numéro de port plus élevé. En général, vous choisirez d'enlever simplement le caractère # au début de la ligne 39, ce qui activera l'utilisation du n° de port 8080. Plus tard, vous souhaiterez peut-être encore modifier le fichier de configuration afin de modifier l'emplacement du répertoire racine pour votre site web (par défaut, c'est le répertoire du serveur lui-même).

Une fois le fichier de configuration modifié, entrez dans le répertoire racine du serveur, si vous n'y êtes pas déjà, et lancez simplement la commande :

python Karrigell.py

C'est tout. Votre serveur Karrigell se met en route, et vous pouvez en vérifier le fonctionnement tout de suite à l'aide de votre navigateur web préféré. Si vous lancez celui-ci sur la même machine que le serveur, vous le dirigerez vers une adresse telle que : http://localhost:8080/index.html, « localhost » étant le terme consacré pour désigner la machine locale, « 8080 » le numéro de port choisi dans le fichier de configuration, et « index.html » le nom du fichier qui contient la page d'accueil du site. Par contre, si vous voulez accéder à cette même page d'accueil depuis une autre machine, vous devrez (dans le navigateur de celle-ci) indiquer le nom ou l'adresse IP du serveur, en lieu et place de localhost.

Avec l'adresse indiquée au paragraphe précédent[2], vous atteindrez la page d'accueil d'un site de démonstration de Karrigell, qui est déjà pré-installé dans le répertoire racine. Vous y retrouverez la documentation de base, ainsi que toute une série d'exemples.

Dans ce qui précède, il est sous-entendu que vous avez lancé le serveur depuis une console texte, ou depuis une fenêtre de terminal. Dans un cas comme dans l'autre, les messages de contrôle émis par le serveur apparaîtront dans cette console ou cette fenêtre. C'est là que vous pourrez rechercher des messages d'erreur éventuels. C'est là aussi que vous devrez intervenir si vous voulez arrêter le serveur (avec la combinaison de touches CTRL-C).

Ébauche de site web

modifier

Essayons à présent de réaliser notre propre ébauche de site web. À la différence d'un serveur web classique, Karrigell peut gérer non seulement des pages HTML statiques (fichiers .htm, .html, .gif, .jpg, .css) mais également :

  • des scripts Python (fichiers .py) ;
  • des scripts hybrides Python Inside HTML (fichiers .pih) ;
  • des scripts hybrides HTML Inside Python (fichiers .hip).

Laissons de côté les scripts hybrides, dont vous pourrez étudier vous-même la syntaxe (par ailleurs très simple) si vous vous lancez dans une réalisation d'une certaine importance (ils pourront vous faciliter la vie). Dans le contexte limité de ces pages, nous nous contenterons de quelques expériences de base avec des scripts Python ordinaires.

Comme tous les autres éléments du site (fichiers .html, .gif, .jpeg, etc.), ces scripts Python devront être placés dans le répertoire racine[3]. Vous pouvez tout de suite effectuer un test élémentaire en rédigeant un petit script d'une seule ligne, tel que :

print "Bienvenue sur mon site web !"

Sauvegardez ce script sous le nom hello.py dans le répertoire racine, puis entrez l'adresse : http://localhost/hello.py (ou même : http://localhost/hello - l'extension .py peut être omise) dans votre navigateur. Vous devriez y voir apparaître le message. Cela signifie donc que dans l'environnement Karrigell, la sortie de l'instruction print est redirigée vers la fenêtre du navigateur client, plutôt que la console (ou la fenêtre de terminal) du serveur.

Étant donné que l'affichage a lieu dans une fenêtre de navigateur web, vous pouvez utiliser toutes les ressources de la syntaxe HTML afin d'obtenir un formatage déterminé. Vous pouvez par exemple afficher un petit tableau de 2 lignes et 3 colonnes, avec les instructions suivantes :

print """
<TABLE BORDER="1" CELLPADDING="5">
<TR> <TD> Rouge </TD> <TD> Vert </TD> <TD> Bleu </TD> </TR>
<TR> <TD> 15 % </TD> <TD> 62 % </TD> <TD> 23 % </TD> </TR>
</TABLE>
"""

Rappelons que la balise TABLE définit un tableau. Son option BORDER spécifie la largeur des bordures de séparation, et CELLPADDING l'écart à réserver autour du contenu des cellules. Les Balises TR et TD (Table Row et Table Data) définissent les lignes et les cellules du tableau.

Vous pouvez bien entendu utiliser également toutes les ressources de Python, comme dans l'exemple ci-dessous où nous construisons une table des sinus, cosinus et tangentes des angles compris entre 0° et 90°, à l'aide d'une boucle classique.

 
tableau en HTML généré par un script python
from math import sin, cos, tan, pi

# Construction de l'en-tête du tableau avec les titres de colonnes :
print """<TABLE BORDER="1" CELLPADDING="5">
<TR><TD>Angle</TD><TD>Sinus</TD><TD>Cosinus</TD><TD>Tangente</TD></TR>"""

for angle in range(0,62,10):    
    # conversion des degrés en radians :
    aRad = angle * pi / 180
    # construction d'une ligne de tableau, en exploitant le formatage des
    # chaînes de caractères pour fignoler l'affichage :
    print "<TR><TD>%s</TD><TD>%8.7f</TD><TD>%8.7f</TD><TD>%8.7g</TD></TR>"\
          % (angle, sin(aRad), cos(aRad), tan(aRad))
    
print "</TABLE>"
Commentaires

Ligne 7 : Nous nous servons de la fonction range() pour définir la gamme d'angles à couvrir (de zéro à 60 degrés par pas de 10).

Ligne 9 : Les fonctions trigonométriques de Python nécessitent que les angles soient exprimés en radians. Il faut donc effectuer une conversion.

Ligne 12 : Chaque ligne du tableau comporte quatre valeurs, lesquelles sont mises en forme à l'aide du système de formatage des chaînes de caractères : le marqueur de conversion %8.7f force un affichage à 8 chiffres, dont 7 après la « virgule » décimale. Le marqueur %8.7g fait à peu près la même chose, mais passe à la notation scientifique lorsque c'est nécessaire.

À ce stade, vous vous demandez peut-être où se situe la différence entre ce que nous venons d'expérimenter ici et un script CGI classique.

L'intérêt de travailler dans un environnement plus spécifique tel que Karrigell apparaît cependant très vite si vous faites des erreurs. En programmation CGI classique, les messages d'erreur émis par l'interpréteur Python ne s'affichent pas dans la fenêtre du navigateur. Ils sont enregistrés dans un fichier journal du serveur (Apache, par exemple), ce qui ne facilite pas leur consultation.

Avec un outil comme Karrigell, par contre, vous disposez d'une signalisation très efficace, ainsi que d'un outil de débogage complet. Faites l'expérience d'introduire une petite erreur dans le script ci-dessus, et relancez votre navigateur sur la page modifiée. Par exemple, en supprimant le double point à la fin de la ligne 7, nous avons obtenu nous-mêmes l'affichage suivant :

 
Page web affichant un cadre d'erreur et un traceback

En cliquant sur le bouton « Debug », on obtient encore une foule d'informations complémentaires (affichage du script complet, variables d'environnement, etc.).

Prise en charge des sessions

modifier

Lorsque l'on élabore un site web interactif, on souhaite fréquemment que la personne visitant le site puisse s'identifier et fournir un certain nombre de renseignements tout au long de sa visite dans différentes pages (l'exemple type étant le remplissage d'un « caddy » au cours de la consultation d'un site commercial), toutes ces informations étant conservées quelque part jusqu'à la fin de sa visite. Et il faut bien entendu réaliser cela indépendamment pour chaque client connecté.

Il serait possible de transmettre les informations de page en page à l'aide de champs de formulaires cachés, mais ce serait compliqué et très contraignant. Il est préférable que le système serveur soit doté d'un mécanisme spécifique, qui attribue à chaque client une session particulière. Karrigell réalise cet objectif par l'intermédiaire de cookies. Lorsqu'un nouveau visiteur du site s'identifie, le serveur génère un cookie appelé sessionId et l'envoie au navigateur web, qui l'enregistre. Ce cookie contient un « identifiant de session » unique, auquel correspond un objet-session sur le serveur. Lorsque le visiteur parcourt les autres pages du site, son navigateur renvoie à chaque fois le contenu du cookie au serveur, et celui-ci peut donc retrouver l'objet-session correspondant, à l'aide de son identifiant. L'objet-session reste donc disponible tout au long de la visite de l'internaute : il s'agit d'un objet Python ordinaire, dans lequel on mémorise un nombre quelconque d'informations sous forme d'attributs.

Au niveau de la programmation, voici comment cela se passe :

Pour chaque page dans laquelle vous voulez consulter ou modifier une information de session, vous commencez par créer un objet de la classe Session() :

objet_session = Session()

Si vous êtes au début de la session, Karrigell génère un identifiant unique, le place dans un cookie et envoie celui-ci au navigateur web. Vous pouvez alors ajouter un nombre quelconque d'attributs à l'objet-session :

objet_session.nom = "Jean Dupont"

Dans les autres pages, vous procédez de la même manière, mais l'objet produit dans ce cas par la classe Session() n'est pas nouveau : c'est l'objet créé en début de session, retrouvé en interne par le serveur grâce à son identifiant relu dans le cookie. Vous pouvez accéder aux valeurs de ses attributs, et aussi en ajouter de nouveaux :

obj_sess = Session()             # récupérer l'objet indiqué par le cookie
nom = obj_sess.nom               # retrouver la valeur d'un attribut existant
obj_sess.article = 49137         # ajouter un nouvel attribut

Les objets-sessions prennent aussi en charge une méthode close(), qui a pour effet d'effacer l'information de session. Vous n'êtes cependant pas obligé de clore explicitement les sessions : Karrigell s'assure de toute façon qu'il n'y ait jamais plus de 1000 sessions simultanées : il efface les plus anciennes quand on arrive à la 1000ème.

Exemple de mise en œuvre

Sauvegardez les trois petits scripts ci-dessous dans le répertoire-racine. Le premier génère un formulaire HTML similaire à ceux qui ont été décrits plus haut. Nommez-le sessionTest1.py :

# Affichage d'un formulaire d'inscription :

print """
<H3>Veuillez vous identifier, SVP :</H3>

<FORM ACTION = "sessionTest2.py">
Votre nom : <INPUT NAME = "nomClient"> <BR> 
Votre prénom : <INPUT NAME = "prenomClient"> <BR>
Votre sexe (m/f) : <INPUT NAME = "sexeClient" SIZE ="1"> <BR>
<INPUT TYPE = "submit" VALUE = "OK">
</FORM>"""

Le suivant sera nommé sessionTest2.py. C'est le script mentionné dans la balise d'ouverture du formulaire ci-dessus à la ligne 6, et qui sera invoqué lorsque l'utilisateur actionnera le bouton mis en place à la ligne 10. Ce script recevra les valeurs entrées par l'utilisateur dans les différents champs du formulaire, par l'intermédiaire d'un dictionnaire de requête situé dans la variable d'environnement QUERY de Karrigell[4] :

obSess = Session()

obSess.nom = QUERY["nomClient"]
obSess.prenom = QUERY["prenomClient"]
obSess.sexe = QUERY["sexeClient"]

if obSess.sexe.upper() == "M":
    vedette ="Monsieur"
else:
    vedette ="Madame"
print "<H3> Bienvenue, %s %s </H3>" % (vedette, obSess.nom)
print "<HR>"
print """
<a href = "sessionTest3.py"> Suite...</a>"""

La première ligne de ce script crée l'objet-session, génère pour lui un identifiant unique, et expédie celui-ci au navigateur sous la forme d'un cookie.

Dans les lignes 3, 4, 5, on récupère les valeurs entrées dans les champs du formulaire précédent, en utilisant leurs noms comme clés d'accès au dictionnaire de requêtes.

La ligne 14 définit un lien http pointant vers le troisième script, nommé sessionTest3.py :

suiviSess = Session()           # retrouver l'objet-session
suiviSess.article = 12345       # lui ajouter des attributs
suiviSess.prix = 43.67

print """
<H3> Page suivante </H3> <HR>
Suivi de la commande du client : <BR> %s %s <BR>
Article n° %s, Prix : %s <HR>
""" % (suiviSess.prenom, suiviSess.nom,
       suiviSess.article, suiviSess.prix)

Dirigez votre navigateur web vers l'adresse : http://localhost:8080/sessionTest1. Entrez des valeurs de votre choix dans les champs du formulaire, et cliquez sur le bouton OK :

 
formulaire rempli
 
Page affichant un bienvenue en fonction du nom entré dans le formulaire

Comme attendu, les informations entrées dans le formulaire sont transmises à la deuxième page. À présent, si vous cliquez sur le lien : « Suite... » dans celle-ci, vous dirigez encore une fois votre navigateur vers une nouvelle page, mais celle-ci n'aura fait l'objet d'aucune transmission de données (puisqu'on n'y accède pas par l'intermédiaire d'un formulaire). Dans le script sessionTest3.py qui génère cette page, vous ne pouvez donc pas utiliser la variable QUERY pour retrouver les informations entrées par le visiteur.

C'est ici qu'intervient le mécanisme des objets-sessions. Lors du lancement de ce troisième script, le cookie mémorisé par le navigateur est relu par le serveur, ce qui lui permet de régénérer l'objet-session créé dans le script précédent.

 
page affichant les informations entrées dans le formulaire au moyen d'un cookie

Analysez les trois premières lignes du script sessionTest3.py : l'objet suiviSess instancié à partit de la classe Session() est l'objet-session régénéré. Il contient les informations sauvegardées à la page précédente, et on peut lui en ajouter d'autres dans des attributs supplémentaires.

Vous aurez compris que vous pouvez désormais récupérer toutes ces informations de la même manière dans n'importe quelle autre page, car elles persisteront jusqu'à ce que l'utilisateur termine sa visite du site, à moins que vous ne fermiez vous-même cette session par programme, à l'aide de la méthode close() évoquée plus haut.

Exercices

  1. Ajoutez au script précédent un lien vers une quatrième page, et écrivez le script qui générera celle-ci. Les informations devront cette fois être affichées dans un tableau :
    Nom Prénom Sexe Article Prix

Solution

  1. Réfléchissez !

Autres développements

modifier

Nous terminons ici cette brève étude de Karrigell, car il nous semble vous avoir expliqué l'essentiel de ce qu'il vous faut connaître pour démarrer. Si vous désirez en savoir davantage, il vous suffira de consulter la documentation et les exemples fournis avec le produit. Comme nous l'avons déjà signalé plus haut, l'installation de Karrigell inclut l'installation du système de bases de données Gadfly. Vous pouvez donc très rapidement et très aisément réaliser un site interactif permettant la consultation à distance d'un ensemble de données quelconques, en admettant bien entendu que la charge de requêtes de votre site reste modérée, et que la taille de la base de données elle-même ne devienne pas gigantesque. N'espérez pas gérer à l'aide de Karrigell un site commercial susceptible de traiter plusieurs millions de requêtes journalières !

Si vous ambitionnez de réaliser ce genre de choses, il vous faudra étudier d'autres offres logicielles, comme par exemple CherryPy ou Zope associés à Apache pour le système serveur, et SQLite, MySQL ou PostgreSQL pour le gestionnaire de bases de données.

  1. Voyez le chapitre précédent : Gadfly est un serveur de bases de données écrit en Python.
  2. Si vous avez laissé en place le n° de port par défaut (80), il est inutile de le rappeler dans les adresses, puisque c'est ce n° de port qui est utilisé par défaut par la plupart des navigateurs. Une autre convention consiste à considérer que la page d'accueil d'un site Web se trouve presque toujours dans un fichier nommé index.htm ou index.html, Lorsque l'on souhaite visiter un site Web en commençant par sa page d'accueil, on peut donc en général omettre ce nom dans l'adresse. Karrigell respecte cette convention, et vous pouvez donc vous connecter en utilisant une adresse simplifiée telle que : http://localhost:8080 ou même : http://localhost (si le n° de port est 80).
  3. ...ou bien dans des sous-répertoires du répertoire racine, comme il est d'usage de le faire lorsque l'on cherche à structurer convenablement le site en construction. Il vous suffira dans ce cas d'inclure le nom de ces sous-répertoires dans les adresses correspondantes.
  4. Karrigell met en place un certain nombre de variables globales dont les noms sont en majuscules pour éviter un conflit éventuel avec les vôtres. Celle-ci joue le même rôle que la fonction FieldStorage() du module cgi. Veuillez consulter la documentation de Karrigell si vous souhaitez obtenir des explications plus détaillées.


Réseau

Communication entre programmes

modifier

Le développement extraordinaire de l'internet a amplement démontré que les ordinateurs peuvent être des outils de communication très efficaces. Dans ce chapitre, nous allons expérimenter la plus simple des techniques d'interconnexion de deux programmes, qui leur permette de s'échanger des informations par l'intermédiaire d'un réseau.

Pour ce qui va suivre, nous supposerons donc que vous collaborez avec un ou plusieurs de vos condisciples, et que vos postes de travail Python sont connectés à un réseau local dont les communications utilisent le protocole TCP/IP. Le système d'exploitation n'a pas d'importance : vous pouvez par exemple installer l'un des scripts Python décrits ci-après sur un poste de travail fonctionnant sous Linux, et le faire dialoguer avec un autre script mis en œuvre sur un poste de travail confié aux bons soins d'un système d'exploitation différent, tel que MacOS ou Windows.

Vous pouvez également expérimenter ce qui suit sur une seule et même machine, en mettant les différents scripts en œuvre dans des fenêtres indépendantes.

Les sockets

modifier

Le premier exercice qui va vous être proposé consistera à établir une communication entre deux machines seulement. L'une et l'autre pourront s'échanger des messages à tour de rôle, mais vous constaterez cependant que leurs configurations ne sont pas symétriques. Le script installé sur l'une de ces machines jouera en effet le rôle d'un logiciel serveur, alors que l'autre se comportera comme un logiciel client.

Le logiciel serveur fonctionne en continu, sur une machine dont l'identité est bien définie sur le réseau grâce à une adresse IP spécifique[1]. Il guette en permanence l'arrivée de requêtes expédiées par les clients potentiels en direction de cette adresse, par l'intermédiaire d'un port de communication bien déterminé. Pour ce faire, le script correspondant doit mettre en œuvre un objet logiciel associé à ce port, que l'on appelle un socket.

Au départ d'une autre machine, le logiciel client tente d'établir la connexion en émettant une requête appropriée. Cette requête est un message qui est confié au réseau, un peu comme on confie une lettre à la Poste. Le réseau pourrait en effet acheminer la requête vers n'importe quelle autre machine, mais une seule est visée : pour que la destination visée puisse être atteinte, la requête contient dans son en-tête l'indication de l'adresse IP et du port de communication destinataires.

Lorsque la connexion est établie avec le serveur, le client lui assigne lui-même l'un de ses propres ports de communication. À partir de ce moment, on peut considérer qu'un canal privilégié relie les deux machines, comme si on les avait connectées l'une à l'autre par l'intermédiaire d'un fil (les deux ports de communication respectifs jouant le rôle des deux extrémités de ce fil). L'échange d'informations proprement dit peut commencer.

Pour pouvoir utiliser les ports de communication réseau, les programmes font appel à un ensemble de procédures et de fonctions du système d'exploitation, par l'intermédiaire d'objets interfaces que l'on appelle des sockets. Ceux-ci peuvent mettre en œuvre deux techniques de communication différentes et complémentaires : celle des paquets (que l'on appelle aussi des datagrammes), très largement utilisée sur l'internet, et celle de la connexion continue, ou stream socket, qui est un peu plus simple.

Construction d'un serveur élémentaire

modifier

Pour nos premières expériences, nous allons utiliser la technique des stream sockets. Celle-ci est en effet parfaitement appropriée lorsqu'il s'agit de faire communiquer des ordinateurs interconnectés par l'intermédiaire d'un réseau local. C'est une technique particulièrement aisée à mettre en œuvre, et elle permet un débit élevé pour l'échange de données.

L'autre technologie (celle des paquets) serait préférable pour les communications expédiées via l'internet, en raison de sa plus grande fiabilité (les mêmes paquets peuvent atteindre leur destination par différents chemins, être émis ou ré-émis en plusieurs exemplaires si cela se révèle nécessaire pour corriger les erreurs de transmission), mais sa mise en œuvre est un peu plus complexe. Nous ne l'étudierons pas dans ce cours.

Le script ci-dessous met en place un serveur capable de communiquer avec un seul client :

Python 2

modifier
# Définition d'un serveur réseau rudimentaire
# Ce serveur attend la connexion d'un client, pour entamer un dialogue avec lui

import socket, sys

HOST = '192.168.14.152'
PORT = 50000

# 1) création du socket :
mySocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 2) liaison du socket à une adresse précise :
try:
    mySocket.bind((HOST, PORT))
except socket.error:
    print "La liaison du socket à l'adresse choisie a échoué."
    sys.exit()

while 1:
    # 3) Attente de la requête de connexion d'un client :
    print "Serveur prêt, en attente de requêtes ..."
    mySocket.listen(5)
    
    # 4) Etablissement de la connexion :
    connexion, adresse = mySocket.accept()
    print "Client connecté, adresse IP %s, port %s" % (adresse[0], adresse[1])
    
    # 5) Dialogue avec le client :
    connexion.send("Vous êtes connecté au serveur Marcel. Envoyez vos messages.")
    msgClient = connexion.recv(1024)
    while 1:
        print "C>", msgClient
        if msgClient.upper() == "FIN" or msgClient =="":
            break
        msgServeur = raw_input("S> ")
        connexion.send(msgServeur)
        msgClient = connexion.recv(1024)

    # 6) Fermeture de la connexion :
    connexion.send("Au revoir !")
    print "Connexion interrompue."
    connexion.close()

    ch = raw_input("<R>ecommencer <T>erminer ? ")
    if ch.upper() =='T':
        break

Version en python 3

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

# Définition d'un serveur réseau rudimentaire
# Ce serveur attend la connexion d'un client, pour entamer un dialogue avec lui

import socket, sys

HOST = 'localhost'
PORT = 50000

# 1) création du socket :
mySocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 2) liaison du socket à une adresse précise :
try:
    mySocket.bind((HOST, PORT))
except socket.error:
    print("La liaison du socket à l'adresse choisie a échoué.")
    sys.exit()

while 1:
    # 3) Attente de la requête de connexion d'un client :
    print("Serveur prêt, en attente de requêtes...")
    mySocket.listen(5)
    
    # 4) Etablissement de la connexion :
    connexion, adresse = mySocket.accept()
    print("Client connecté, adresse IP %s, port %s" % (adresse[0], adresse[1]))
    
    # 5) Dialogue avec le client :
    msgServeur = "Vous êtes connecté au serveur Marcel. Envoyez vos messages."
    connexion.send(msgServeur.encode("Utf8"))
    msgClient = connexion.recv(1024).decode("Utf8")
    while 1:
        print("C>", msgClient)
        if msgClient.upper() == "FIN" or msgClient =="":
            break
        msgServeur = input("S> ")
        connexion.send(msgServeur.encode("Utf8"))
        msgClient = connexion.recv(1024)

    # 6) Fermeture de la connexion :
    connexion.send("Au revoir !".encode("Utf8"))
    print("Connexion interrompue.")
    connexion.close()

    ch = input("<R>ecommencer <T>erminer ? ")
    if ch.upper() =='T':
        break

Commentaires (Python 2)

modifier
  • Ligne 4 : Le module socket contient toutes les fonctions et les classes nécessaires pour construire des programmes communiquants. Comme nous allons le voir dans les lignes suivantes, l'établissement de la communication comporte six étapes.
  • Lignes 6 et 7 : Ces deux variables définissent l'identité du serveur, telle qu'on l'intégrera au socket. HOST doit contenir une chaîne de caractères indiquant l'adresse IP du serveur sous la forme décimale habituelle, ou encore le nom DNS de ce même serveur (mais à la condition qu'un mécanisme de résolution des noms ait été mis en place sur le réseau). PORT doit contenir un entier, à savoir le numéro d'un port qui ne soit pas déjà utilisé pour un autre usage, et de préférence une valeur supérieure à 1024 (Cfr. votre cours sur les services réseau).
  • Lignes 9 et 10 : Première étape du mécanisme d'interconnexion. On instancie un objet de la classe socket(), en précisant deux options qui indiquent le type d'adresses choisi (nous utiliserons des adresses de type « internet ») ainsi que la technologie de transmission (datagrammes ou connexion continue (stream) : nous avons décidé d'utiliser cette dernière).
  • Lignes 12 à 17 : Seconde étape. On tente d'établir la liaison entre le socket et le port de communication. Si cette liaison ne peut être établie (port de communication occupé, par exemple, ou nom de machine incorrect), le programme se termine sur un message d'erreur.
    Remarque : la méthode bind() du socket attend un argument du type tuple, raison pour laquelle nous devons enfermer nos deux variables dans une double paire de parenthèses.
  • Ligne 19 : Notre programme serveur étant destiné à fonctionner en permanence dans l'attente des requêtes de clients potentiels, nous le lançons dans une boucle sans fin.
  • Lignes 20 à 22 : Troisième étape. Le socket étant relié à un port de communication, il peut à présent se préparer à recevoir les requêtes envoyées par les clients. C'est le rôle de la méthode listen(). L'argument qu'on lui transmet indique le nombre maximum de connexions à accepter en parallèle.
  • Lignes 24 à 26 : Quatrième étape. Lorsqu'on fait appel à sa méthode accept(), le socket attend indéfiniment qu'une requête se présente. Le script est donc interrompu à cet endroit, un peu comme il le serait si nous faisions appel à une fonction input() pour attendre une entrée clavier. Si une requête est réceptionnée, la méthode accept() renvoie un tuple de deux éléments : le premier est la référence d'un nouvel objet de la classe socket()[2], qui sera la véritable interface de communication entre le client et le serveur, et le second un autre tuple contenant les coordonnées de ce client (son adresse IP et le n° de port qu'il utilise lui-même).
  • Lignes 28 à 30 : Cinquième étape. La communication proprement dite est établie. Les méthodes send() et recv() du socket servent évidemment à l'émission et à la réception des messages, qui doivent être de simples chaînes de caractères.
    Remarques : la méthode send() renvoie le nombre d'octets expédiés. L'appel de la méthode recv() doit comporter un argument entier indiquant le nombre maximum d'octets à réceptionner en une fois (Les octets surnuméraires sont mis en attente dans un tampon. Ils sont transmis lorsque la même méthode recv() est appelée à nouveau).
  • Lignes 31 à 37 : Cette nouvelle boucle sans fin maintient le dialogue jusqu'à ce que le client décide d'envoyer le mot « fin » ou une simple chaîne vide. Les écrans des deux machines afficheront chacune l'évolution de ce dialogue.
  • Lignes 39 à 42 : Sixième étape. Fermeture de la connexion.

Construction d'un client rudimentaire

modifier

Le script ci-dessous définit un logiciel client complémentaire du serveur décrit dans les pages précédentes. On notera sa grande simplicité.

En python 2

modifier
# Définition d'un client réseau rudimentaire
# Ce client dialogue avec un serveur ad hoc

import socket, sys

HOST = '192.168.14.152'
PORT = 50000

# 1) création du socket :
mySocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 2) envoi d'une requête de connexion au serveur :
try:
    mySocket.connect((HOST, PORT))
except socket.error:
    print "La connexion a échoué."
    sys.exit()    
print "Connexion établie avec le serveur."    

# 3) Dialogue avec le serveur :
msgServeur = mySocket.recv(1024)

while 1:
    if msgServeur.upper() == "FIN" or msgServeur =="":
        break
    print "S>", msgServeur
    msgClient = raw_input("C> ")
    mySocket.send(msgClient)
    msgServeur = mySocket.recv(1024)

# 4) Fermeture de la connexion :
print "Connexion interrompue."
mySocket.close()

En Python 3

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

# Définition d'un client réseau rudimentaire
# Ce client dialogue avec un serveur ad hoc

import socket, sys

HOST = '192.168.14.152'
HOST = "localhost"
PORT = 50000

# 1) création du socket :
mySocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 2) envoi d'une requête de connexion au serveur :
try:
    mySocket.connect((HOST, PORT))
except socket.error:
    print("La connexion a échoué.")
    sys.exit()    
print("Connexion établie avec le serveur.")    

# 3) Dialogue avec le serveur :
msgServeur = mySocket.recv(1024)

while 1:
    if msgServeur.upper() == "FIN" or msgServeur =="":
        break
    print("S>", msgServeur)
    msgClient = input("C> ")
    mySocket.send(msgClient.encode("Utf8"))
    msgServeur = mySocket.recv(1024).decode("Utf8")

# 4) Fermeture de la connexion :
print("Connexion interrompue.")
mySocket.close()

Commentaires (Python 2)

modifier
  • Le début du script est similaire à celui du serveur. L'adresse IP et le port de communication doivent être ceux du serveur.
  • Lignes 12 à 18 : On ne crée cette fois qu'un seul objet socket, dont on utilise la méthode connect() pour envoyer la requête de connexion.
  • Lignes 20 à 33 : Une fois la connexion établie, on peut dialoguer avec le serveur en utilisant les méthodes send() et recv() déjà décrites plus haut pour celui-ci.

Récupérer une page Web en python

modifier

Python intègre le module httplib[3] qui permet d'émettre et de recevoir des requêtes HTTP.

Afficher une page Web

modifier

Le code suivant (src) permet de récupérer, à l'aide d'une requête HTTP, une page Web et affiche son code source à l'écran.

Exemple d'appel HTTP GET
# On utilise le module httplib
import httplib

# Connexion au proxy 
# (si vous n'être pas derrière un proxy, alors mettre directement 'fr.wikibooks.org')
conn = httplib.HTTP('proxy:3128')

# Requête GET 
# (si vous n'être pas derrière un proxy, alors mettre directement
# '/w/index.php?title=Programmation_Python_Le_r%C3%A9seau&action=edit'
conn.putrequest('GET', 'http://fr.wikibooks.org/w/index.php?title=Programmation_Python_Le_r%C3%A9seau&action=edit')

conn.putheader('Accept', 'text/html')
conn.putheader('Accept', 'text/plain')

# Décommenter les 2 lignes suivantes si votre proxy nécessite une authentification
# auth = "Basic " + "username:password".encode('base64')
# h1.putheader('Proxy-Authorization', auth)

conn.endheaders()

# Récupération de la réponse
errcode, errmsg, headers = conn.getreply()


# Affichage d'éventuelles erreurs
print errcode
print errmsg
print headers

# Affichage de la réponse ligne après ligne
f=conn.getfile()
for line in f:
    print line

# fin de la connexion
conn.close()

Références

modifier
  1. Une machine particulière peut également être désignée par un nom plus explicite, mais à la condition qu'un mécanisme ait été mis en place sur le réseau (DNS) pour traduire automatiquement ce nom en adresse IP. Veuillez consulter votre cours sur les systèmes d'exploitation et les réseaux pour en savoir davantage.
  2. . En bref, si nous voulons que notre serveur puisse prendre en charge simultanément les connexions de plusieurs clients, il nous faudra disposer d'un socket distinct pour chacun d'eux, indépendamment du premier que l'on laissera fonctionner en permanence pour réceptionner les requêtes qui continuent à arriver en provenance de nouveaux clients.
  3. http://docs.python.org/lib/module-httplib.html


Threads

Gestion de plusieurs tâches en parallèle à l'aide des threads

modifier

Le système de communication que nous avons élaboré dans les pages précédentes est vraiment très rudimentaire : d'une part il ne met en relation que deux machines seulement, et d'autre part il limite la liberté d'expression des deux interlocuteurs. Ceux-ci ne peuvent en effet envoyer des messages que chacun à leur tour. Par exemple, lorsque l'un d'eux vient d'émettre un message, son système reste bloqué tant que son partenaire ne lui a pas envoyé une réponse. Lorsqu'il vient de recevoir une telle réponse, son système reste incapable d'en réceptionner une autre, tant qu'il n'a pas entré lui-même un nouveau message, ... et ainsi de suite.

Tous ces problèmes proviennent du fait que nos scripts habituels ne peuvent s'occuper que d'une seule chose à la fois. Lorsque le flux d'instructions rencontre une fonction input(), par exemple, il ne se passe plus rien tant que l'utilisateur n'a pas introduit la donnée attendue. Et même si cette attente dure très longtemps, il n'est habituellement pas possible que le programme effectue d'autres tâches pendant ce temps. Ceci n'est toutefois vrai qu'au sein d'un seul et même programme : vous savez certainement que vous pouvez exécuter d'autres applications entre-temps sur votre ordinateur, car les systèmes d'exploitation modernes sont multi-tâches.

Les pages qui suivent sont destinées à vous expliquer comment vous pouvez introduire cette fonctionnalité multi-tâches dans vos programmes, afin que vous puissiez développer de véritables applications réseau, capables de communiquer simultanément avec plusieurs partenaires.

Veuillez à présent considérer le script de la page précédente. Sa fonctionnalité essentielle réside dans la boucle while des lignes 23 à 29. Or, cette boucle s'interrompt à deux endroits :

  • À la ligne 27, pour attendre les entrées clavier de l'utilisateur (fonction raw_input()) ;
  • À la ligne 29, pour attendre l'arrivée d'un message réseau.

Ces deux attentes sont donc successives, alors qu'il serait bien plus intéressant qu'elles soient simultanées. Si c'était le cas, l'utilisateur pourrait expédier des messages à tout moment, sans devoir attendre à chaque fois la réaction de son partenaire. Il pourrait également recevoir n'importe quel nombre de messages, sans l'obligation d'avoir à répondre à chacun d'eux pour recevoir les autres.

Nous pouvons arriver à ce résultat si nous apprenons à gérer plusieurs séquences d'instructions en parallèle au sein d'un même programme. Mais comment cela est-il possible ?

Au cours de l'histoire de l'informatique, plusieurs techniques ont été mises au point pour partager le temps de travail d'un processeur entre différentes tâches, de telle manière que celles-ci paraissent être effectuées en même temps (alors qu'en réalité le processeur s'occupe d'un petit bout de chacune d'elles à tour de rôle). Ces techniques sont implémentées dans le système d'exploitation, et il n'est pas nécessaire de les détailler ici, même s'il est possible d'accéder à chacune d'elles avec Python.

Dans les pages suivantes, nous allons apprendre à utiliser celle de ces techniques qui est à la fois la plus facile à mettre en œuvre, et la seule qui soit véritablement portable (elle est en effet supportée par tous les grands systèmes d'exploitation) : on l'appelle la technique des processus légers ou threads[1].

Dans un programme d'ordinateur, les threads sont des flux d'instructions qui sont menés en parallèle (quasi-simultanément), tout en partageant le même espace de noms global.

En fait, le flux d'instructions de n'importe quel programme Python suit toujours au moins un thread : le thread principal.

À partir de celui-ci, d'autres threads « enfants » peuvent être amorcés, qui seront exécutés en parallèle. Chaque thread enfant se termine et disparaît sans autre forme de procès lorsque toutes les instructions qu'il contient ont été exécutées. Par contre, lorsque le thread principal se termine, il faut parfois s'assurer que tous ses threads enfants « meurent » avec lui.

Client gérant l'émission et la réception simultanées

modifier

Nous allons maintenant mettre en pratique la technique des threads pour construire un système de « chat »[2] simplifié. Ce système sera constitué d'un seul serveur et d'un nombre quelconque de clients. Contrairement à ce qui se passait dans notre premier exercice, personne n'utilisera le serveur lui-même pour communiquer, mais lorsque celui-ci aura été mis en route, plusieurs clients pourront s'y connecter et commencer à s'échanger des messages.

Chaque client enverra tous ses messages au serveur, mais celui-ci les ré-expédiera immédiatement à tous les autres clients connectés, de telle sorte que chacun puisse voir l'ensemble du trafic. Chacun pourra à tout moment envoyer ses messages, et recevoir ceux des autres, dans n'importe quel ordre, la réception et l'émission étant gérées simultanément, dans des threads séparés.

Le script ci-après définit le programme client. Vous constaterez que la partie principale du script (ligne 38 et suivantes) est similaire à celle de l'exemple précédent. Seule la partie « Dialogue avec le serveur » a été remplacée. Au lieu d'une boucle while, vous y trouvez à présent les instructions de création de deux objets threads (aux lignes 49 et 50), dont on démarre la fonctionnalité aux deux lignes suivantes. Ces objets threads sont créés par dérivation, à partir de la classe Thread() du module threading. Ils s'occuperont indépendamment de la réception et de l'émission des messages. Les deux threads « enfants » sont ainsi parfaitement encapsulés dans des objets distincts, ce qui facilite la compréhension du mécanisme.

Python 2

modifier
# Définition d'un client réseau gérant en parallèle l'émission
# et la réception des messages (utilisation de 2 THREADS).

host = '192.168.0.235'
port = 40000

import socket, sys, threading

class ThreadReception(threading.Thread):
    """objet thread gérant la réception des messages"""
    def __init__(self, conn):
        threading.Thread.__init__(self)
        self.connexion = conn           # réf. du socket de connexion
        
    def run(self):
        while 1:
            message_recu = self.connexion.recv(1024)
            print "*" + message_recu + "*"
            if message_recu =='' or message_recu.upper() == "FIN":
                break
        # Le thread <réception> se termine ici.
        # On force la fermeture du thread <émission> :
        th_E._Thread__stop()
        print "Client arrêté. Connexion interrompue."
        self.connexion.close()
    
class ThreadEmission(threading.Thread):
    """objet thread gérant l'émission des messages"""
    def __init__(self, conn):
        threading.Thread.__init__(self)
        self.connexion = conn           # réf. du socket de connexion
        
    def run(self):
        while 1:
            message_emis = raw_input()
            self.connexion.send(message_emis)

# Programme principal - Établissement de la connexion :
connexion = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
    connexion.connect((host, port))
except socket.error:
    print "La connexion a échoué."
    sys.exit()    
print "Connexion établie avec le serveur."
            
# Dialogue avec le serveur : on lance deux threads pour gérer
# indépendamment l'émission et la réception des messages :
th_E = ThreadEmission(connexion)
th_R = ThreadReception(connexion)
th_E.start()
th_R.start()

Python 3

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

# Définition d'un client réseau gérant en parallèle l'émission
# et la réception des messages (utilisation de 2 THREADS).

import socket, sys, threading

HOST = 'localhost'
PORT = 50000

class ThreadReception(threading.Thread):
    """objet thread gérant la réception des messages"""
    def __init__(self, conn):
        threading.Thread.__init__(self)
        self.connexion = conn           # réf. du socket de connexion
        
    def run(self):
        while 1:
            message_recu = self.connexion.recv(1024).decode("Utf8")
            print("*" + message_recu + "*")
            if message_recu =='' or message_recu.upper() == "FIN":
                break
        # Le thread <réception> se termine ici.
        # On force la fermeture du thread <émission> :
        th_E._Thread__stop()
        print("Client arrêté. Connexion interrompue.")
        self.connexion.close()
    
class ThreadEmission(threading.Thread):
    """objet thread gérant l'émission des messages"""
    def __init__(self, conn):
        threading.Thread.__init__(self)
        self.connexion = conn           # réf. du socket de connexion
        
    def run(self):
        while 1:
            message_emis = input()
            self.connexion.send(message_emis.encode("Utf8"))

# Programme principal - Établissement de la connexion :
connexion = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
    connexion.connect((HOST, PORT))
except socket.error:
    print("La connexion a échoué.")
    sys.exit()    
print("Connexion établie avec le serveur.")
            
# Dialogue avec le serveur : on lance deux threads pour gérer
# indépendamment l'émission et la réception des messages :
th_E = ThreadEmission(connexion)
th_R = ThreadReception(connexion)
th_E.start()
th_R.start()

Commentaires (python 2)

modifier

Remarque générale : Dans cet exemple, nous avons décidé de créer deux objets threads indépendants du thread principal, afin de bien mettre en évidence les mécanismes. Notre programme utilise donc trois threads en tout, alors que le lecteur attentif aura remarqué que deux pourraient suffire. En effet : le thread principal ne sert en définitive qu'à lancer les deux autres ! Il n'y a cependant aucun intérêt à limiter le nombre de threads. Au contraire : à partir du moment où l'on décide d'utiliser cette technique, il faut en profiter pour compartimenter l'application en unités bien distinctes.

Ligne 7 : Le module threading contient la définition de toute une série de classes intéressantes pour gérer les threads. Nous n'utiliserons ici que la seule classe Thread(), lorsque nous devrons nous préoccuper de problèmes de synchronisation entre différents threads concurrents.

Lignes 9 à 25 : Les classes dérivées de la classe Thread() contiendront essentiellement une méthode run(). C'est dans celle-ci que l'on placera la portion de programme spécifiquement confiée au thread. Il s'agira souvent d'une boucle répétitive, comme ici. Vous pouvez parfaitement considérer le contenu de cette méthode comme un script indépendant, qui s'exécute en parallèle avec les autres composants de votre application. Lorsque ce code a été complètement exécuté, le thread se referme.

Lignes 16 à 20 : Cette boucle gère la réception des messages. À chaque itération, le flux d'instructions s'interrompt à la ligne 17 dans l'attente d'un nouveau message, mais le reste du programme n'est pas figé pour autant : les autres threads continuent leur travail indépendamment.

Ligne 19 : La sortie de boucle est provoquée par la réception d'un message 'fin' (en majuscules ou en minuscules), ou encore d'un message vide (c'est notamment le cas si la connexion est coupée par le partenaire). Quelques instructions de « nettoyage » sont alors exécutées, et puis le thread se termine.

Ligne 23 : Lorsque la réception des messages est terminée, nous souhaitons que le reste du programme se termine lui aussi. Il nous faut donc forcer la fermeture de l'autre objet thread, celui que nous avons mis en place pour gérer l'émission des messages. Cette fermeture forcée peut être obtenue à l'aide de la méthode _Thread__stop()[3].

Lignes 27 à 36 : Cette classe définit donc un autre objet thread, qui contient cette fois une boucle de répétition perpétuelle. Il ne pourra donc se terminer que contraint et forcé par méthode décrite au paragraphe précédent. À chaque itération de cette boucle, le flux d'instructions s'interrompt à la ligne 35 dans l'attente d'une entrée clavier, mais cela n'empêche en aucune manière les autres threads de faire leur travail.

Lignes 38 à 45 : Ces lignes sont reprises à l'identique des scripts précédents.

Lignes 47 à 52 : Instanciation et démarrage des deux objets threads « enfants ». Veuillez noter qu'il est recommandé de provoquer ce démarrage en invoquant la méthode intégrée start(), plutôt qu'en faisant appel directement à la méthode run() que vous aurez définie vous-même. Sachez également que vous ne pouvez invoquer start() qu'une seule fois (une fois arrêté, un objet thread ne peut pas être redémarré).

Serveur gérant les connexions de plusieurs clients en parallèle

modifier

Le script ci-après crée un serveur capable de prendre en charge les connexions d'un certain nombre de clients du même type que ce que nous avons décrit dans les pages précédentes.

Ce serveur n'est pas utilisé lui-même pour communiquer : ce sont les clients qui communiquent les uns avec les autres, par l'intermédiaire du serveur. Celui-ci joue donc le rôle d'un relais : il accepte les connexions des clients, puis attend l'arrivée de leurs messages. Lorsqu'un message arrive en provenance d'un client particulier, le serveur le ré-expédie à tous les autres, en lui ajoutant au passage une chaîne d'identification spécifique du client émetteur, afin que chacun puisse voir tous les messages, et savoir de qui ils proviennent.

Python 2

modifier
# Définition d'un serveur réseau gérant un système de CHAT simplifié.
# Utilise les threads pour gérer les connexions clientes en parallèle.

HOST = '192.168.0.235'
PORT = 40000

import socket, sys, threading

class ThreadClient(threading.Thread):
    '''dérivation d'un objet thread pour gérer la connexion avec un client'''
    def __init__(self, conn):
        threading.Thread.__init__(self)
        self.connexion = conn
        
    def run(self):
        # Dialogue avec le client :
        nom = self.getName()        # Chaque thread possède un nom
        while 1:
            msgClient = self.connexion.recv(1024)
            if msgClient.upper() == "FIN" or msgClient =="":
                break
            message = "%s> %s" % (nom, msgClient)
            print message
            # Faire suivre le message à tous les autres clients :
            for cle in conn_client:
                if cle != nom:      # ne pas le renvoyer à l'émetteur
                    conn_client[cle].send(message)
                    
        # Fermeture de la connexion :
        self.connexion.close()      # couper la connexion côté serveur
        del conn_client[nom]        # supprimer son entrée dans le dictionnaire
        print "Client %s déconnecté." % nom
        # Le thread se termine ici    

# Initialisation du serveur - Mise en place du socket :
mySocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
    mySocket.bind((HOST, PORT))
except socket.error:
    print "La liaison du socket à l'adresse choisie a échoué."
    sys.exit()
print "Serveur prêt, en attente de requêtes ..."
mySocket.listen(5)

# Attente et prise en charge des connexions demandées par les clients :
conn_client = {}                # dictionnaire des connexions clients
while 1:    
    connexion, adresse = mySocket.accept()
    # Créer un nouvel objet thread pour gérer la connexion :
    th = ThreadClient(connexion)
    th.start()
    # Mémoriser la connexion dans le dictionnaire : 
    it = th.getName()        # identifiant du thread
    conn_client[it] = connexion
    print "Client %s connecté, adresse IP %s, port %s." %\
           (it, adresse[0], adresse[1])
    # Dialogue avec le client :
    connexion.send("Vous êtes connecté. Envoyez vos messages.")

Python 3

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

# Définition d'un serveur réseau gérant un système de CHAT simplifié.
# Utilise les threads pour gérer les connexions clientes en parallèle.

HOST = 'localhost'
PORT = 50000

import socket, sys, threading

class ThreadClient(threading.Thread):
    '''dérivation d'un objet thread pour gérer la connexion avec un client'''
    def __init__(self, conn):
        threading.Thread.__init__(self)
        self.connexion = conn
        
    def run(self):
        # Dialogue avec le client :
        nom = self.getName()        # Chaque thread possède un nom
        while 1:
            msgClient = self.connexion.recv(1024).decode("Utf8")
            if msgClient.upper() == "FIN" or msgClient =="":
                break
            message = "%s> %s" % (nom, msgClient)
            print(message)
            # Faire suivre le message à tous les autres clients :
            for cle in conn_client:
                if cle != nom:      # ne pas le renvoyer à l'émetteur
                    conn_client[cle].send(message.encode("Utf8"))
                    
        # Fermeture de la connexion :
        self.connexion.close()      # couper la connexion côté serveur
        del conn_client[nom]        # supprimer son entrée dans le dictionnaire
        print("Client %s déconnecté." % nom)
        # Le thread se termine ici    

# Initialisation du serveur - Mise en place du socket :
mySocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
    mySocket.bind((HOST, PORT))
except socket.error:
    print("La liaison du socket à l'adresse choisie a échoué.")
    sys.exit()
print("Serveur prêt, en attente de requêtes ...")
mySocket.listen(5)

# Attente et prise en charge des connexions demandées par les clients :
conn_client = {}                # dictionnaire des connexions clients
while 1:    
    connexion, adresse = mySocket.accept()
    # Créer un nouvel objet thread pour gérer la connexion :
    th = ThreadClient(connexion)
    th.start()
    # Mémoriser la connexion dans le dictionnaire : 
    it = th.getName()        # identifiant du thread
    conn_client[it] = connexion
    print("Client %s connecté, adresse IP %s, port %s." %\
           (it, adresse[0], adresse[1]))
    # Dialogue avec le client :
    connexion.send("Vous êtes connecté. Envoyez vos messages.".encode("Utf8"))

Commentaires (Python 2)

modifier

Lignes 35 à 43 : L'initialisation de ce serveur est identique à celle du serveur rudimentaire décrit au début du présent chapitre.

Ligne 46 : Les références des différentes connexions doivent être mémorisées. Nous pourrions les placer dans une liste, mais il est plus judicieux de les placer dans un dictionnaire, pour deux raisons : La première est que nous devrons pouvoir ajouter ou enlever ces références dans n'importe quel ordre, puisque les clients se connecteront et se déconnecteront à leur guise. La seconde est que nous pouvons disposer aisément d'un identifiant unique pour chaque connexion, lequel pourra servir de clé d'accès dans un dictionnaire. Cet identifiant nous sera en effet fourni automatiquement par La classe Thread().

Lignes 47 à 51 : Le programme commence ici une boucle de répétition perpétuelle, qui va constamment attendre l'arrivée de nouvelles connexions. Pour chacune de celles-ci, un nouvel objet ThreadClient() est créé, lequel pourra s'occuper d'elle indépendamment de toutes les autres.

Lignes 52 à 54 : Obtention d'un identifiant unique à l'aide de la méthode getName(). Nous pouvons profiter ici du fait que Python attribue automatiquement un nom unique à chaque nouveau thread : ce nom convient bien comme identifiant (ou clé) pour retrouver la connexion correspondante dans notre dictionnaire. Vous pourrez constater qu'il s'agit d'une chaîne de caractères, de la forme : « Thread-N » (N étant le numéro d'ordre du thread).

Lignes 15 à 17 : Gardez bien à l'esprit qu'il se créera autant d'objets ThreadClient() que de connexions, et que tous ces objets fonctionneront en parallèle. La méthode getName() peut alors être utilisée au sein de l'un quelconque de ces objets pour retrouver son identité particulière. Nous utiliserons cette information pour distinguer la connexion courante de toutes les autres (voir ligne 26).

Lignes 18 à 23 : L'utilité du thread est de réceptionner tous les messages provenant d'un client particulier. Il faut donc pour cela une boucle de répétition perpétuelle, qui ne s'interrompra qu'à la réception du message spécifique : « fin », ou encore à la réception d'un message vide (cas où la connexion est coupée par le partenaire).

Lignes 24 à 27 : Chaque message reçu d'un client doit être ré-expédié à tous les autres. Nous utilisons ici une boucle for pour parcourir l'ensemble des clés du dictionnaire des connexions, lesquelles nous permettent ensuite de retrouver les connexions elles-mêmes. Un simple test (à la ligne 26) nous évite de ré-expédier le message au client dont il provient.

Ligne 31 : Lorsque nous fermons un socket de connexion, il est préférable de supprimer sa référence dans le dictionnaire, puisque cette référence ne peut plus servir. Et nous pouvons faire cela sans précaution particulière, car les éléments d'un dictionnaire ne sont pas ordonnés (nous pouvons en ajouter ou en enlever dans n'importe quel ordre).

Jeu des bombardes, version réseau

modifier

Au chapitre 15, nous avons commenté le développement d'un petit jeu de combat dans lequel des joueurs s'affrontaient à l'aide de bombardes. L'intérêt de ce jeu reste toutefois fort limité, tant qu'il se pratique sur un seul et même ordinateur. Nous allons donc le perfectionner, en y intégrant les techniques que nous venons d'apprendre. Comme le système de « chat » décrit dans les pages précédentes, l'application complète se composera désormais de deux programmes distincts : un logiciel serveur qui ne sera mis en fonctionnement que sur une seule machine, et un logiciel client qui pourra être lancé sur toute une série d'autres. Du fait du caractère portable de Python, il vous sera même possible d'organiser des combats de bombardes entre ordinateurs gérés par des systèmes d'exploitation différents (MacOS <> Linux <> Windows !).

 
capture d'écran du jeu des bombardes

Programme serveur : vue d'ensemble

modifier

Les programmes serveur et client exploitent la même base logicielle, elle-même largement récupérée de ce qui avait déjà été mis au point tout au long du chapitre 15. Nous admettrons donc pour la suite de cet exposé que les deux versions précédentes du jeu ont été sauvegardées dans les fichiers-modules canon03.py et canon04.py, installés dans le répertoire courant. Nous pouvons en effet réutiliser une bonne partie du code qu'ils contiennent, en nous servant judicieusement de l'importation et de l'héritage de classes.

Du module canon04, nous allons réutiliser la classe Canon() telle quelle, aussi bien pour le logiciel serveur que pour le logiciel client. De ce même module, nous importerons également la classe AppBombardes(), dont nous ferons dériver la classe maîtresse de notre application serveur : AppServeur().

Du module canon03, nous récupérerons la classe Pupitre() dont nous tirerons une version plus adaptée au « contrôle à distance ».

Enfin, deux nouvelles classes viendront s'ajouter aux précédentes, chacune spécialisée dans la création d'un objet thread : la classe ThreadClients(), dont une instance surveillera en permanence le socket destiné à réceptionner les demandes de connexion de nouveaux clients, et la classe ThreadConnexion(), qui servira à créer autant d'objets sockets que nécessaire pour assurer le dialogue avec chacun des clients déjà connectés.

Ces nouvelles classes seront inspirées de celles que nous avions développées pour notre serveur de chat dans les pages précédentes. La principale différence par rapport à celui-ci est que nous devrons activer un thread spécifique pour le code qui gère l'attente et la prise en charge des connexions clientes, afin que l'application principale puisse faire autre chose pendant ce temps.

À partir de là, notre plus gros travail consistera à développer un protocole de communication pour le dialogue entre le serveur et ses clients. De quoi est-il question ? Tout simplement de définir la teneur des messages que vont s'échanger les machines connectées. Rassurez-vous : la mise au point de ce « langage » peut être progressive. On commence par établir un dialogue de base, puis on y ajoute petit à petit un « vocabulaire » plus étendu.

L'essentiel de ce travail peut être accompli en s 'aidant du logiciel client développé précédemment pour le système de chat. On se sert de celui-ci pour envoyer des « ordres » au serveur en cours de développement, et on corrige celui-ci jusqu'à ce qu'il « obéisse » : en clair, les procédures que l'on met en place progressivement sur le serveur sont testées au fur et à mesure, en réponse aux messages correspondants émis « à la main » à partir du client.

Protocole de communication

modifier

Il va de soi que le protocole décrit ci-après est tout à fait arbitraire. Il serait parfaitement possible de choisir d'autres conventions complètement différentes. Vous pouvez bien évidemment critiquer les choix effectués, et vous souhaiterez peut-être même les remplacer par d'autres, plus efficients ou plus simples.

Vous savez déjà que les messages échangés sont de simples chaînes de caractères. Prévoyant que certains de ces messages devront transmettre plusieurs informations à la fois, nous avons décidé que chacun d'eux pourrait comporter plusieurs champs, que nous séparerons à l'aide de virgules. Lors de la réception de l'un quelconque de ces messages, nous pourrons alors aisément récupérer tous ses composants dans une liste, à l'aide de la méthode intégrée split().

Voici un exemple de dialogue type, tel qu'il peut être suivi du côté d'un client. Les messages entre astérisques sont ceux qui sont reçus du serveur ; les autres sont ceux qui sont émis par le client lui-même :

*serveur OK*
client OK
*canons,Thread-3;104;228;1;dark red,Thread-2;454;166;-1;dark blue,*
OK
*nouveau_canon,Thread-4,481,245,-1,dark green,le_vôtre*
orienter,25,
feu
*mouvement_de,Thread-4,549,280,*
feu
*mouvement_de,Thread-4,504,278,*
*scores,Thread-4;1,Thread-3;-1,Thread-2;0,*
*angle,Thread-2,23,*
*angle,Thread-2,20,*
*tir_de,Thread-2,*
*mouvement_de,Thread-2,407,191,*
*départ_de,Thread-2*
*nouveau_canon,Thread-5,502,276,-1,dark green*

Lorsqu'un nouveau client démarre, il envoie une requête de connexion au serveur, lequel lui expédie en retour le message : « serveur OK ». À la réception de ce dernier, le client répond alors en envoyant lui-même : « client OK ». Ce premier échange de politesses n'est pas absolument indispensable, mais il permet de vérifier que la communication passe bien dans les deux sens. Étant donc averti que le client est prêt à travailler, le serveur lui expédie alors une description des canons déjà présents dans le jeu (éventuellement aucun) : identifiant, emplacement sur le canevas, orientation et couleur (ligne 3).

En réponse à l'accusé de réception du client (ligne 4), le serveur installe un nouveau canon dans l'espace de jeu, puis il signale les caractéristiques de cette installation non seulement au client qui l'a provoquée, mais également à tous les autres clients connectés. Le message expédié au nouveau client comporte cependant une différence (car c'est lui le propriétaire de ce nouveau canon) : en plus des caractéristiques du canon, qui sont fournies à tout le monde, il comporte un champ supplémentaire contenant simplement « le_vôtre » (comparez par exemple la ligne 5 avec la ligne 17, laquelle signale la connexion d'un autre joueur). Cette indication supplémentaire permet au client propriétaire du canon de distinguer parmi plusieurs messages similaires éventuels, celui qui contient l'identifiant unique que lui a attribué le serveur.

Les messages des lignes 6 et 7 sont des commandes envoyées par le client (réglage de la hausse et commande de tir). Dans la version précédente du jeu, nous avions déjà convenu que les canons se déplaceraient quelque peu (et au hasard) après chaque tir. Le serveur effectue donc cette opération, et s'empresse ensuite d'en faire connaître le résultat à tous les clients connectés. Le message reçu du serveur à la ligne 8 est donc l'indication d'un tel déplacement (les coordonnées fournies sont les coordonnées résultantes pour le canon concerné).

La ligne 11 reproduit le type de message expédié par le serveur lorsqu'une cible a été touchée. Les nouveaux scores de tous les joueurs sont ainsi communiqués à tous les clients.

Les messages serveur des lignes 12, 13 et 14 indiquent les actions entreprises par un autre joueur (réglage de hausse suivi d'un tir). Cette fois encore, le canon concerné est déplacé au hasard après qu'il ait tiré (ligne 15).

Lignes 16 et 17 : lorsque l'un des clients coupe sa connexion, le serveur en avertit tous les autres, afin que le canon correspondant disparaisse de l'espace de jeu sur tous les postes. À l'inverse, de nouveaux clients peuvent se connecter à tout moment pour participer au jeu.

Remarques complémentaires

Le premier champ de chaque message indique sa teneur. Les messages envoyés par le client sont très simples : ils correspondent aux différentes actions entreprises par le joueur (modifications de l'angle de tir et commandes de feu). Ceux qui sont envoyés par le serveur sont un peu plus complexes. La plupart d'entre eux sont expédiés à tous les clients connectés, afin de les tenir informés du déroulement du jeu. En conséquence, ces messages doivent mentionner l'identifiant du joueur qui a commandé une action ou qui est concerné par un changement quelconque. Nous avons vu plus haut que ces identifiants sont des noms générés automatiquement par le gestionnaire de threads du serveur, chaque fois qu'un nouveau client se connecte.

Certains messages concernant l'ensemble du jeu contiennent plusieurs informations par champ. Dans ce cas, les différents « sous-champs » sont séparés par des points-virgules (lignes 3 et 11).

Programme serveur : première partie

modifier

Vous trouverez dans les pages qui suivent le script complet du programme serveur. Nous vous le présentons en trois morceaux successifs afin de rapprocher les commentaires du code correspondant, mais la numérotation de ses lignes est continue. Bien qu'il soit déjà relativement long et complexe, vous estimerez probablement qu'il mérite d'être encore perfectionné, notamment au niveau de la présentation générale. Nous vous laisserons le soin d'y ajouter vous-même tous les compléments qui vous sembleront utiles (par exemple, une proposition de choisir les coordonnées de la machine hôte au démarrage, une barre de menus, etc.) :

#######################################################
# Jeu des bombardes - partie serveur                  #
# (C) Gérard Swinnen, Liège (Belgique)-  Juillet 2004 #
# Licence : GPL                                       #
# Avant d'exécuter ce script, vérifiez que l'adresse  #
# IP ci-dessous soit bien celle de la machine hôte.   #
# Vous pouvez choisir un numéro de port différent, ou #
# changer les dimensions de l'espace de jeu.          #
# Dans tous les cas, vérifiez que les mêmes choix ont #
# été effectués pour chacun des scripts clients.      #
#######################################################

host, port = '192.168.0.235', 35000
largeur, hauteur = 700, 400             # dimensions de l'espace de jeu

from Tkinter import *
import socket, sys, threading, time
import canon03
from canon04 import Canon, AppBombardes

class Pupitre(canon03.Pupitre):
    """Pupitre de pointage amélioré""" 
    def __init__(self, boss, canon):
        canon03.Pupitre.__init__(self, boss, canon)

    def tirer(self):
        "déclencher le tir du canon associé"
        self.appli.tir_canon(self.canon.id)
        
    def orienter(self, angle):
        "ajuster la hausse du canon associé"
        self.appli.orienter_canon(self.canon.id, angle)

    def valeur_score(self, sc =None):
        "imposer un nouveau score <sc>, ou lire le score existant"
        if sc == None:
            return self.score
        else:
            self.score =sc
            self.points.config(text = ' %s ' % self.score)

    def inactiver(self):
        "désactiver le bouton de tir et le système de réglage d'angle"
        self.bTir.config(state =DISABLED)
        self.regl.config(state =DISABLED) 

    def activer(self):
        "activer le bouton de tir et le système de réglage d'angle"
        self.bTir.config(state =NORMAL)
        self.regl.config(state =NORMAL)
        
    def reglage(self, angle):
        "changer la position du curseur de réglage"
        self.regl.config(state =NORMAL)
        self.regl.set(angle)
        self.regl.config(state =DISABLED)

La classe Pupitre() est construite par dérivation de la classe de même nom importée du module canon03. Elle hérite donc toutes les caractéristiques de celle-ci, mais nous devons surcharger[4] ses méthodes tirer() et orienter() :

Dans la version monoposte du logiciel, en effet, chacun des pupitres pouvait commander directement l'objet canon correspondant. Dans cette version réseau, par contre, ce sont les clients qui contrôlent à distance le fonctionnement des canons. Par conséquent, les pupitres qui apparaissent dans la fenêtre du serveur ne peuvent être que de simples répétiteurs des manœuvres effectuées par les joueurs sur chaque client. Le bouton de tir et le curseur de réglage de la hausse sont donc désactivés, mais les indications fournies obéissent aux injonctions qui leur sont adressées par l'application principale.

Cette nouvelle classe Pupitre() sera également utilisée telle quelle dans chaque exemplaire du programme client. Dans la fenêtre de celui-ci comme dans celle du serveur, tous les pupitres seront affichés comme des répétiteurs, mais l'un d'entre eux cependant sera complètement fonctionnel : celui qui correspond au canon du joueur.

Toutes ces raisons expliquent également l'apparition des nouvelles méthodes : activer(), desactiver(), reglage() et valeur_score(), qui seront elles aussi invoquées par l'application principale, en réponse aux messages-instructions échangés entre le serveur et ses clients.

La classe ThreadConnexion() ci-dessous sert à instancier la série d'objets threads qui s'occuperont en parallèle de toutes les connexions lancées par les clients. Sa méthode run() contient la fonctionnalité centrale du serveur, à savoir la boucle d'instructions qui gère la réception des messages provenant d'un client particulier, lesquels entraînent chacun toute une cascade de réactions. Vous y trouverez la mise en œuvre concrète du protocole de communication décrit dans les pages précédentes.

class ThreadConnexion(threading.Thread):
    """objet thread gestionnaire d'une connexion client"""
    def __init__(self, boss, conn):
        threading.Thread.__init__(self)
        self.connexion = conn           # réf. du socket de connexion
        self.app = boss                 # réf. de la fenêtre application

    def run(self):
        "actions entreprises en réponse aux messages reçus du client"
        nom = self.getName()            # id. du client = nom du thread
        while 1:
            msgClient = self.connexion.recv(1024)
            print "**%s** de %s" % (msgClient, nom)
            deb = msgClient.split(',')[0]
            if deb == "fin" or deb =="":
                self.app.enlever_canon(nom)
                # signaler le départ de ce canon aux autres clients :
                self.app.verrou.acquire()
                for cli in self.app.conn_client:
                    if cli != nom:
                        message = "départ_de,%s" % nom
                        self.app.conn_client[cli].send(message)
                self.app.verrou.release()                
                # fermer le présent thread :
                break                   
            elif deb =="client OK":
                # signaler au nouveau client les canons déjà enregistrés :
                msg ="canons,"
                for g in self.app.guns:
                    gun = self.app.guns[g]
                    msg =msg +"%s;%s;%s;%s;%s," % \
                              (gun.id, gun.x1, gun.y1, gun.sens, gun.coul)
                self.app.verrou.acquire()
                self.connexion.send(msg)
                # attendre un accusé de réception ('OK') :
                self.connexion.recv(100)
                self.app.verrou.release()                
                # ajouter un canon dans l'espace de jeu serveur.
                # la méthode invoquée renvoie les caract. du canon créé :
                x, y, sens, coul = self.app.ajouter_canon(nom)
                # signaler les caract. de ce nouveau canon à tous les
                # clients déjà connectés :
                self.app.verrou.acquire()
                for cli in self.app.conn_client:
                    msg ="nouveau_canon,%s,%s,%s,%s,%s" % \
                                       (nom, x, y, sens, coul)
                    # pour le nouveau client, ajouter un champ indiquant
                    # que le message concerne son propre canon :
                    if cli == nom:
                        msg =msg +",le_vôtre"
                    self.app.conn_client[cli].send(msg)
                self.app.verrou.release()
            elif deb =='feu':
                self.app.tir_canon(nom)
                # Signaler ce tir à tous les autres clients :
                self.app.verrou.acquire()
                for cli in self.app.conn_client:
                    if cli != nom:
                        message = "tir_de,%s," % nom
                        self.app.conn_client[cli].send(message)        
                self.app.verrou.release()
            elif deb =="orienter":
                t =msgClient.split(',')
                # on peut avoir reçu plusieurs angles. utiliser le dernier: 
                self.app.orienter_canon(nom, t[-2])
                # Signaler ce changement à tous les autres clients :
                self.app.verrou.acquire()
                for cli in self.app.conn_client:
                    if cli != nom:
                        # virgule terminale, car messages parfois groupés :
                        message = "angle,%s,%s," % (nom, t[-2])
                        self.app.conn_client[cli].send(message)
                self.app.verrou.release()
                    
        # Fermeture de la connexion :
        self.connexion.close()          # couper la connexion
        del self.app.conn_client[nom]   # suppr. sa réf. dans le dictionn.
        self.app.afficher("Client %s déconnecté.\n" % nom)
        # Le thread se termine ici

Synchronisation de threads concurrents à l'aide de « verrous » (thread locks)

modifier

Au cours de votre examen du code ci-dessus, vous aurez certainement remarqué la structure particulière des blocs d'instructions par lesquelles le serveur expédie un même message à tous ses clients. Considérez par exemple les lignes 74 à 80 :

La ligne 75 active la méthode acquire() d'un objet « verrou » qui a été créé par le constructeur de l'application principale. Cet objet est une instance de la classe Lock(), laquelle fait partie du module threading que nous avons importé en début de script. Les lignes suivantes (76 à 79) provoquent l'envoi d'un message à tous les clients connectés (sauf un). Ensuite, l'objet « verrou » est à nouveau sollicité, cette fois pour sa méthode release().

À quoi cet objet « verrou » peut-il donc bien servir ? Puisqu'il est produit par une classe du module threading, vous pouvez deviner que son utilité concerne les threads. En fait, de tels objets « verrous » servent à synchroniser les threads concurrents. De quoi s'agit-il ?

Vous savez que le serveur démarre un thread différent pour chacun des clients qui se connecte. Ensuite, tous ces threads fonctionnent en parallèle. Il existe donc un risque que de temps à autre, deux ou plusieurs de ces threads essaient d'utiliser une ressource commune en même temps.

Dans les lignes de code que nous venons de discuter, par exemple, nous avons affaire à un thread qui souhaite exploiter quasiment toutes les connexions présentes pour poster un message. Il est donc parfaitement possible que pendant ce temps, un autre thread tente d'exploiter lui aussi l'une ou l'autre de ces connexions, ce qui risque de provoquer un dysfonctionnement (en l'occurrence, la superposition chaotique de plusieurs messages).

Un tel problème de concurrence entre threads peut être résolu par l'utilisation d'un objet-verrou (thread lock). Un tel objet n'est créé qu'en un seul exemplaire, dans un espace de noms accessible à tous les threads concurrents. Il se caractérise essentiellement par le fait qu'il se trouve toujours dans l'un ou l'autre de deux états : soit verrouillé, soit déverrouillé. Son état initial est l'état déverrouillé.

Utilisation

Lorsqu'un thread quelconque s'apprête à accéder à une ressource commune, il active d'abord la méthode acquire() du verrou. Si celui-ci était dans l'état déverrouillé, il se verrouille, et le thread demandeur peut alors utiliser la ressource commune, en toute tranquillité. Lorsqu'il aura fini d'utiliser la ressource, il s'empressera cependant d'activer la méthode release() du verrou, ce qui le fera repasser dans l'état déverrouillé.

En effet : Si un autre thread concurrent active lui aussi la méthode acquire() du verrou, alors que celui-ci est dans l'état verrouillé, la méthode « ne rend pas la main », provoquant le blocage de ce thread, lequel suspend donc son activité jusqu'à ce que le verrou repasse dans l'état déverrouillé. Ceci l'empêche donc d'accéder à la ressource commune durant tout le temps où un autre thread s'en sert. Lorsque le verrou est déverrouillé, l'un des threads en attente (il peut en effet y en avoir plusieurs) reprend alors son activité, et ainsi de suite.

L'objet verrou mémorise les références des threads bloqués, de manière à n'en débloquer qu'un seul à la fois lorsque sa méthode release() est invoquée. Il faut donc toujours veiller à ce que chaque thread qui active la méthode acquire() du verrou avant d'accéder à une ressource, active également sa méthode release() peu après.

Pour autant que tous les threads concurrents respectent la même procédure, cette technique simple empêche donc qu'une ressource commune soit exploitée en même temps par plusieurs d'entre eux. On dira dans ce cas que les threads ont été synchronisés.

Programme serveur : suite et fin

modifier

Les deux classes ci-dessous complètent le script serveur. Le code implémenté dans la classe ThreadClients() est assez similaire à celui que nous avions développé précédemment pour le corps d'application du logiciel de « Chat ». Dans le cas présent, toutefois, nous le plaçons dans une classe dérivée de Thread(), parce que devons faire fonctionner ce code dans un thread indépendant de celui de l'application principale. Celui-ci est en effet déjà complètement accaparé par la boucle mainloop() de l'interface graphique.

La classe AppServeur() dérive de la classe AppBombardes() du module canon04. Nous lui avons ajouté un ensemble de méthodes complémentaires destinées à exécuter toutes les opérations qui résulteront du dialogue entamé avec les clients. Nous avons déjà signalé plus haut que les clients instancieront chacun une version dérivée de cette classe (afin de profiter des mêmes définitions de base pour la fenêtre, le canevas, etc.).

class ThreadClients(threading.Thread):
    """objet thread gérant la connexion de nouveaux clients"""
    def __init__(self, boss, connex):
        threading.Thread.__init__(self)
        self.boss = boss                # réf. de la fenêtre application
        self.connex = connex            # réf. du socket initial
        
    def run(self):
        "attente et prise en charge de nouvelles connexions clientes"
        txt ="Serveur prêt, en attente de requêtes ...\n"
        self.boss.afficher(txt)
        self.connex.listen(5) 
        # Gestion des connexions demandées par les clients :
        while 1:    
            nouv_conn, adresse = self.connex.accept()
            # Créer un nouvel objet thread pour gérer la connexion :
            th = ThreadConnexion(self.boss, nouv_conn)
            th.start()
            it = th.getName()        # identifiant unique du thread
            # Mémoriser la connexion dans le dictionnaire :
            self.boss.enregistrer_connexion(nouv_conn, it)
            # Afficher :
            txt = "Client %s connecté, adresse IP %s, port %s.\n" %\
                   (it, adresse[0], adresse[1])
            self.boss.afficher(txt)
            # Commencer le dialogue avec le client :
            nouv_conn.send("serveur OK")

class AppServeur(AppBombardes):
    """fenêtre principale de l'application (serveur ou client)"""
    def __init__(self, host, port, larg_c, haut_c):
        self.host, self.port = host, port
        AppBombardes.__init__(self, larg_c, haut_c)        
        self.active =1          # témoin d'activité
        # veiller à quitter proprement si l'on referme la fenêtre :
        self.bind('<Destroy>',self.fermer_threads)

    def specificites(self):
        "préparer les objets spécifiques de la partie serveur"    
        self.master.title('<<< Serveur pour le jeu des bombardes >>>')
        
        # widget Text, associé à une barre de défilement :
        st =Frame(self)
        self.avis =Text(st, width =65, height =5)
        self.avis.pack(side =LEFT)
        scroll =Scrollbar(st, command =self.avis.yview)
        self.avis.configure(yscrollcommand =scroll.set)
        scroll.pack(side =RIGHT, fill =Y)
        st.pack()
        
        # partie serveur réseau :
        self.conn_client = {}           # dictionn. des connexions clients
        self.verrou =threading.Lock()   # verrou pour synchroniser threads
        # Initialisation du serveur - Mise en place du socket :
        connexion = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        try:
            connexion.bind((self.host, self.port))
        except socket.error:
            txt ="La liaison du socket à l'hôte %s, port %s a échoué.\n" %\
                  (self.host, self.port)
            self.avis.insert(END, txt)
            self.accueil =None
        else:
            # démarrage du thread guettant la connexion des clients :
            self.accueil = ThreadClients(self, connexion)
            self.accueil.start()

    def depl_aleat_canon(self, id):
        "déplacer aléatoirement le canon <id>"
        x, y = AppBombardes.depl_aleat_canon(self, id)
        # signaler ces nouvelles coord. à tous les clients :
        self.verrou.acquire()
        for cli in self.conn_client:
            message = "mouvement_de,%s,%s,%s," % (id, x, y)
            self.conn_client[cli].send(message)
        self.verrou.release()
 
    def goal(self, i, j):
        "le canon <i> signale qu'il a atteint l'adversaire <j>"
        AppBombardes.goal(self, i, j)
        # Signaler les nouveaux scores à tous les clients :
        self.verrou.acquire()
        for cli in self.conn_client:
            msg ='scores,'
            for id in self.pupi:
                sc = self.pupi[id].valeur_score()
                msg = msg +"%s;%s," % (id, sc)
            self.conn_client[cli].send(msg)        
        time.sleep(.5)               # pour mieux séparer les messages 
        self.verrou.release()

    def ajouter_canon(self, id):
        "instancier un canon et un pupitre de nom <id> dans 2 dictionn."
        # on alternera ceux des 2 camps :
        n = len(self.guns)
        if n %2 ==0:
            sens = -1
        else:
            sens = 1
        x, y = self.coord_aleat(sens)
        coul =('dark blue', 'dark red', 'dark green', 'purple',
               'dark cyan', 'red', 'cyan', 'orange', 'blue', 'violet')[n]
        self.guns[id] = Canon(self.jeu, id, x, y, sens, coul)
        self.pupi[id] = Pupitre(self, self.guns[id])
        self.pupi[id].inactiver()
        return (x, y, sens, coul)
        
    def enlever_canon(self, id):
        "retirer le canon et le pupitre dont l'identifiant est <id>"
        if self.active == 0:        # la fenêtre a été refermée
            return                  
        self.guns[id].effacer()
        del self.guns[id]
        self.pupi[id].destroy()
        del self.pupi[id]
        
    def orienter_canon(self, id, angle):
        "régler la hausse du canon <id> à la valeur <angle>"
        self.guns[id].orienter(angle)
        self.pupi[id].reglage(angle)    
  
    def tir_canon(self, id):
        "déclencher le tir du canon <id>"
        self.guns[id].feu()

    def enregistrer_connexion(self, conn, it):
        "Mémoriser la connexion dans un dictionnaire"
        self.conn_client[it] = conn

    def afficher(self, txt):
        "afficher un message dans la zone de texte"
        self.avis.insert(END, txt)

    def fermer_threads(self, evt):
        "couper les connexions existantes et fermer les threads"
        # couper les connexions établies avec tous les clients :
        for id in self.conn_client:
            self.conn_client[id].send('fin')
        # forcer la terminaison du thread serveur qui attend les requêtes :
        if self.accueil != None:
            self.accueil._Thread__stop()
        self.active =0                  # empêcher accès ultérieurs à Tk

if __name__ =='__main__':
    AppServeur(host, port, largeur, hauteur).mainloop()
Commentaires
  • Ligne 173 : Il vous arrivera de temps à autre de vouloir « intercepter » l'ordre de fermeture de l'application que l'utilisateur déclenche en quittant votre programme, par exemple parce que vous voulez forcer la sauvegarde de données importantes dans un fichier, ou fermer aussi d'autres fenêtres, etc. Il suffit pour ce faire de détecter l'événement <Destroy>, comme nous le faisons ici pour forcer la terminaison de tous les threads actifs.
  • Lignes 179 à 186 : Au passage, voici comment vous pouvez associer une barre de défilement (widget Scrollbar) à un widget Text (vous pouvez faire de même avec un widget Canvas), sans faire appel à la bibliothèque Pmw[5].
  • Ligne 190 : Instanciation de l'obet « verrou » permettant de synchroniser les threads.
  • Lignes 202, 203 : Instanciation de l'objet thread qui attendra en permanence les demandes de connexion des clients potentiels.
  • Lignes 205 à 213, 215 à 227 : Ces méthodes surchargent les méthodes de même nom héritées de leur classe parente. Elles commencent par invoquer celles-ci pour effectuer le même travail (lignes 207, 217), puis ajoutent leur fonctionnalité propre, laquelle consiste à signaler à tout le monde ce qui vient de se passer.
  • Lignes 229 à 243 : Cette méthode instancie un nouveau poste de tir, chaque fois qu'un nouveau client se connecte. Les canons sont placés alternativement dans le camp de droite et dans celui de gauche, procédure qui pourrait bien évidemment être améliorée. La liste des couleurs prévues limite le nombre de clients à 10, ce qui devrait suffire.

Programme client

modifier

Le script correspondant au logiciel client est reproduit ci-après. Comme celui qui correspond au serveur, il est relativement court, parce qu'il utilise lui aussi l'importation de modules et l'héritage de classes. Le script serveur doit avoir été sauvegardé dans un fichier-module nommé canon_serveur.py. Ce fichier doit être placé dans le répertoire courant, de même que les fichiers-modules canon03.py et canon04.py qu'il utilise lui-même.

De ces modules ainsi importés, le présent script utilise les classes Canon() et Pupitre() à l'identique, ainsi qu'une forme dérivée de la classe AppServeur(). Dans cette dernière, de nombreuses méthodes ont été surchargées, afin d'adapter leur fonctionnalité. Considérez par exemple les méthodes goal() et depl_aleat_canon(), dont la variante surchargée ne fait plus rien du tout (instruction pass), parce que le calcul des scores et le repositionnement des canons après chaque tir ne peuvent être effectués que sur le serveur seulement.

C'est dans la méthode run() de la classe ThreadSocket() (lignes 86 à 126) que se trouve le code traitant les messages échangés avec le serveur. Nous y avons d'ailleurs laissé une instruction print (à la ligne 88) afin que les messages reçus du serveur apparaissent sur la sortie standard. Si vous réalisez vous-même une forme plus définitive de ce jeu, vous pourrez bien évidemment supprimer cette instruction.

#######################################################
# Jeu des bombardes - partie cliente                  #
# (C) Gérard Swinnen, Liège (Belgique) - Juillet 2004 #
# Licence : GPL                                       #
# Avant d'exécuter ce script, vérifiez que l'adresse, #
# le numéro de port et les dimensions de l'espace de  #
# jeu indiquées ci-dessous correspondent exactement   #
# à ce qui a été défini pour le serveur.              #
#######################################################

from Tkinter import *
import socket, sys, threading, time
from canon_serveur import Canon, Pupitre, AppServeur 

host, port = '192.168.0.235', 35000
largeur, hauteur = 700, 400          # dimensions de l'espace de jeu

class AppClient(AppServeur):
    def __init__(self, host, port, larg_c, haut_c):
        AppServeur.__init__(self, host, port, larg_c, haut_c)
        
    def specificites(self):
        "préparer les objets spécifiques de la partie client"    
        self.master.title('<<< Jeu des bombardes >>>')
        self.connex =ThreadSocket(self, self.host, self.port)
        self.connex.start()
        self.id =None

    def ajouter_canon(self, id, x, y, sens, coul):
        "instancier 1 canon et 1 pupitre de nom <id> dans 2 dictionnaires"
        self.guns[id] = Canon(self.jeu, id, int(x),int(y),int(sens), coul)
        self.pupi[id] = Pupitre(self, self.guns[id])
        self.pupi[id].inactiver()
    
    def activer_pupitre_personnel(self, id):
        self.id =id                         # identifiant reçu du serveur
        self.pupi[id].activer()
        
    def tir_canon(self, id):
        r = self.guns[id].feu()             # renvoie False si enrayé
        if r and id == self.id:
            self.connex.signaler_tir()
        
    def imposer_score(self, id, sc):
        self.pupi[id].valeur_score(int(sc))
        
    def deplacer_canon(self, id, x, y):
        "note: les valeurs de x et y sont reçues en tant que chaînes"
        self.guns[id].deplacer(int(x), int(y))

    def orienter_canon(self, id, angle):
        "régler la hausse du canon <id> à la valeur <angle>"
        self.guns[id].orienter(angle)
        if id == self.id:
            self.connex.signaler_angle(angle)
        else:
            self.pupi[id].reglage(angle)
            
    def fermer_threads(self, evt):
        "couper les connexions existantes et refermer les threads"
        self.connex.terminer()
        self.active =0                  # empêcher accès ultérieurs à Tk

    def depl_aleat_canon(self, id):
        pass                            # => méthode inopérante

    def goal(self, a, b):
        pass                            # => méthode inopérante


class ThreadSocket(threading.Thread):
    """objet thread gérant l'échange de messages avec le serveur"""
    def __init__(self, boss, host, port):
        threading.Thread.__init__(self)
        self.app = boss            # réf. de la fenêtre application
        # Mise en place du socket - connexion avec le serveur :
        self.connexion = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        try:
            self.connexion.connect((host, port))
        except socket.error:
            print "La connexion a échoué."
            sys.exit()    
        print "Connexion établie avec le serveur."

    def run(self):
        while 1:
            msg_recu = self.connexion.recv(1024)
            print "*%s*" % msg_recu
            # le message reçu est d'abord converti en une liste :
            t =msg_recu.split(',')
            if t[0] =="" or t[0] =="fin":
                # fermer le présent thread :
                break                   
            elif t[0] =="serveur OK":
                self.connexion.send("client OK")
            elif t[0] =="canons":
                self.connexion.send("OK")       # accusé de réception
                # éliminons le 1er et le dernier élément de la liste.
                # ceux qui restent sont eux-mêmes des listes :
                lc = t[1:-1]
                # chacune est la description complète d'un canon :
                for g in lc:
                    s = g.split(';')
                    self.app.ajouter_canon(s[0], s[1], s[2], s[3], s[4])
            elif t[0] =="nouveau_canon":
                self.app.ajouter_canon(t[1], t[2], t[3], t[4], t[5])
                if len(t) >6:
                    self.app.activer_pupitre_personnel(t[1])
            elif t[0] =='angle':
                # il se peut que l'on ait reçu plusieurs infos regroupées.
                # on ne considère alors que la première :
                self.app.orienter_canon(t[1], t[2])                 
            elif t[0] =="tir_de":
                self.app.tir_canon(t[1])
            elif t[0] =="scores":
                # éliminons le 1er et le dernier élément de la liste.
                # ceux qui restent sont eux-mêmes des listes :
                lc = t[1:-1]
                # chaque élément est la description d'un score :
                for g in lc:
                    s = g.split(';')
                    self.app.imposer_score(s[0], s[1])
            elif t[0] =="mouvement_de":
                self.app.deplacer_canon(t[1],t[2],t[3])
            elif t[0] =="départ_de":
                self.app.enlever_canon(t[1])

        # Le thread <réception> se termine ici.
        print "Client arrêté. Connexion interrompue."
        self.connexion.close()
        
    def signaler_tir(self):
        self.connexion.send('feu')

    def signaler_angle(self, angle):
        self.connexion.send('orienter,%s,' % angle)
    
    def terminer(self):
        self.connexion.send('fin')

# Programme principal :
if __name__ =='__main__':
    AppClient(host, port, largeur, hauteur).mainloop()
Commentaires
  • Lignes 15, 16 : Vous pouvez vous-même perfectionner ce script en lui ajoutant un formulaire qui demandera ces valeurs à l'utilisateur au cours du démarrage.
  • Lignes 19 à 27 : Le constructeur de la classe parente se termine en invoquant la méthode specificites(). On peut donc placer dans celle-ci ce qui doit être construit différemment dans le serveur et dans les clients. (Le serveur instancie notamment un widget text qui n'est pas repris dans les clients ; l'un et l'autre démarrent des objets threads différents pour gérer les connexions).
  • Lignes 39 à 42 : Cette méthode est invoquée chaque fois que l'utilisateur enfonce le bouton de tir. Le canon ne peut cependant pas effectuer des tirs en rafale. Par conséquent, aucun nouveau tir ne peut être accepté tant que l'obus précédent n'a pas terminé sa trajectoire. C'est la valeur « vraie » ou « fausse » renvoyée par la méthode feu() de l'objet canon qui indique si le tir a été accepté ou non. On utilise cette valeur pour ne signaler au serveur (et donc aux autres clients) que les tirs qui ont effectivement eu lieu.

Lignes 105 à 108 : Un nouveau canon doit être ajouté dans l'espace de jeu de chacun (c'est-à-dire dans le canevas du serveur, et dans le canevas de tous les clients connectés), chaque fois qu'un nouveau client se connecte. Le serveur envoie donc à ce moment un même message à tous les clients pour les informer de la présence de ce nouveau partenaire. Mais le message envoyé à celui-ci en particulier comporte un champ supplémentaire (lequel contient simplement la chaîne « le_vôtre »), afin que ce partenaire sache que ce message concerne son propre canon, et qu'il puisse donc activer le pupitre correspondant, tout en mémorisant l'identifiant qui lui a été attribué par le serveur (voir également les lignes 35 à 37).

Conclusions et perspectives

Cette application vous a été présentée dans un but didactique. Nous y avons délibérément simplifié un certain nombre de problèmes. Par exemple, si vous testez vous-même ces logiciels, vous constaterez que les messages échangés sont souvent rassemblés en « paquets », ce qui nécessiterait d'affiner les algorithmes mis en place pour les interpréter. De même, nous avons à peine esquissé le mécanisme fondamental du jeu : répartition des joueurs dans les deux camps, destruction des canons touchés, obstacles divers, etc. Il vous reste bien des pistes à explorer !

Exercices

  1. Simplifiez le script correspondant au client de « chat » vu précédemment, en supprimant l'un des deux objets threads. Arrangez-vous par exemple pour traiter l'émission de messages au niveau du thread principal.
  2. Modifiez le jeu des bombardes vu précédemment (version monoposte), en ne gardant qu'un seul canon et un seul pupitre de pointage. Ajoutez-y une cible mobile, dont le mouvement sera géré par un objet thread indépendant (de manière à bien séparer les portions de code qui contrôlent l'animation de la cible et celle du boulet).

Solution

  1. Réfléchissez !
  2. #####################################
    # Bombardement d'une cible mobile   #
    # (C) G. Swinnen - Avril 2004 - GPL #
    #####################################
    
    from Tkinter import *
    from math import sin, cos, pi
    from random import randrange
    from threading import Thread
    
    class Canon:
        """Petit canon graphique"""
        def __init__(self, boss, num, x, y, sens):
            self.boss = boss            # référence du canevas
            self.num = num              # n° du canon dans la liste
            self.x1, self.y1 = x, y     # axe de rotation du canon
            self.sens = sens            # sens de tir (-1:gauche, +1:droite)
            self.lbu = 30               # longueur de la buse
            # dessiner la buse du canon (horizontale) :
            self.x2, self.y2 = x + self.lbu * sens, y
            self.buse = boss.create_line(self.x1, self.y1,
                                         self.x2, self.y2, width =10)
            # dessiner le corps du canon (cercle de couleur) :
            self.rc = 15                # rayon du cercle 
            self.corps = boss.create_oval(x -self.rc, y -self.rc, x +self.rc,
                                          y +self.rc, fill ='black')
            # pré-dessiner un obus (au départ c'est un simple point) :
            self.obus = boss.create_oval(x, y, x, y, fill='red')
            self.anim = 0
            # retrouver la largeur et la hauteur du canevas :
            self.xMax = int(boss.cget('width'))
            self.yMax = int(boss.cget('height'))
    
        def orienter(self, angle):
            "régler la hausse du canon"
            # rem : le paramètre <angle> est reçu en tant que chaîne.
            # il faut donc le traduire en réel, puis le convertir en radians :
            self.angle = float(angle)*2*pi/360      
            self.x2 = self.x1 + self.lbu * cos(self.angle) * self.sens
            self.y2 = self.y1 - self.lbu * sin(self.angle)
            self.boss.coords(self.buse, self.x1, self.y1, self.x2, self.y2)
            
        def feu(self):
            "déclencher le tir d'un obus"
            # référence de l'objet cible :
            self.cible = self.boss.master.cible
            if self.anim ==0:
                self.anim =1
                # position de départ de l'obus (c'est la bouche du canon) :
                self.xo, self.yo = self.x2, self.y2
                v = 20              # vitesse initiale
                # composantes verticale et horizontale de cette vitesse :
                self.vy = -v *sin(self.angle)
                self.vx = v *cos(self.angle) *self.sens
                self.animer_obus()
        
        def animer_obus(self):
            "animer l'obus (trajectoire balistique)"
            # positionner l'obus, en re-définissant ses coordonnées :
            self.boss.coords(self.obus, self.xo -3, self.yo -3,
                                        self.xo +3, self.yo +3)
            if self.anim >0:
                # calculer la position suivante :
                self.xo += self.vx
                self.yo += self.vy
                self.vy += .5
                self.test_obstacle()        # a-t-on atteint un obstacle ?
                self.boss.after(1, self.animer_obus)
            else:
                # fin de l'animation :
                self.boss.coords(self.obus, self.x1, self.y1, self.x1, self.y1) 
       
        def test_obstacle(self):
            "évaluer si l'obus a atteint une cible ou les limites du jeu"
            if self.yo >self.yMax or self.xo <0 or self.xo >self.xMax:
                self.anim =0
                return
            if self.yo > self.cible.y -3 and self.yo < self.cible.y +18 \
            and self.xo > self.cible.x -3 and self.xo < self.cible.x +43:
                # dessiner l'explosion de l'obus (cercle orange) :
                self.explo = self.boss.create_oval(self.xo -10,
                             self.yo -10, self.xo +10, self.yo +10,
                             fill ='orange', width =0)
                self.boss.after(150, self.fin_explosion)
                self.anim =0
       
        def fin_explosion(self):
            "effacer le cercle d'explosion - gérer le score"
            self.boss.delete(self.explo)
            # signaler le succès à la fenêtre maîtresse :
            self.boss.master.goal()        
    
    class Pupitre(Frame):
        """Pupitre de pointage associé à un canon""" 
        def __init__(self, boss, canon):
            Frame.__init__(self, bd =3, relief =GROOVE)
            self.score =0
            s =Scale(self, from_ =88, to =65,
                     troughcolor ='dark grey',
                     command =canon.orienter)
            s.set(45)                       # angle initial de tir
            s.pack(side =LEFT)
            Label(self, text ='Hausse').pack(side =TOP, anchor =W, pady =5)        
            Button(self, text ='Feu !', command =canon.feu).\
                                        pack(side =BOTTOM, padx =5, pady =5)
            Label(self, text ="points").pack()
            self.points =Label(self, text=' 0 ', bg ='white')
            self.points.pack()
            # positionner à gauche ou à droite suivant le sens du canon :
            gd =(LEFT, RIGHT)[canon.sens == -1]
            self.pack(padx =3, pady =5, side =gd)
    
        def attribuerPoint(self, p):
            "incrémenter ou décrémenter le score"
            self.score += p
            self.points.config(text = ' %s ' % self.score)
    
    class Cible:
        """objet graphique servant de cible"""
        def __init__(self, can, x, y):
            self.can = can             # référence du canevas
            self.x, self.y = x, y
            self.cible = can.create_oval(x, y, x+40, y+15, fill ='purple')
            
        def deplacer(self, dx, dy):
            "effectuer avec la cible un déplacement dx,dy" 
            self.can.move(self.cible, dx, dy)
            self.x += dx
            self.y += dy
            return self.x, self.y
    
    class Thread_cible(Thread):
        """objet thread gérant l'animation de la cible"""
        def __init__(self, app, cible):
            Thread.__init__(self)
            self.cible = cible          # objet à déplacer
            self.app = app              # réf. de la fenêtre d'application
            self.sx, self.sy = 6, 3     # incréments d'espace et de
            self.dt =300                # temps pour l'animation (ms)
       
        def run(self):
            "animation, tant que la fenêtre d'application existe" 
            x, y = self.cible.deplacer(self.sx, self.sy)
            if x > self.app.xm -50 or x < self.app.xm /5:
                    self.sx = -self.sx
            if y < self.app.ym /2 or y > self.app.ym -20:
                    self.sy = -self.sy
            if self.app != None:
                self.app.after(int(self.dt), self.run)
    
        def stop(self):
            "fermer le thread si la fenêtre d'application est refermée"
            self.app =None
            
        def accelere(self):
            "accélérer le mouvement"
            self.dt /= 1.5
    
    class Application(Frame):
        def __init__(self):
            Frame.__init__(self)
            self.master.title('<<< Tir sur cible mobile >>>')
            self.pack()
            self.xm, self.ym = 600, 500
            self.jeu = Canvas(self, width =self.xm, height =self.ym,
                              bg ='ivory', bd =3, relief =SUNKEN)
            self.jeu.pack(padx =4, pady =4, side =TOP)
    
            # Instanciation d'un canon et d'un pupitre de pointage :
            x, y = 30, self.ym -20
            self.gun =Canon(self.jeu, 1, x, y, 1)
            self.pup =Pupitre(self, self.gun)
            
            # instanciation de la cible mobile :
            self.cible = Cible(self.jeu, self.xm/2, self.ym -25)
            # animation de la cible mobile, sur son propre thread :
            self.tc = Thread_cible(self, self.cible)
            self.tc.start()
            # arrêter tous les threads lorsque l'on ferme la fenêtre :
            self.bind('<Destroy>',self.fermer_threads)
    
        def goal(self):
            "la cible a été touchée"
            self.pup.attribuerPoint(1)
            self.tc.accelere()
            
        def fermer_threads(self, evt):
            "arrêter le thread d'animation de la cible"
            self.tc.stop()
    
    if __name__ =='__main__':
        Application().mainloop()
    

Utilisation de threads pour optimiser les animations.

modifier

Le dernier exercice proposé à la fin de la section précédente nous suggère une méthodologie de développements d'applications qui peut se révéler particulièrement intéressante, dans le cas de jeux vidéo impliquant plusieurs animations simultanées.

En effet : si vous programmez les différents éléments animés d'un jeu comme des objets indépendants fonctionnant chacun sur son propre thread, alors non seulement vous vous simplifiez la tâche et vous améliorez la lisibilité de votre script, mais encore vous augmentez la vitesse d'exécution et donc la fluidité de ces animations. Pour arriver à ce résultat, vous devrez abandonner la technique de temporisation que vous avez exploitée jusqu'ici, mais celle que vous allez utiliser à sa place est finalement plus simple !

Temporisation des animations à l'aide de after()

modifier

Dans toutes les animations que nous avons décrites jusqu'à présent, le « moteur » était constitué à chaque fois par une fonction contenant la méthode after(), laquelle est associée d'office à tous les widgets Tkinter. Vous savez que cette méthode permet d'introduire une temporisation dans le déroulement de votre programme : un chronomètre interne est activé, de telle sorte qu'après un intervalle de temps convenu, le système invoque automatiquement une fonction quelconque. En général, c'est la fonction contenant after() qui est elle-même invoquée : on réalise ainsi une boucle récursive, dans laquelle il reste à programmer les déplacements des divers objets graphiques.

Vous devez bien comprendre que pendant l'écoulement de l'intervalle de temps programmé à l'aide de la méthode after(), votre application n'est pas du tout « figée ». Vous pouvez par exemple pendant ce temps : cliquer sur un bouton, redimensionner la fenêtre, effectuer une entrée clavier, etc. Comment cela est-il rendu possible ?

Nous avons mentionné déjà à plusieurs reprises le fait que les applications graphiques modernes comportent toujours une sorte de moteur qui « tourne » continuellement en tâche de fond : ce dispositif se met en route lorsque vous activez la méthode mainloop() de votre fenêtre principale. Comme son nom l'indique fort bien, cette méthode met en œuvre une boucle répétitive perpétuelle, du même type que les boucles while que vous connaissez bien. De nombreux mécanismes sont intégrés à ce « moteur ». L'un d'entre eux consiste à réceptionner tous les événements qui se produisent, et à les signaler ensuite à l'aide de messages appropriés aux programmes qui en font la demande (voir : Programmes pilotés par des événements), d'autres contrôlent les actions à effectuer au niveau de l'affichage, etc. Lorsque vous faites appel à la méthode after() d'un widget, vous utilisez en fait un mécanisme de chronométrage qui est intégré lui aussi à mainloop(), et c'est donc ce gestionnaire central qui déclenche l'appel de fonction que vous souhaitez, après un certain intervalle de temps.

La technique d'animation utilisant la méthode after() est la seule possible pour une application fonctionnant toute entière sur un seul thread, parce que c'est la boucle mainloop() qui dirige l'ensemble du comportement d'une telle application de manière absolue. C'est notamment elle qui se charge de redessiner tout ou partie de la fenêtre chaque fois que cela s'avère nécessaire. Pour cette raison, vous ne pouvez pas imaginer de construire un moteur d'animation qui redéfinirait les coordonnées d'un objet graphique à l'intérieur d'une simple boucle while, par exemple, parce que pendant tout ce temps l'exécution de mainloop() resterait suspendue, ce qui aurait pour conséquence que pendant tout ce temps aucun objet graphique ne serait redessiné (en particulier celui que vous souhaitez mettre en mouvement !). En fait, toute l'application apparaîtrait figée, aussi longtemps que la boucle while ne serait pas interrompue.

Puisqu'elle est la seule possible, c'est donc cette technique que nous avons utilisée jusqu'à présent dans tous nos exemples d'applications mono-thread. Elle comporte cependant un inconvénient gênant : du fait du grand nombre d'opérations prises en charge à chaque itération de la boucle mainloop(), la temporisation que l'on peut programmer à l'aide de after() ne peut pas être très courte. Par exemple, elle ne peut guère descendre en dessous de 15 ms sur un PC typique (processeur de type Pentium IV, f = 1,5 GHz). Vous devez tenir compte de cette limitation si vous souhaitez développer des animations rapides.

Un autre inconvénient lié à l'utilisation de la méthode after() réside dans la structure de la boucle d'animation (à savoir une fonction ou une méthode « récursive », c'est-à-dire qui s'appelle elle-même) : il n'est pas toujours simple en effet de bien maîtriser ce genre de construction logique, en particulier si l'on souhaite programmer l'animation de plusieurs objets graphiques indépendants, dont le nombre ou les mouvements doivent varier au cours du temps.

Temporisation des animations à l'aide de time.sleep()

modifier

Vous pouvez ignorer les limitations de la méthode after() évoquées ci-dessus, si vous en confiez l'animation de vos objets graphiques à des threads indépendants. En procédant ainsi, vous vous libérez de la tutelle de mainloop(), et il vous est permis alors de construire des procédures d'animation sur la base de structures de boucles plus « classiques », utilisant l'instruction while ou l'instruction for par exemple.

Au cœur de chacune de ces boucles, vous devez cependant toujours veiller à insérer une temporisation pendant laquelle vous « rendez la main » au système d'exploitation (afin qu'il puisse s'occuper des autres threads). Pour ce faire, vous ferez appel à la fonction sleep() du module time. Cette fonction permet de suspendre l'exécution du thread courant pendant un certain intervalle de temps, pendant lequel les autres threads et applications continuent à fonctionner. La temporisation ainsi produite ne dépend pas de mainloop(), et par conséquent, elle peut être beaucoup plus courte que celle que vous autorise la méthode after().

Attention : cela ne signifie pas que le rafraîchissement de l'écran sera lui-même plus rapide, car ce rafraîchissement continue à être assuré par mainloop(). Vous pourrez cependant accélérer fortement les différents mécanismes que vous installez vous-même dans vos procédures d'animation. Dans un logiciel de jeu, par exemple, il est fréquent d'avoir à comparer périodiquement les positions de deux mobiles (tels qu' un projectile et une cible), afin de pouvoir entreprendre une action lorsqu'ils se rejoignent (explosion, ajout de points à un score, etc.). Avec la technique d'animation décrite ici, vous pouvez effectuer beaucoup plus souvent ces comparaisons et donc espérer un résultat plus précis. De même, vous pouvez augmenter le nombre de points pris en considération pour le calcul d'une trajectoire en temps réel, et donc affiner celle-ci.

Remarque : Lorsque vous utilisez la méthode after(), vous devez lui indiquer la temporisation souhaitée en millisecondes, sous la forme d'un argument entier. Lorsque vous faites appel à la fonction sleep(), par contre, l'argument que vous transmettez doit être exprimé en secondes, sous la forme d'un réel (float). Vous pouvez cependant utiliser des très petites valeurs (0.0003 par ex.).

Exemple concret

modifier

Le petit script reproduit ci-dessous illustre la mise en œuvre de cette technique, dans un exemple volontairement minimaliste. Il s'agit d'une petite application graphique dans laquelle une figure se déplace en cercle à l'intérieur d'un canevas. Son « moteur » mainloop() est lancé comme d'habitude sur le thread principal. Le constructeur de l'application instancie un canevas contenant le dessin d'un cercle, un bouton et un objet thread. C'est cet objet thread qui assure l'animation du dessin, mais sans faire appel à la méthode after() d'un widget. Il utilise plutôt une simple boucle while très classique, installée dans sa méthode run().

 
capture d'écran de l'application
from Tkinter import *
from math import sin, cos
import time, threading

class App(Frame):
    def __init__(self):
        Frame.__init__(self)
        self.pack()
        can =Canvas(self, width =400, height =400,
                    bg ='ivory', bd =3, relief =SUNKEN)
        can.pack(padx =5, pady =5)
        cercle = can.create_oval(185, 355, 215, 385, fill ='red')
        tb = Thread_balle(can, cercle)
        Button(self, text ='Marche', command =tb.start).pack(side =LEFT)
        # Button(self, text ='Arrêt', command =tb.stop).pack(side =RIGHT)
        # arrêter l'autre thread si l'on ferme la fenêtre :
        self.bind('<Destroy>', tb.stop)
   
class Thread_balle(threading.Thread):
    def __init__(self, canevas, dessin):
        threading.Thread.__init__(self)
        self.can, self.dessin = canevas, dessin
        self.anim =1
    
    def run(self):
        a = 0.0
        while self.anim == 1:
            a += .01
            x, y = 200 + 170*sin(a), 200 +170*cos(a)
            self.can.coords(self.dessin, x-15, y-15, x+15, y+15)
            time.sleep(0.010)

    def stop(self, evt =0):
        self.anim =0

App().mainloop()
Commentaires
  • Lignes 13 & 14 : Afin de simplifier notre exemple au maximum, nous créons l'objet thread chargé de l'animation, directement dans le constructeur de l'application principale. Cet objet thread ne démarrera cependant que lorsque l'utilisateur aura cliqué sur le bouton « Marche », qui active sa méthode start() (rappelons ici que c'est cette méthode intégrée qui lancera elle-même la méthode run() où nous avons installé notre boucle d'animation).
  • Ligne 15 : Vous ne pouvez par redémarrer un thread qui s'est terminé. De ce fait, vous ne pouvez lancer cette animation qu'une seule fois (tout au moins sous la forme présentée ici). Pour vous en convaincre, activez la ligne n° 15 en enlevant le caractère # situé au début (et qui fait que Python considère qu'il s'agit d'un simple commentaire) : lorsque l'animation est lancée, un clic de souris sur le bouton ainsi mis en place provoque la sortie de la boucle while des lignes 27-31, ce qui termine la méthode run(). L'animation s'arrête, mais le thread qui la gérait s'est terminé lui aussi. Si vous essayez de le relancer à l'aide du bouton « Marche », vous n'obtenez rien d'autre qu'un message d'erreur.
  • Lignes 26 à 31 : Pour simuler un mouvement circulaire uniforme, il suffit de faire varier continuellement la valeur d'un angle a. Le sinus et le cosinus de cet angle permettent alors de calculer les coordonnées x et y du point de la circonférence qui correspond à cet angle. À chaque itération, l'angle ne varie que d'un centième de radian seulement (environ 0,6°), et il faudra donc 628 itérations pour que le mobile effectue un tour complet. La temporisation choisie pour ces itérations se trouve à la ligne 31 : 10 millisecondes. Vous pouvez accélérer le mouvement en diminuant cette valeur, mais vous ne pourrez guère descendre en dessous de 1 milliseconde (0.001 s), ce qui n'est déjà pas si mal.
  1. Dans un système d'exploitation de type Unix (comme Linux), les différents threads d'un même programme font partie d'un seul processus. Il est également possible de gérer différents processus à l'aide d'un même script Python (opération fork), mais l'explication de cette technique dépasse largement le cadre de ce cours.
  2. Le chat est l'occupation qui consiste à « papoter » par l'intermédiaire d'ordinateurs. Les canadiens francophones ont proposé le terme de clavardage pour désigner ce « bavardage par claviers interposés ».
  3. Que les puristes veuillent bien me pardonner : j'admets volontiers que cette astuce pour forcer l'arrêt d'un thread n'est pas vraiment recommandable. Je me suis autorisé ce raccourci afin de ne pas trop alourdir ce texte, qui se veut seulement une initiation. Le lecteur exigeant pourra approfondir cette question en consultant l'un ou l'autre des ouvrages de référence mentionnés dans la bibliographie située en fin d'ouvrage.
  4. Rappel : dans une classe dérivée, vous pouvez définir une nouvelle méthode avec le même nom qu'une méthode de la classe parente, afin de modifier sa fonctionnalité dans la classe dérivée. Cela s'appelle surcharger cette méthode
  5. Voir : Python Mega Widgets
À faire... 


  1. ajouter des images



  GFDL Vous avez la permission de copier, distribuer et/ou modifier ce document selon les termes de la licence de documentation libre GNU, version 1.2 ou plus récente publiée par la Free Software Foundation ; sans sections inaltérables, sans texte de première page de couverture et sans texte de dernière page de couverture.