« Programmation Python/Gestion d'une base de données » : différence entre les versions

Contenu supprimé Contenu ajouté
Tavernier (discussion | contributions)
découpage en paragraphes
Tavernier (discussion | contributions)
adaptation en wikisyntaxe, typo, retouches
Ligne 1 :
{{todo|mettreajouter en formeimages}}
 
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 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 ==
 
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 page 109.
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 page {{todo}}.
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.
 
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.)
 
Nous verrons plus loin comment représenter des tables de données et les relations qui les lient.
 
=== SGBDR - Le modèle client/serveur ===
 
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''[[w:IBM|IBM]]'', Oracle''[[w:Oracle|Oracle]]'', Microsoft''[[w:Microsoft|Microsoft]]'', Informix''[[w:Informix|Informix]]'', Sybase''[[w:Sybase|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''[[PostgreSQL]]'', MySQL ...''[[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.
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ées1. 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.
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<ref>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, ...</ref>. 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 ===
 
É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<ref>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.</ref>.
 
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 oeuvre 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.
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 oeuvre 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ément3. Il intègre un large sous-ensemble de commandes SQL. Ses performances ne sont évidemment pas comparables à celles d'un gros SGBDR spécialisé4, 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.
 
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ément3. Il intègre un large sous-ensemble de commandes SQL. Ses performances ne sont évidemment pas comparables à celles d'un gros SGBDR spécialisé<ref>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.).</ref>, 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.
 
Ligne 36 ⟶ 51 :
=== Création de la base de données ===
 
Comme vous vous y attendez certainement, il suffit d'importer le module gadfly pour accéder aux fonctionnalités correspondantes.<ref>Le module Gadfly est disponible gratuitement sur l'internet. Voir http://sourceforge.net/projects/gadfly
L'installation de ce module est décrite dans l'annexe A, page {{todo}}</ref>
 
Vous devez ensuite créer une instance (un objet) de la classe gadfly :
 
<pre>
import gadfly
baseDonn = gadfly.gadfly()
</pre>
 
L'objet <code>baseDonn</code> 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.
 
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 :
 
<pre>
baseDonn.startup("mydata","E:/Python/essais/gadfly")
</pre>
 
Le premier paramètre transmis, <code>mydata</code>, 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).
 
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.
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 :
Afin de pouvoir transmettre vos requêtes SQL à l'objet <code>baseDonn</code> , 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'' :
 
<pre>
cur = baseDonn.cursor()
cur.execute("create table membres (age integer, nom varchar, taille float)")
Ligne 56 ⟶ 84 :
cur.execute("Insert Into Membres(Age, Nom, Taille) Values (18,'Forcas',1.69)")
baseDonn.commit()
</pre>
 
La première des lignes ci-dessus crée l'objet curseur <code>cur</code>. 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 variable5) 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.
La seconde ligne crée une table nommée <code>membres</code>, 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<ref>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.</ref>) et le champ <code>taille</code>, 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.
 
A ce stade des opérations, les enregistrement n'ont pas encore été transférés dans de véritables fichiers sur disque. Il est donc possible de revenir en arrière, comme nous le verrons un peu plus loin. Le transfert sur disque est activé par la méthode commit() de la dernière ligne d'instructions.
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 <code>membres</code>.
 
À ce stade des opérations, les enregistrement n'ont pas encore été transférés dans de véritables fichiers sur disque. Il est donc possible de revenir en arrière, comme nous le verrons un peu plus loin. Le transfert sur disque est activé par la méthode <code>commit()</code> de la dernière ligne d'instructions.
 
=== Connexion à une base de données existante ===
 
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 :
 
<pre>
import gadfly
baseDonn = gadfly.gadfly("mydata","E:/Python/essais/gadfly")
</pre>
 
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 :
 
<pre>
cur = baseDonn.cursor()
cur.execute("select * from membres")
print cur.pp()
</pre>
 
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 ».
La méthode <code>pp()</code> 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). « <code>pp</code> » 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 :
 
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 <code>fetchall()</code>, laquelle renvoie une liste de tuples. Essayez par exemple :
 
<pre>
for x in cur.fetchall():
print x, x[0], x[1], x[2]
</pre>
 
Vous pouvez bien entendu ajouter des enregistrements supplémentaires :
 
<pre>
cur.execute("Insert Into Membres(Age, Nom, Taille) Values (19,'Ricard',1.75)")
</pre>
 
Pour modifier un ou plusieurs enregistrements, exécutez une requête du type :
 
<pre>
cur.execute("update membres set nom ='Gerart' where nom='Ricard'")
</pre>
 
Pour supprimer un ou plusieurs enregistrements, utilisez une requête telle que :
 
<pre>
cur.execute("delete from membres where nom='Gerart'")
</pre>
 
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 <code>baseDonn.commit()</code>.
 
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 :
 
<pre>
baseDonn.close()
</pre>
 
=== Recherches dans une base de données ===
 
{{Exercices}}
(16)Exercice
<ol>
16.1.Avant d'aller plus loin, et à titre d'exercice de synthèse, nous allons vous demander de créer entièrement vous-même une base de données « Musique » qui contiendra les deux tables suivantes (Cela représente un certain travail, mais il faut que vous puissiez disposer d'un certain nombre de données pour pouvoir expérimenter les fonctions de recherche et de tri) :
<li>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 <code>Musique</code> 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
 
{|
Compositeurs
|-style="vertical-align:top"
comp (chaîne)
|
{|class="wikitable"
!oeuvres
|-
|<code>comp</code> (chaîne)
|-
|<code>titre</code> (chaîne)
|-
|<code>duree</code> (entier)
|-
|<code>interpr</code> (chaîne)
|}
 
|
comp (chaîne)
titre (chaîne)
 
{|class="wikitable"
a_naiss (entier)
!compositeurs
duree (entier)
|-
|<code>comp</code> (chaîne)
|-
|<code>a_naiss</code> (entier)
|-
|<code>a_mort</code> (entier)
|}
 
|}
a_mort (entier)
interpr (chaîne)
 
</li>
</ol>
{{fin}}
 
Commencez à remplir la table Compositeurs<code>compositeurs</code> 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
 
<pre>
Mozart 1756 1791
comp a_naiss a_mort
Beethoven 1770 1827
 
Handel 1685 1759
Mozart 1756 1791
Schubert 1797 1828
Beethoven 1770 1827
Vivaldi 1678 1741
Handel 1685 1759
Monteverdi 1567 1643
Schubert 1797 1828
Chopin 1810 1849
Vivaldi 1678 1741
Bach 1685 1750
Monteverdi 1567 1643
Chopin 1810 1849
Bach 1685 1750
</pre>
 
Dans la table oeuvres, entrez les données suivantes :
comp titre duree interpr
 
<pre>
Vivaldi Les quatre saisons 20 T. Pinnock
comp titre duree interpr
Mozart Concerto piano N°12 25 M. Perahia
 
Brahms Concerto violon N°2 40 A. Grumiaux
Vivaldi Les quatre saisons 20 T. Pinnock
Beethoven Sonate "au clair de lune" 14 W. Kempf
Mozart Concerto piano N°12 25 M. Perahia
Beethoven Sonate "pathétique" 17 W. Kempf
Brahms Concerto violon N°2 40 A. Grumiaux
Schubert Quintette "la truite" 39 SE of London
Beethoven Sonate "au clair de lune" 14 W. Kempf
Haydn La création 109 H. Von Karajan
Beethoven Sonate "pathétique" 17 W. Kempf
Chopin Concerto piano N°1 42 M.J. Pires
Schubert Quintette "la truite" 39 SE of London
Bach Toccata & fugue 9 P. Burmester
Haydn La création 109 H. Von Karajan
Beethoven Concerto piano N°4 33 M. Pollini
Chopin Concerto piano N°1 42 M.J. Pires
Mozart Symphonie N°40 29 F. Bruggen
Bach Toccata & fugue 9 P. Burmester
Mozart Concerto piano N°22 35 S. Richter
Beethoven Concerto piano N°3 37 S4 33 M. RichterPollini
Mozart Symphonie N°40 29 F. Bruggen
Mozart Concerto piano N°22 35 S. Richter
Beethoven Concerto piano N°3 37 S. Richter
</pre>
 
 
Les champs <code>a_naiss</code> et <code>a_mort</code> 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 dans les annexes de ces notes, à la page 356).
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 exemplevoir 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 <code>commit()</code> n'a pas été invoquée.
 
<pre>
# Utilisation d'une petite base de données acceptant les requêtes SQL
 
Ligne 169 ⟶ 257 :
else:
baseDonn.close()
</pre>
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 119.
 
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 {{todo}}.
 
=== La requête select ===
 
L'une des instructions les plus puissantes du langage SQL est l'instruction <code>select</code>, 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 :
 
<pre>
select * from oeuvres
select * from oeuvres where comp = 'Mozart'
Ligne 184 ⟶ 277 :
select sum(duree) from oeuvres where comp='Beethoven'
select * from oeuvres where duree >35 order by duree desc
</pre>
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.
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.
 
<pre>
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
Ligne 191 ⟶ 287 :
select comp from compositeurs except select comp from oeuvres
select distinct comp from oeuvres union select comp from compositeurs
</pre>
 
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.).
Ligne 197 ⟶ 294 :
 
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 oeuvre 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 personnel6.
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 :
 
<pre>
mysqladmin -u root password xxxx
</pre>
 
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é) :
 
<pre>
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
</pre>
 
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 :
 
<pre>
mysql -u jules -p
create database discotheque;
\q
</pre>
 
... 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 ===
 
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.
Au contraire, il est hautement recommandable ''de décrire plutôt la structure complète de la base de données en un seul endroit du programme'', et d'utiliser ensuite cette description comme référence pour la génération semi-automatique des instructions particulières concernant telle table ou tel champ. On évite ainsi, dans une large mesure, le cauchemar de devoir traquer et modifier un grand nombre d'instructions un peu partout dans le code, chaque fois que la structure de la base de données change un tant soit peu. Au lieu de cela, il suffit de changer seulement la description de référence, et la plus grosse partie du code reste correcte sans nécessiter de modification.
Nous tenons là une idée maîtresse pour réaliser des applications robustes :
 
Un logiciel destiné au traitement de données devrait toujours être construit sur la base d'un dictionnaire d'application.
{{remarque|Nous tenons là une idée maîtresse pour réaliser des applications robustes : un logiciel destiné au traitement de données devrait toujours être construit sur la base d'un dictionnaire d'application.}}

Ce que nous entendons ici par « dictionnaire d'application » ne doit pas nécessairement revêtir la forme d'un dictionnaire Python. N'importe quelle structure de données peut convenir, l'essentiel étant de se construire une ''référence centrale décrivant les données'' que l'on se propose de manipuler, avec peut-être aussi un certain nombre d'informations concernant leur mise en forme.
 
Du fait de leur capacité à rassembler en une même entité des données de n'importe quel type, les listes, tuples et dictionnaires de Python conviennent parfaitement pour ce travail. Dans l'exemple des pages suivantes, nous avons utilisé nous-mêmes un dictionnaire, dont les valeurs sont des listes de tuples, mais vous pourriez tout aussi bien opter pour une organisation différente des mêmes informations.
 
Tout cela étant bien établi, il nous reste encore à régler une question d'importance : où allons-nous installer concrètement ce dictionnaire d'application ?
 
Ses informations devront pouvoir être consultées depuis n'importe quel endroit du programme. Il semble donc obligatoire de l'installer dans une variable globale, de même d'ailleurs que d'autres données nécessaires au fonctionnement de l'ensemble de notre logiciel. Or vous savez que l'utilisation de variables globales n'est pas recommandée : elle comporte des risques, qui augmentent avec la taille du programme. De toute façon, les variables dites globales ne sont en fait globales qu'à l'intérieur d'un même module. Si nous souhaitons organiser notre logiciel comme un ensemble de modules (ce qui constitue par ailleurs une excellente pratique), nous n'aurons accès à nos variables globales que dans un seul d'entre eux.
Pour résoudre ce petit problème, il existe cependant une solution simple et élégante : regrouper dans une classe particulière toutes les variables qui nécessitent un statut global pour l'ensemble de l'application. Ainsi encapsulées dans l'espace de noms d'une classe, ces variables peuvent être utilisées sans problème dans n'importe quel module : il suffit en effet que celui-ci importe la classe en question. De plus, l'utilisation de cette technique entraîne une conséquence intéressante : le caractère « global » des variables définies de cette manière apparaît très clairement dans leur nom qualifié, puisque ce nom commence par celui de la classe contenante.
Si vous choisissez, par exemple, un nom explicite tel que Glob pour la classe destinée à accueillir vos variables « globales », vous vous assurez de devoir faire référence à ces variables partout dans votre code avec des noms tout aussi explicites tels que Glob.ceci , Glob.cela , etc7.
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. :
 
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.
1.class Glob:
2. """Espace de noms pour les variables et fonctions <pseudo-globales>"""
3.
4. dbName = "discotheque" # nom de la base de données
5. user = "jules" # propriétaire ou utilisateur
6. passwd = "abcde" # mot de passe d'accès
7. host = "192.168.0.235" # nom ou adresse IP du serveur
8.
9. # Structure de la base de données. Dictionnaire des tables & champs :
10. dicoT ={"compositeurs":[('id_comp', "k", "clé primaire"),
11. ('nom', 25, "nom"),
12. ('prenom', 25, "prénom"),
13. ('a_naiss', "i", "année de naissance"),
14. ('a_mort', "i", "année de mort")],
15. "oeuvres":[('id_oeuv', "k", "clé primaire"),
16. ('id_comp', "i", "clé compositeur"),
17. ('titre', 50, "titre de l'oeuvre"),
18. ('duree', "i", "durée (en minutes)"),
19. ('interpr', 30, "interprète principal")]}
 
Si vous choisissez, par exemple, un nom explicite tel que <code>Glob</code> 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<ref>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.</ref>.
 
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 <code>Glob()</code>, 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 <code>Glob.dbName</code> ; le nom ou l'adresse IP du serveur dans la variable <code>Glob.host</code>, etc. :
 
 
<pre>
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'oeuvre"),
('duree', "i", "durée (en minutes)"),
('interpr', 30, "interprète principal")]}
</pre>
 
Le dictionnaire d'application décrivant la structure de la base de données est contenu dans la variable <code>Glob.dicoT</code>.
 
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 oeuvre 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. Vous en verrez une démonstration un peu plus loin. 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 ===
 
La classe <code>Glob()</code> 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 <code>Glob()</code> 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.
 
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 <code>open()</code>. 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 <code>GestionBD()</code>, 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.
 
<pre>
1.import MySQLdb, sys
2.from dict_app import *
Ligne 320 ⟶ 469 :
70. if self.baseDonn:
71. self.baseDonn.close()
</pre>
 
;Commentaires
 
* Lignes 1-2 : Outre notre propre module <code>dict_app</code> qui contient les variables « globales », nous importons le module <code>sys</code> 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 n° 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 (voir page 119), car nous ne pouvons pas présumer que le serveur sera nécessairement accessible. Remarquons au passage que la méthode __init__() ne peut pas renvoyer de valeur (à l'aide de l'instruction return), du fait qu'elle est invoquée automatiquement par Python lors de l'instanciation d'un objet. En effet : ce qui est renvoyé dans ce cas au programme appelant est l'objet nouvellement construit. Nous ne pouvons donc pas signaler la réussite ou l'échec de la connexion au programme appelant à l'aide d'une valeur de retour. Une solution simple à ce petit problème consiste à mémoriser le résultat de la tentative de connexion dans un attribut d'instance (variable self.echec), que le programme appelant peut ensuite tester quand bon lui semble.
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.
 
* 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 nunmé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.
Si vous détaillez le code, vous constaterez qu'il consiste simplement à construire une requête SQL pour chaque table, morceau par morceau, dans la chaîne de caractères req. Celle-ci est ensuite transmise à la méthode executerReq() pour exécution. Si vous souhaitez visualiser la requête ainsi construite, vous pouvez évidemment ajouter une instruction « print req » juste après la ligne 40.
 
* Lignes 8 à 17 : Il est hautement recommandable de placer le code servant à établir la connexion à l'intérieur d'un gestionnaire d'exceptions <code>try-except-else</code> (voir page {{todo}}), car nous ne pouvons pas présumer que le serveur sera nécessairement accessible. Remarquons au passage que la méthode <code>__init__()</code> ne peut pas renvoyer de valeur (à l'aide de l'instruction <code>return</code>), du fait qu'elle est invoquée automatiquement par Python lors de l'instanciation d'un objet. En effet : ce qui est renvoyé dans ce cas au programme appelant est l'objet nouvellement construit. Nous ne pouvons donc pas signaler la réussite ou l'échec de la connexion au programme appelant à l'aide d'une valeur de retour. Une solution simple à ce petit problème consiste à mémoriser le résultat de la tentative de connexion dans un attribut d'instance (variable <code>self.echec</code>), que le programme appelant peut ensuite tester quand bon lui semble.
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.
<ul><li>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.
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.
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 <code>executerReq()</code> pour exécution. Si vous souhaitez visualiser la requête ainsi construite, vous pouvez évidemment ajouter une instruction <code>print req</code> 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.</li></ul>
 
* 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 <code>MySQLdb.connect()</code>, et l'objet curseur correspondant. Elles permettent de simplifier légèrement le code du programme appelant.
 
=== Construire un générateur de formulaires ===
 
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.
 
1.class Enregistreur:
<pre>
2. """classe pour gérer l'entrée d'enregistrements divers"""
class Enregistreur: #1
3. def __init__(self, bd, table):
"""classe pour gérer l'entrée d'enregistrements divers""" #2
4. self.bd =bd
def __init__(self, bd, table): #3
5. self.table =table
self.bd =bd #4
6. self.descriptif =Glob.dicoT[table] # descriptif des champs
self.table =table #5
7.
self.descriptif =Glob.dicoT[table] # descriptif des champs #6
8. def entrer(self):
#7
9. "procédure d'entrée d'un enregistrement entier"
10. def entrer(self): champs ="(" # ébauche de chaîne pour les noms de champs #8
11. valeurs ="(procédure d'entrée d'un enregistrement entier" # ébauche de chaîne pour les valeurs #9
champs ="(" # ébauche de chaîne pour les noms de champs #10
12. # Demander successivement une valeur pour chaque champ :
valeurs ="(" # ébauche de chaîne pour les valeurs #11
13. for cha, type, nom in self.descriptif:
14. # Demander successivement une ifvaleur typepour chaque champ =="k": # on ne demandera pas le d'enregistrement#12
15 for cha, type, nom in self.descriptif: continue # à l'utilisateur (numérotation auto.) #13
16. champsif type =="k": champs + cha +# ","on ne demandera pas le n° d'enregistrement #14
continue # à l'utilisateur (numérotation auto.) #15
17. val = raw_input("Entrez le champ %s :" % nom)
champs = champs + cha + "," #16
18. if type =="i":
19. val = raw_input("Entrez le valeurschamp =%s valeurs:" +% valnom) +"," #17
if type =="i": #18
20. else:
21. valeurs = valeurs + val +"'%s'," % (val) #19
else: #20
22.
23. champs valeurs = champs[:-1]valeurs + ")'%s'," % (val) # supprimer la dernière virgule, #21
#22
24. valeurs = valeurs[:-1] + ")" # ajouter une parenthèse
champs = champs[:-1] + ")" # supprimer la dernière virgule, #23
25. req ="INSERT INTO %s %s VALUES %s" % (self.table, champs, valeurs)
valeurs = valeurs[:-1] + ")" # ajouter une parenthèse #24
26. self.bd.executerReq(req)
req ="INSERT INTO %s %s VALUES %s" % (self.table, champs, valeurs) #25
27.
self.bd.executerReq(req) #26
28. ch =raw_input("Continuer (O/N) ? ")
#27
29. if ch.upper() == "O":
ch =raw_input("Continuer (O/N) ? ") #28
30. return 0
if ch.upper() == "O": #29
31. else:
32. return 10 #30
else: #31
return 1 #32
</pre>
 
;Commentaires
 
<ul>
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.
<li>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.</li>
Ligne 8 : Cette méthode entrer() génère le formulaire proprement dit. Elle prend en charge l'entrée des enregistrements dans la table, en s'adaptant à leur structure propre grâce au descriptif trouvé dans le dictionnaire.
Sa fonctionnalité concrète consiste encore une fois à construire morceau par morceau une chaîne de caractères qui deviendra une requête SQL, comme dans la méthode creerTables() de la classe GestionBD() décrite à la rubrique précédente.
 
<li>Ligne 8 : Cette méthode <code>entrer()</code> 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.
Vous pourriez bien entendu ajouter à la présente classe encore d'autres méthodes, pour gérer par exemple la suppression et/ou la modification d'enregistrements.
 
Lignes 12 à 21 : L'attribut d'instance self.descriptif contient une liste de tuples, et chacun de ceux-ci est fait de trois éléments, à savoir le nom d'un champ, le type de données qu'il est censé recevoir, et sa description « en clair ». La boucle for de la ligne 13 parcourt cette liste et affiche pour chaque champ un message d'invite construit sur la base de la description qui accompagne ce champ. Lorsque l'utilisateur a entré la valeur demandée, celle-ci et formatée dans une chaîne en construction. Le formatage s'adapte aux conventions du langage SQL, conformément au type requis pour le champ.
Sa fonctionnalité concrète consiste encore une fois à construire morceau par morceau une chaîne de caractères qui deviendra une requête SQL, comme dans la méthode <code>creerTables()</code> de la classe <code>GestionBD()</code> décrite à la rubrique précédente.
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.
 
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.</li>
 
<li>Lignes 12 à 21 : L'attribut d'instance <code>self.descriptif</code> contient une liste de tuples, et chacun de ceux-ci est fait de trois éléments, à savoir le nom d'un champ, le type de données qu'il est censé recevoir, et sa description « en clair ». La boucle <code>for</code> 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.</li>
 
<li>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 <code>print req</code> juste après la ligne 25.</li>
</ul>
 
=== Le corps de l'application ===
 
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 !
 
<pre>
1.###### Programme principal : #########
2.
Ligne 435 ⟶ 604 :
48. bd.close()
49. break
</pre>
 
;Commentaires
 
On supposera bien évidemment que les classes décrites plus haut soient présentes dans le même script, ou qu'elles aient été importées.
Lignes 3 à 6 : L'objet-interface est créé ici. Si la création échoue, l'attribut d'instance bd.echec contient la valeur 1. Le test des lignes 5 et 6 permet alors d'arrêter l'application immédiatement (la fonction exit() du module sys sert spécifiquement à cela).
Ligne 8 : Le reste de l'application consiste à proposer sans cesse le même menu, jusqu'à ce que l'utilisateur choisisse l'option n° 9.
Lignes 27 et 28 : La classe Enregistreur() accepte de gérer les enregistrements de n'importe quelle table. Afin de déterminer laquelle doit être utilisée lors de l'instanciation, on utilise un petit dictionnaire qui indique quel nom retenir, en fonction du choix opéré par l'utilisateur (option n° 3 ou n° 4).
Lignes 29 à 31 : La méthode entrer() de l'objet-enregistreur renvoie une valeur 0 ou 1 suivant que l'utilisateur ait choisi de continuer à entrer des enregistrements, ou bien d'arrêter. Le test de cette valeur permet d'interrompre la boucle de répétition en conséquence.
Lignes 35 et 44 : La méthode executerReq() renvoie une valeur 0 ou 1 suivant que la requête ait été acceptée ou non par le serveur. On peut donc tester cette valeur pour décider si le résultat doit être affiché ou non.
 
* Lignes 3 à 6 : L'objet-interface est créé ici. Si la création échoue, l'attribut d'instance <code>bd.echec</code> contient la valeur 1. Le test des lignes 5 et 6 permet alors d'arrêter l'application immédiatement (la fonction <code>exit()</code> du module sys sert spécifiquement à cela).
Exercices :
 
16.2.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.
* 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.
16.3.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.
 
* Lignes 27 et 28 : La classe <code>Enregistreur()</code> 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 <code>entrer()</code> 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 <code>executerReq()</code> 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.
{{fin}}
 
== Notes ==
{{références}}