Programmation Python/Version imprimable2b
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
Fonctions
Définir une fonction
[modifier | modifier le wikicode]La programmation est l'art d'apprendre à un ordinateur comment accomplir des tâches qu'il n'était pas capable de réaliser auparavant. L'une des méthodes les plus intéressantes pour y arriver consiste à ajouter de nouvelles instructions au langage de programmation que vous utilisez, sous la forme de fonctions originales. De plus, créer ses propres fonctions permet de factoriser le code redondant en pouvant être appelées depuis plusieurs endroits.
L'approche efficace d'un problème complexe consiste souvent à le décomposer en plusieurs sous-problèmes plus simples qui seront étudiés séparément (ces sous-problèmes peuvent éventuellement être eux-mêmes décomposés à leur tour, et ainsi de suite). Or il est important que cette décomposition soit représentée fidèlement dans les algorithmes pour que ceux-ci restent clairs.
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
etwhile
, l'instructiondef
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 | modifier le wikicode]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 | modifier le wikicode]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 | modifier le wikicode]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 | modifier le wikicode]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 | modifier le wikicode]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 | modifier le wikicode]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 | modifier le wikicode]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 | modifier le wikicode]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 | modifier le wikicode]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 | modifier le wikicode]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 | modifier le wikicode]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.2.Définissez une fonction
ligneCar(n, ca)
qui renvoie une chaîne den
caractèresca
. - 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ésultat19.635
- 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 dimensionsx1
,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
- Définissez une fonction
maximum(n1,n2,n3)
qui renvoie le plus grand de 3 nombresn1
,n2
,n3
fournis en arguments. Par exemple, l'exécution de l'instruction :
print maximum(2,5,4)
doit donner le résultat :5
- Complétez le module de fonctions graphiques dessins_tortue.py.
Commencez par ajouter un paramètreangle
à la fonctioncarre()
, de manière à ce que les carrés puissent être tracés dans différentes orientations. Définissez ensuite une fonctiontriangle(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 : - 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 : - 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 fonctiontriangle()
définie précédemment.
Votre programme principal dessinera également une série de ces étoiles : - Définissez une fonction
compteCar(ca,ch)
qui renvoie le nombre de fois que l'on rencontre le caractèreca
dans la chaîne de caractèresch
. Par exemple, l'exécution de l'instruction : print compteCar('e','Cette phrase est un exemple') doit donner le résultat : 7 - 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
- 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
- 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). - Définissez une fonction
compteMots(ph)
qui renvoie le nombre de mots contenus dans la phraseph
(On considère comme mots les ensembles de caractères inclus entre des espaces).
Solution
- Réfléchissez !
-
from math import pi def surfCercle(r): "Surface d'un cercle de rayon r" return pi * r**2 # test : print surfCercle(2.5)
-
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)
-
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)
- Réfléchissez !
- Réfléchissez !
- Réfléchissez !
-
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")
-
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)
-
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)
- Réfléchissez !
- Réfléchissez !
Exercices
- 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
- 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
- Définissez une fonction
changeCar(ch,ca1,ca2,debut,fin)
qui remplace tous les caractèresca1
par des caractèresca2
dans la chaîne de caractèresch
, à partir de l'indicedebut
et jusqu'à l'indicefin
, 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.
- 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
-
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)
-
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)
-
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)
-
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)
Références
[modifier | modifier le wikicode]
Modules
Définition
[modifier | modifier le wikicode]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 | modifier le wikicode]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 | modifier le wikicode]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 | modifier le wikicode]Les modules standards les plus importants sont[3] :
- cgi
- math
- os
- pickle
- random
- re
- socket
- sys
- time
- urllib
Exemple du module "math"
[modifier | modifier le wikicode]# 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)
- É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)
- É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). - É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.
- É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
-
# 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"
-
# 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
- Réfléchissez !
-
# 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 | modifier le wikicode]
Exercices sur les bases du langage
Révision
[modifier | modifier le wikicode]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 | modifier le wikicode]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.")
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 | modifier le wikicode]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
- 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é'
- 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'
- Reprendre le programme c) avec a = 0 au lieu de a = 1. Que se passe-t-il ? Conclure !
- É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. - 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).
- 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.
- 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.
- 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.
- 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 - 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. - É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.
- 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
- Réfléchissez !
- Réfléchissez !
- Réfléchissez !
-
# 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
-
# 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"
- Réfléchissez !
-
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."
- Réfléchissez !
- Réfléchissez !
- Réfléchissez !
-
# 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
- Réfléchissez !
Exercices
[modifier | modifier le wikicode]Exercice 1
[modifier | modifier le wikicode]Écrire un programme qui affiche "Bonjour le monde".
Solution
print ("Bonjour le monde") |
Exercice 2
[modifier | modifier le wikicode]Écrire un programme qui permet de saisir le nom de l'utilisateur et de renvoyer "Bonjour", suivi de ce nom.
(3 solutions possibles)
Solution 1 nom = input ("Quel est votre nom ? ")
print ("Bonjour", nom)
|
Solution 2 print ("Bonjour", input ("Quel est votre nom ? "))
|
Solution 3 name = input("Quel est votre nom ? ")
print("Bonjour " + name + "!")
|
Exercice 3
[modifier | modifier le wikicode]Écrire un programme qui demande à l'utilisateur la saisie de a et b et affiche la somme de a et de b.
Solution
a = input ("Valeur de a : ") b = input ("Valeur de b : ") a= int (a) b= int (b) print ("Somme de a+b = ",a+b) |
En version 3.1 cela pourrait être
var_a = int(input("Donnez moi la valeur de a: ")) var_b = int(input("Donnez moi la valeur de b: ")) print("Voici le résultat de a + b :" , var_a + var_b , "!") |
Exercice 4
[modifier | modifier le wikicode]É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.
Solution
# Ce script ne gère pas si l'anniversaire est passé ou non d'où le "environ" annee_courante = 2016 print ("Quelle est votre année de naissance ?") reponse = raw_input() print ("Vous avez environ", annee_courante- int(reponse),"ans") |
Pour Python 3.1
# Ce script gère si l'anniversaire est passé from datetime import datetime dn=input('Votre date de naissance ? (format jj/mm/aaaa) :') dn=dn.split('/');jn=int(dn[0]);mn=int(dn[1]);an=int(dn[2]) da=datetime.now() if mn<da.month or (mn==da.month and jn<=da.day): age=da.year-an else: age=da.year-an-1 print('Vous avez',age,'ans.') |
Exercice 5
[modifier | modifier le wikicode]É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 :
Solution
import math
print ("xA?")
xA = int(raw_input())
print ("yA?")
yA = int(raw_input())
print ("xB?")
xB = int(raw_input())
print ("yB?")
yB = int(raw_input())
print ("distance entre A et B:", math.sqrt( (xA-xB)**2 + (yA-yB)**2))
|
En version 3.1
import math
xA = int(input("Donnez moi Xa"))
yA = int(input("Donnez moi Ya"))
xB = int(input("Donnez moi Xb"))
yB = int(input("Donnez moi Yb"))
distance = math.sqrt((xB - xA)**2 + (yB - yA)**2)
print("Bonjour, la distance qui sépare les deux points est de " + str(distance))
|
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 | modifier le wikicode]Les expressions rationnelles peuvent être analysées et testées via un débogueur en ligne comme https://regex101.com/.
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. |
^
|
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. |
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é.
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 :
|
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.
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].
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 |
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 |
\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.
- 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
?>
: 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
- Chercher une lettre u précédée d'une lettre q :
?<!
: 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 :
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 | modifier le wikicode]compile()
renvoieNone
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 | modifier le wikicode]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'
Flags
[modifier | modifier le wikicode]Le comportement de certaines expressions peut être reconfiguré en ajoutant un "flag" en paramètre des méthodes[10].
re.IGNORECASE
[modifier | modifier le wikicode]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 | modifier le wikicode]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 | modifier le wikicode]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 | modifier le wikicode]#!/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 | modifier le wikicode]- 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 | modifier le wikicode]- ↑ https://docs.python.org/3/howto/regex.html
- ↑ https://pypi.org/project/regex/
- ↑ https://unicode-table.com/fr/
- ↑ https://docstore.mik.ua/orelly/webprog/pcook/ch13_05.htm
- ↑ https://www.regular-expressions.info/posixbrackets.html
- ↑ https://www.regular-expressions.info/unicode.html
- ↑ https://www.regextester.com/15
- ↑ Jan Goyvaerts, Steven Levithan, Regular Expressions Cookbook, O'Reilly Media, Inc., (lire en ligne)
- ↑ Les options sont appelées modificateurs (modifiers en anglais), voir https://www.regular-expressions.info/modifiers.html
- ↑ https://docs.python.org/2/library/re.html#module-contents
- ↑ https://docs.python.org/2/library/re.html#search-vs-match
Programmation orientée objet pour les non-programmeurs
Le concept objet
[modifier | modifier le wikicode]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 | modifier le wikicode]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 | modifier le wikicode]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 | modifier le wikicode]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 dé 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 | modifier le wikicode]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.
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 | modifier le wikicode]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'instructionclass
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
.
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()
.
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 | modifier le wikicode]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.
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)
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
.
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 | modifier le wikicode]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
- Écrivez une fonction
distance()
qui permette de calculer la distance entre deux points. Cette fonction attendra évidemment deux objetsPoint()
comme arguments.
Solution
- Réfléchissez !
Similitude et unicité
[modifier | modifier le wikicode]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 | modifier le wikicode]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 :
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 | modifier le wikicode]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 | modifier le wikicode]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 | modifier le wikicode]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ètreself
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 | modifier le wikicode]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 | modifier le wikicode]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 | modifier le wikicode]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 | modifier le wikicode]Destructeur : se lance quand l'objet est détruit.
__enter__ et __exit__
[modifier | modifier le wikicode]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 | modifier le wikicode]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 | modifier le wikicode]>>> 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 | modifier le wikicode]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 | modifier le wikicode]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 | modifier le wikicode]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 | modifier le wikicode]Réservé à Python 2.x.
Opérateurs binaires
[modifier | modifier le wikicode]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 | modifier le wikicode]Fonction | Opérateur |
---|---|
__pos__ | +A |
__neg__ | -A |
__inv__ | ~A |
__abs__ | abs(A) |
__len__ | len(A) |
Gestion des attributs
[modifier | modifier le wikicode]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 | modifier le wikicode]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] |
Divers
[modifier | modifier le wikicode]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 | modifier le wikicode]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 | modifier le wikicode]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 | modifier le wikicode]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 :
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 :
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)
Exercices
[modifier | modifier le wikicode]Exercices
- 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.
>>> 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.
- une méthode
- Définissez une classe
CompteBancaire()
, qui permette d'instancier des objets tels quecompte1
,compte2
, etc. Le constructeur de cette classe initialisera deux attributs d'instance "nom" et "solde", avec les valeurs par défaut'Dupont'
et1000
. Trois autres méthodes seront définies :depot(somme)
permettra d'ajouter une certaine somme au solderetrait(somme)
permettra de retirer une certaine somme du soldeaffiche()
permettra d'afficher le nom du titulaire et le solde de son compte.
>>> 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.
- 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 objetVoiture()
, 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.
- 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 objetSatellite()
, 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
-
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
- Réfléchissez !
-
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()
-
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
- 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ètrerayon
), vous définirez une méthodesurface()
, qui devra renvoyer la surface du cercle. Définissez ensuite une classeCylindre()
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éthodevolume()
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
- 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 - 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éthodenom_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éthodebattre()
: 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éthodetirer()
: 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écialNone
au programme appelant. Exemple d'utilisation de la classeJeuDeCartes()
: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
- 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
- Voir ci-dessous.
-
#(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()
-
# 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
-
#(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 | modifier le wikicode]
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 | modifier le wikicode]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 :
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éthodegrid()
.
- 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'optionfill
de chacune des bandes colorées, en utilisant les noms de couleur extraits de la listeself.cc
grâce à aux trois indicesli[1]
,li[2]
etli[3]
qui contiennent les 3 chiffres correspondants.
Exercices
- 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. - Modifiez le script ci-dessus de telle manière que l'image dessinée soit deux fois plus grande.
- 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' Ω.
- 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.
- 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
- Réfléchissez !
- Réfléchissez !
- Réfléchissez !
- Réfléchissez !
- Réfléchissez !
« Petit train » : héritage, échange d'informations entre classes
[modifier | modifier le wikicode]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éthodeperso()
. 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éthodecoucou()
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éthodemainloop()
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
- 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 fonctioncercle()
). À cette même classeWagon()
, ajoutez encore une méthodeallumer()
, 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 fonctioncercle()
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
-
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 | modifier le wikicode]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
- 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
- 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 classeCanvas()
. 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 classeCanvas()
.
- 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
ethaut
(largeur et hauteur) servent à assigner des valeurs aux optionswidth
etheight
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 classeCanvas()
à 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
ethaut
dans des variables d'instance, parce que nous devrons pouvoir y accéder aussi dans la méthodetraceCourbe()
.
- 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'optionsmooth =1
améliore l'aspect final, par lissage.
Exercices
- 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.
- 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éthodecreate_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 classeOscilloGraphe()
, 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) :
- 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. - Complétez encore votre widget en y faisant apparaître des repères numériques.
Solution
- Réfléchissez !
- Réfléchissez !
- Réfléchissez !
- Réfléchissez !
« Curseurs » : un widget composite
[modifier | modifier le wikicode]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 | modifier le wikicode]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 | modifier le wikicode]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 | modifier le wikicode]- Ligne 6 : La méthode
constructeur
utilise un paramètre optionnelcoul
. Ce paramètre permettra de choisir une couleur pour le graphique soumis au contrôle du widget. Le paramètreboss
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 similairesDoubleVar()
,StringVar()
etBooleanVar()
. 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é dansself.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éthodeset()
permet de lui assigner une valeur, et la méthodeget()
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éthodesetCurve()
, le premier curseur active la méthodesetFrequency()
, le second curseur active la méthodesetPhase()
, et le troisième curseur active la méthodesetAmplitude()
. Remarquez bien au passage que l'optioncommand
des widgetsScale
transmet un argument à la méthode associée (la position actuelle du curseur), alors que la même optioncommand
ne transmet rien dans le cas du widgetCheckbutton
. 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éthodeevent_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 classeChoixVibra()
et un widget de la classeLabel()
. À 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 fonctionafficherTout()
. 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 classeChoixVibra()
, 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 fonctionafficherTout()
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 classeLabel()
. - 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éthodeget()
.
Propagation des évènements
[modifier | modifier le wikicode]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
- 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éthodeconfigure()
. 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 scriptoscillo.py
(ligne 10). - Si l'on assigne la valeur
1
à l'optionshowvalue
des widgetsScale()
, 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ètrephase
. - L'option
troughcolor
des widgetsScale()
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. - Modifiez le script de telle manière que les widgets curseurs soient écartés davantage les uns des autres (options
padx
etpady
de la méthodepack()
).
Solution
- Réfléchissez !
- Réfléchissez !
- Réfléchissez !
- Réfléchissez !
Intégration de widgets composites dans une application synthèse
[modifier | modifier le wikicode]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 | modifier le wikicode]- 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 listeself.trace
pour mémoriser les références de ces objets graphiques, et enfin une listeself.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 classeCanvas()
, 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ètreself
. 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 classeTk()
). 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'attributmaster
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éthodedelete()
: le widgetOscilloGraphe()
a hérité cette méthode de sa classe parenteCanvas()
. 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éthodetraceCourbe()
de notre widgetOscilloGraphe()
. Les numéros de référence de nos dessins sont mémorisés dans la listeself.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 listeself.controle
. Nous pouvons récupérer ces attributs en utilisant la qualification des noms par points.
Exercices
- 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) :
- 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'
, ... - 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 appellerezvaleurs()
, 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>
- É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. - É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. - 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
- 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
- Réfléchissez !
- Réfléchissez !
- Réfléchissez !
- Réfléchissez !
- Réfléchissez !
-
# 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()
-
(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()
Notes
[modifier | modifier le wikicode]- ↑ 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.
- ↑ 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. - ↑ 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.
- ↑ 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 | modifier le wikicode]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 | modifier le wikicode]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 | modifier le wikicode]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 | modifier le wikicode]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 | modifier le wikicode]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 estMonfichier
(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 modew
(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')
- Exemple :
- 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 dewrite()
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 | modifier le wikicode]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 :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 = open('Monficier','r') IOError: [Errno 2] No such file or directory: 'Monficier'
ofi
, faisant référence au fichier réelMonfichier
, lequel est ouvert en lecture (argumentr
). - 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 | modifier le wikicode]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 | modifier le wikicode]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éthodereadline()
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éthodereadlines()
renvoie une liste. À la fin du fichier,readline()
renvoie une chaîne vide, tandis quereadlines()
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 | modifier le wikicode]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 | modifier le wikicode]La fonction os.listdir()
liste les fichiers du dossier courant, ou ceux du dossier en paramètre s'il est mentioné.
Exercices
[modifier | modifier le wikicode]Exercices
- Écrivez un script qui compte dans un fichier texte quelconque le nombre de lignes contenant des caractères numériques.
- Écrivez un script qui compte le nombre de mots contenus dans un fichier texte.
- Écrivez un script qui recopie un fichier texte en veillant à ce que chaque ligne commence par une majuscule.
- Écrivez un script qui recopie un fichier texte en fusionnant (avec la précédente) les lignes qui ne commencent pas par une majuscule.
- 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. - 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
- Réfléchissez !
-
# 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))
-
# 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()
-
# 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()
-
# 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()
-
# 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
- É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). - 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.
- É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).
- Écrivez un script qui recopie un fichier texte en triplant tous les espaces entre les mots.
- 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). - Écrivez un script qui compare les contenus de deux fichiers et signale la première différence rencontrée.
- À 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.
- É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)
- É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).
- 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é.
- 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.
- É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
-
#(é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()
- Réfléchissez !
-
#(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()
- Réfléchissez !
-
# 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."
- Réfléchissez !
-
# 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()
-
# 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()
-
# 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()
-
# 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()
- Réfléchissez !
- Réfléchissez !
Exercices
- 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.
Juliette@18#1.67 Jean-Pierre@17#1.78 Delphine@19#1.71 Anne-Marie@17#1.63 etc.
- 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
-
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()
-
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 | modifier le wikicode] 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 | modifier le wikicode]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 | modifier le wikicode]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 | modifier le wikicode]- ↑ 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
. - ↑ 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émenttruc
, qui fait partie de l'ensemblemachin
, lequel fait lui-même partie de l'ensemblesysteme
. Nous verrons de nombreux exemples de cette technique de désignation, notamment lors de notre étude des classes d'objets. - ↑ 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).
- ↑ 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 | modifier le wikicode]
Exceptions
Principe
[modifier | modifier le wikicode]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
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 | modifier le wikicode]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 | modifier le wikicode]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 | modifier le wikicode]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 | modifier le wikicode]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 :
MySQL
[modifier | modifier le wikicode]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()
- On ouvre l'accès aux fonctions du Module
MySQLdb
. - 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).
- La ligne trois définit l'objet "cursor" qui va servir d'interface avec la base de données.
- On prépare ensuite la commande en langage SQL (ce qui pourrait aussi être défini en tout début du programme).
- On exécute cette requête dans la base.
- On formate les données brutes du résultat
- 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 | modifier le wikicode]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 | modifier le wikicode]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 | modifier le wikicode]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 | modifier le wikicode]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 | modifier le wikicode]- (anglais) documentation SQLite
- (anglais) Psycopg2 (module PostgreSQL)
- (anglais) module MySQL
- (anglais) module IBM Informix Dynamic Server
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 | modifier le wikicode]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 | modifier le wikicode]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 | modifier le wikicode]É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 | modifier le wikicode]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 | modifier le wikicode]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 | modifier le wikicode]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 | modifier le wikicode]Exercices
- 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
-
(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
.
La requête select
[modifier | modifier le wikicode]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 | modifier le wikicode]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 | modifier le wikicode]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.
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 | modifier le wikicode]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 modulesys
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'instructionreturn
), 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 (variableself.echec
), que le programme appelant peut ensuite tester quand bon lui semble.
- 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 instructionprint 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 | modifier le wikicode]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éthodecreerTables()
de la classeGestionBD()
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 bouclefor
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 | modifier le wikicode]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 fonctionexit()
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
- 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.
- 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
- Réfléchissez !
- Réfléchissez !
Notes
[modifier | modifier le wikicode]- ↑ 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, ...
- ↑ 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.
- ↑ 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.).
- ↑ Le module Gadfly est disponible gratuitement sur l'internet. Voir http://sourceforge.net/projects/gadfly
- ↑ 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.
- ↑ 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 | modifier le wikicode]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 | modifier le wikicode]- 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 | modifier le wikicode]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 | modifier le wikicode]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 | modifier le wikicode]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.